vh-bombsquad-modded-server-.../dist/ba_data/python/efro/entity/_entity.py
2024-02-26 00:17:10 +05:30

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.
"""