mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
222 lines
8.7 KiB
Python
222 lines
8.7 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""Functionality for the actual Entity types."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING, TypeVar
|
|
|
|
from efro.entity._support import FieldInspector, BoundCompoundValue
|
|
from efro.entity._value import CompoundValue
|
|
from efro.json import ExtendedJSONEncoder, ExtendedJSONDecoder
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Dict, Any, Type, Union, Optional
|
|
|
|
T = TypeVar('T', bound='EntityMixin')
|
|
|
|
|
|
class EntityMixin:
|
|
"""Mixin class to add data-storage to CompoundValue, forming an Entity.
|
|
|
|
Distinct Entity types should inherit from this first and a CompoundValue
|
|
(sub)type second. This order ensures that constructor arguments for this
|
|
class are accessible on the new type.
|
|
"""
|
|
|
|
def __init__(self,
|
|
d_data: Dict[str, Any] = None,
|
|
error: bool = True) -> None:
|
|
super().__init__()
|
|
if not isinstance(self, CompoundValue):
|
|
raise RuntimeError('EntityMixin class must be combined'
|
|
' with a CompoundValue class.')
|
|
|
|
# Underlying data for this entity; fields simply operate on this.
|
|
self.d_data: Dict[str, Any] = {}
|
|
assert isinstance(self, EntityMixin)
|
|
self.set_data(d_data if d_data is not None else {}, error=error)
|
|
|
|
def reset(self) -> None:
|
|
"""Resets data to default."""
|
|
self.set_data({}, error=True)
|
|
|
|
def set_data(self, data: Dict, error: bool = True) -> None:
|
|
"""Set the data for this entity and apply all value filters to it.
|
|
|
|
Note that it is more efficient to pass data to an Entity's constructor
|
|
than it is to create a default Entity and then call this on it.
|
|
"""
|
|
assert isinstance(self, CompoundValue)
|
|
self.d_data = self.filter_input(data, error=error)
|
|
|
|
def copy_data(self, target: Union[CompoundValue,
|
|
BoundCompoundValue]) -> None:
|
|
"""Copy data from a target Entity or compound-value.
|
|
|
|
This first verifies that the target has a matching set of fields
|
|
and then copies its data into ourself. To copy data into a nested
|
|
compound field, the assignment operator can be used.
|
|
"""
|
|
import copy
|
|
from efro.entity.util import have_matching_fields
|
|
tvalue: CompoundValue
|
|
if isinstance(target, CompoundValue):
|
|
tvalue = target
|
|
elif isinstance(target, BoundCompoundValue):
|
|
tvalue = target.d_value
|
|
else:
|
|
raise TypeError(
|
|
'Target must be a CompoundValue or BoundCompoundValue')
|
|
target_data = getattr(target, 'd_data', None)
|
|
if target_data is None:
|
|
raise ValueError('Target is not bound to data.')
|
|
assert isinstance(self, CompoundValue)
|
|
if not have_matching_fields(self, tvalue):
|
|
raise ValueError(
|
|
f'Fields for target {type(tvalue)} do not match ours'
|
|
f" ({type(self)}); can't copy data.")
|
|
self.d_data = copy.deepcopy(target_data)
|
|
|
|
def steal_data(self, target: EntityMixin) -> None:
|
|
"""Steal data from another entity.
|
|
|
|
This is more efficient than copy_data, as data is moved instead
|
|
of copied. However this leaves the target object in an invalid
|
|
state, and it must no longer be used after this call.
|
|
This can be convenient for entities to use to update themselves
|
|
with the result of a database transaction (which generally return
|
|
fresh entities).
|
|
"""
|
|
from efro.entity.util import have_matching_fields
|
|
if not isinstance(target, EntityMixin):
|
|
raise TypeError('EntityMixin is required.')
|
|
assert isinstance(target, CompoundValue)
|
|
assert isinstance(self, CompoundValue)
|
|
if not have_matching_fields(self, target):
|
|
raise ValueError(
|
|
f'Fields for target {type(target)} do not match ours'
|
|
f" ({type(self)}); can't steal data.")
|
|
assert target.d_data is not None
|
|
self.d_data = target.d_data
|
|
|
|
# Make sure target blows up if someone tries to use it.
|
|
# noinspection PyTypeHints
|
|
target.d_data = None # type: ignore
|
|
|
|
def pruned_data(self) -> Dict[str, Any]:
|
|
"""Return a pruned version of this instance's data.
|
|
|
|
This varies from d_data in that values may be stripped out if
|
|
they are equal to defaults (for fields with that option enabled).
|
|
"""
|
|
import copy
|
|
data = copy.deepcopy(self.d_data)
|
|
assert isinstance(self, CompoundValue)
|
|
self.prune_fields_data(data)
|
|
return data
|
|
|
|
def to_json_str(self,
|
|
prune: bool = True,
|
|
pretty: bool = False,
|
|
sort_keys_override: Optional[bool] = None) -> str:
|
|
"""Convert the entity to a json string.
|
|
|
|
This uses efro.jsontools.ExtendedJSONEncoder/Decoder
|
|
to support data types not natively storable in json.
|
|
Be sure to use the corresponding loading functions here for
|
|
this same reason.
|
|
By default, keys are sorted when pretty-printing and not otherwise,
|
|
but this can be overridden by passing a bool as sort_keys_override.
|
|
"""
|
|
if prune:
|
|
data = self.pruned_data()
|
|
else:
|
|
data = self.d_data
|
|
if pretty:
|
|
return json.dumps(
|
|
data,
|
|
indent=2,
|
|
sort_keys=(sort_keys_override
|
|
if sort_keys_override is not None else True),
|
|
cls=ExtendedJSONEncoder)
|
|
|
|
# When not doing pretty, go for quick and compact.
|
|
return json.dumps(data,
|
|
separators=(',', ':'),
|
|
sort_keys=(sort_keys_override if sort_keys_override
|
|
is not None else False),
|
|
cls=ExtendedJSONEncoder)
|
|
|
|
@staticmethod
|
|
def json_loads(s: Union[str, bytes]) -> Any:
|
|
"""Load a json string using our special extended decoder.
|
|
|
|
Note that this simply returns loaded json data; no
|
|
Entities are involved.
|
|
"""
|
|
return json.loads(s, cls=ExtendedJSONDecoder)
|
|
|
|
def load_from_json_str(self,
|
|
s: Union[str, bytes],
|
|
error: bool = True) -> None:
|
|
"""Set the entity's data in-place from a json string.
|
|
|
|
The 'error' argument determines whether Exceptions will be raised
|
|
for invalid data values. Values will be reset/conformed to valid ones
|
|
if error is False. Note that Exceptions will always be raised
|
|
in the case of invalid formatted json.
|
|
"""
|
|
data = self.json_loads(s)
|
|
self.set_data(data, error=error)
|
|
|
|
@classmethod
|
|
def from_json_str(cls: Type[T],
|
|
s: Union[str, bytes],
|
|
error: bool = True) -> T:
|
|
"""Instantiate a new instance with provided json string.
|
|
|
|
The 'error' argument determines whether exceptions will be raised
|
|
on invalid data values. Values will be reset/conformed to valid ones
|
|
if error is False. Note that exceptions will always be raised
|
|
in the case of invalid formatted json.
|
|
"""
|
|
obj = cls(d_data=cls.json_loads(s), error=error)
|
|
return obj
|
|
|
|
# Note: though d_fields actually returns a FieldInspector,
|
|
# in type-checking-land we currently just say it returns self.
|
|
# This allows the type-checker to at least validate subfield access,
|
|
# though the types will be incorrect (values instead of inspectors).
|
|
# This means that anything taking FieldInspectors needs to take 'Any'
|
|
# at the moment. Hopefully we can make this cleaner via a mypy
|
|
# plugin at some point.
|
|
if TYPE_CHECKING:
|
|
|
|
@property
|
|
def d_fields(self: T) -> T:
|
|
"""For accessing entity field objects (as opposed to values)."""
|
|
...
|
|
else:
|
|
|
|
@property
|
|
def d_fields(self):
|
|
"""For accessing entity field objects (as opposed to values)."""
|
|
return FieldInspector(self, self, [], [])
|
|
|
|
|
|
class Entity(EntityMixin, CompoundValue):
|
|
"""A data class consisting of Fields and their underlying data.
|
|
|
|
Fields and Values simply define a data layout; Entities are concrete
|
|
objects using those layouts.
|
|
|
|
Inherit from this class and add Fields to define a simple Entity type.
|
|
Alternately, combine an EntityMixin with any CompoundValue child class
|
|
to accomplish the same. The latter allows sharing CompoundValue
|
|
layouts between different concrete Entity types. For example, a
|
|
'Weapon' CompoundValue could be embedded as part of a 'Character'
|
|
Entity but also exist as a distinct 'WeaponEntity' in an armory
|
|
database.
|
|
"""
|