diff --git a/addons/source-python/docs/source-python/source/developing/modules/core.cache.rst b/addons/source-python/docs/source-python/source/developing/modules/core.cache.rst
new file mode 100644
index 000000000..9e0fcdebf
--- /dev/null
+++ b/addons/source-python/docs/source-python/source/developing/modules/core.cache.rst
@@ -0,0 +1,12 @@
+core.cache module
+==================
+
+.. automodule:: core.cache
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: _core._cache.CachedProperty
+ :members:
+ :undoc-members:
+ :show-inheritance:
\ No newline at end of file
diff --git a/addons/source-python/packages/source-python/core/cache.py b/addons/source-python/packages/source-python/core/cache.py
new file mode 100644
index 000000000..6285f101f
--- /dev/null
+++ b/addons/source-python/packages/source-python/core/cache.py
@@ -0,0 +1,77 @@
+# ../core/cache.py
+
+"""Provides caching functionality.
+
+.. data:: cached_property
+ An alias of :class:`core.cache.CachedProperty`.
+"""
+
+
+# =============================================================================
+# >> IMPORTS
+# =============================================================================
+# Python Imports
+# FuncTools
+from functools import wraps
+# Types
+from types import MethodType
+
+
+# =============================================================================
+# >> FORWARD IMPORTS
+# =============================================================================
+# Source.Python Imports
+# Core
+from _core._cache import CachedProperty
+from _core._cache import cached_property
+
+
+# =============================================================================
+# >> ALL DECLARATION
+# =============================================================================
+__all__ = [
+ 'CachedProperty',
+ 'cached_property'
+ 'cached_result'
+]
+
+
+# =============================================================================
+# >> FUNCTIONS
+# =============================================================================
+def cached_result(fget):
+ """Decorator used to register a cached method.
+
+ :param function fget:
+ Method that is only called once and its result cached for subsequent
+ calls.
+ :rtype: CachedProperty
+ """
+ # Get a dummy object as default cache, so that we can cache None
+ NONE = object()
+
+ def getter(self):
+ """Getter function that generates the cached method."""
+ # Set our cache to the default value
+ cache = NONE
+
+ # Wrap the decorated method as the inner function
+ @wraps(fget)
+ def wrapper(self, *args, **kwargs):
+ """Calls the decorated method and cache the result."""
+ nonlocal cache
+
+ # Did we cache a result already?
+ if cache is NONE:
+
+ # No cache, let's call the wrapped method and cache the result
+ cache = fget(self, *args, **kwargs)
+
+ # Return the cached result
+ return cache
+
+ # Bind the wrapper function to the passed instance and return it
+ return MethodType(wrapper, self)
+
+ # Return a cached property bound to the getter function
+ return CachedProperty(getter, doc=fget.__doc__)
diff --git a/addons/source-python/packages/source-python/core/dumps.py b/addons/source-python/packages/source-python/core/dumps.py
index dcec9052a..48f85b68b 100644
--- a/addons/source-python/packages/source-python/core/dumps.py
+++ b/addons/source-python/packages/source-python/core/dumps.py
@@ -17,6 +17,7 @@
from entities import ServerClassGenerator
from entities.datamaps import FieldType
from entities.entity import BaseEntity
+from entities.props import SendPropFlags
from entities.props import SendPropType
# Memory
from memory import CLASS_INFO
@@ -259,7 +260,10 @@ def _dump_server_class_table(table, open_file, level=1, offset=0):
for prop in table:
# Skip all baseclasses
- if prop.name == 'baseclass':
+ if (prop.name == 'baseclass' or
+ prop.is_exclude_prop() or
+ prop.flags & SendPropFlags.COLLAPSIBLE
+ ):
continue
# Get the current offset in case this
diff --git a/addons/source-python/packages/source-python/engines/sound.py b/addons/source-python/packages/source-python/engines/sound.py
index e0d1aa1e7..00a955183 100644
--- a/addons/source-python/packages/source-python/engines/sound.py
+++ b/addons/source-python/packages/source-python/engines/sound.py
@@ -22,7 +22,7 @@
# Source.Python Imports
# Core
-from core import AutoUnload
+from core import WeakAutoUnload
# Engines
from engines import engines_logger
# Entities
@@ -100,7 +100,7 @@ class Attenuation(float, Enum):
# =============================================================================
# >> CLASSES
# =============================================================================
-class _BaseSound(AutoUnload):
+class _BaseSound(WeakAutoUnload):
"""Base class for sound classes."""
# Set the base _downloads attribute to know whether
diff --git a/addons/source-python/packages/source-python/engines/trace.py b/addons/source-python/packages/source-python/engines/trace.py
index fc07a252a..450763437 100644
--- a/addons/source-python/packages/source-python/engines/trace.py
+++ b/addons/source-python/packages/source-python/engines/trace.py
@@ -204,7 +204,7 @@ def __init__(self, ignore=(), trace_type=TraceType.EVERYTHING):
"""
super().__init__()
self.trace_type = trace_type
- self.ignore = tuple(map(inthandle_from_baseentity, ignore))
+ self.ignore = set(map(inthandle_from_baseentity, ignore))
def should_hit_entity(self, entity, mask):
"""Called when a trace is about to hit an entity.
@@ -215,14 +215,7 @@ def should_hit_entity(self, entity, mask):
The mask that was used to intialize the trace.
:rtype: bool
"""
- entity_inthandle = entity.basehandle.to_int()
-
- # Check for entities to ignore
- for ignore_inthandle in self.ignore:
- if ignore_inthandle == entity_inthandle:
- return False
-
- return True
+ return entity.basehandle.to_int() not in self.ignore
def get_trace_type(self):
"""Return the trace type.
diff --git a/addons/source-python/packages/source-python/entities/_base.py b/addons/source-python/packages/source-python/entities/_base.py
index 47528eead..eec4b211a 100644
--- a/addons/source-python/packages/source-python/entities/_base.py
+++ b/addons/source-python/packages/source-python/entities/_base.py
@@ -19,12 +19,12 @@
# Core
from core import GAME_NAME
from core import BoostPythonClass
+from core.cache import cached_property
# Entities
from entities.constants import INVALID_ENTITY_INDEX
# Engines
from engines.precache import Model
from engines.sound import Attenuation
-from engines.sound import engine_sound
from engines.sound import Channel
from engines.sound import Pitch
from engines.sound import Sound
@@ -37,20 +37,16 @@
from engines.trace import Ray
from engines.trace import TraceFilterSimple
# Entities
-from _entities._entity import BaseEntity
-from entities import BaseEntityGenerator
from entities import TakeDamageInfo
from entities.classes import server_classes
+from entities.constants import WORLD_ENTITY_INDEX
from entities.constants import DamageTypes
-from entities.constants import RenderMode
from entities.helpers import index_from_inthandle
from entities.helpers import index_from_pointer
from entities.helpers import wrap_entity_mem_func
# Filters
from filters.weapons import WeaponClassIter
# Listeners
-from listeners import OnEntityDeleted
-from listeners import on_entity_deleted_listener_manager
from listeners.tick import Delay
from listeners.tick import Repeat
from listeners.tick import RepeatStatus
@@ -58,13 +54,21 @@
from mathlib import NULL_VECTOR
# Memory
from memory import make_object
+from memory.helpers import MemberFunction
# Players
from players.constants import HitGroup
# Studio
-from studio.cache import model_cache
from studio.constants import INVALID_ATTACHMENT_INDEX
+# =============================================================================
+# >> FORWARD IMPORTS
+# =============================================================================
+# Source.Python Imports
+# Entities
+from _entities._entity import BaseEntity
+
+
# =============================================================================
# >> GLOBAL VARIABLES
# =============================================================================
@@ -119,16 +123,23 @@ def __call__(cls, index, caching=None):
# Let's first lookup for a cached instance
if caching:
- obj = cls._cache.get(index, None)
- if obj is not None:
- return obj
+ try:
+ return cls._cache[index]
+ except KeyError:
+ pass
# Nothing in cache, let's create a new instance
obj = super().__call__(index)
# Let's cache the new instance we just created
if caching:
- cls._cache[index] = obj
+
+ # Only cache entities that are not marked for deletion.
+ # This is required, because if someone request an entity instance
+ # after we invalidated our cache but before the engine processed
+ # the deletion we would now have an invalid instance in the cache.
+ if not obj.is_marked_for_deletion():
+ cls._cache[index] = obj
# We are done, let's return the instance
return obj
@@ -188,29 +199,39 @@ def __init__(self, index, caching=True):
super().__init__(index)
# Set the entity's base attributes
- object.__setattr__(self, '_index', index)
+ type(self).index.set_cached_value(self, index)
def __hash__(self):
- """Return a hash value based on the entity index."""
+ """Return a hash value based on the entity inthandle."""
# Required for sets, because we have implemented __eq__
- return hash(self.index)
+ return hash(self.inthandle)
def __getattr__(self, attr):
"""Find if the attribute is valid and returns the appropriate value."""
# Loop through all of the entity's server classes
- for server_class in self.server_classes:
+ for instance in self.server_classes.values():
- # Does the current server class contain the given attribute?
- if hasattr(server_class, attr):
+ try:
+ # Get the attribute's value
+ value = getattr(instance, attr)
+ except AttributeError:
+ continue
+
+ # Is the value a dynamic function?
+ if isinstance(value, MemberFunction):
- # Return the attribute's value
- return getattr(make_object(server_class, self.pointer), attr)
+ # Cache the value
+ with suppress(AttributeError):
+ object.__setattr__(self, attr, value)
+
+ # Return the attribute's value
+ return value
# If the attribute is not found, raise an error
raise AttributeError('Attribute "{0}" not found'.format(attr))
def __setattr__(self, attr, value):
- """Find if the attribute is value and sets its value."""
+ """Find if the attribute is valid and sets its value."""
# Is the given attribute a property?
if (attr in super().__dir__() and isinstance(
getattr(self.__class__, attr, None), property)):
@@ -222,13 +243,13 @@ def __setattr__(self, attr, value):
return
# Loop through all of the entity's server classes
- for server_class in self.server_classes:
+ for server_class, instance in self.server_classes.items():
# Does the current server class contain the given attribute?
if hasattr(server_class, attr):
# Set the attribute's value
- setattr(server_class(self.pointer, wrap=True), attr, value)
+ setattr(instance, attr, value)
# No need to go further
return
@@ -254,58 +275,6 @@ def __dir__(self):
# Return a sorted list of attributes
return sorted(attributes)
- @classmethod
- def create(cls, classname):
- """Create a new networked entity with the given classname.
-
- :param str classname:
- Classname of the entity to create.
- :raise ValueError:
- Raised if the given classname is not a networked entity.
- """
- entity = BaseEntity.create(classname)
- if entity.is_networked():
- return cls(entity.index)
-
- entity.remove()
- raise ValueError('"{}" is not a networked entity.'.format(classname))
-
- @classmethod
- def find(cls, classname):
- """Try to find an entity with the given classname.
-
- If not entity has been found, None will be returned.
-
- :param str classname:
- The classname of the entity.
- :return:
- Return the found entity.
- :rtype: Entity
- """
- entity = BaseEntity.find(classname)
- if entity is not None and entity.is_networked():
- return cls(entity.index)
-
- return None
-
- @classmethod
- def find_or_create(cls, classname):
- """Try to find an entity with the given classname.
-
- If no entity has been found, it will be created.
-
- :param str classname:
- The classname of the entity.
- :return:
- Return the found or created entity.
- :rtype: Entity
- """
- entity = cls.find(classname)
- if entity is None:
- entity = cls.create(classname)
-
- return entity
-
@classmethod
def from_inthandle(cls, inthandle, caching=None):
"""Create an entity instance from an inthandle.
@@ -330,14 +299,6 @@ def is_networked(self):
"""
return True
- @property
- def index(self):
- """Return the entity's index.
-
- :rtype: int
- """
- return self._index
-
@property
def owner(self):
"""Return the entity's owner.
@@ -351,30 +312,47 @@ def owner(self):
except (ValueError, OverflowError):
return None
- @property
+ @cached_property
def server_classes(self):
"""Yield all server classes for the entity."""
- yield from server_classes.get_entity_server_classes(self)
+ return {
+ server_class: make_object(server_class, self.pointer) for
+ server_class in server_classes.get_entity_server_classes(self)
+ }
- @property
+ @cached_property
def properties(self):
"""Iterate over all descriptors available for the entity."""
+ properties = {}
for server_class in self.server_classes:
- yield from server_class.properties
+ for prop, data in server_class.properties.items():
+ properties[prop] = data
+ return properties
- @property
+ @cached_property
def inputs(self):
"""Iterate over all inputs available for the entity."""
- for server_class in self.server_classes:
- yield from server_class.inputs
-
- @property
+ inputs = {}
+ for server_class in self.server_classes.values():
+ for input in server_class.inputs:
+ inputs[input] = getattr(
+ make_object(
+ server_class._inputs, self.pointer
+ ),
+ input
+ )
+ return inputs
+
+ @cached_property
def outputs(self):
"""Iterate over all outputs available for the entity."""
+ outputs = {}
for server_class in self.server_classes:
- yield from server_class.outputs
+ for output in server_class.outputs:
+ outputs[output] = super().get_output(output)
+ return outputs
- @property
+ @cached_property
def keyvalues(self):
"""Iterate over all entity keyvalues available for the entity.
@@ -383,8 +361,11 @@ def keyvalues(self):
An entity might also have hardcoded keyvalues that can't be listed
with this property.
"""
+ keyvalues = {}
for server_class in self.server_classes:
- yield from server_class.keyvalues
+ for keyvalue, data in server_class.keyvalues.items():
+ keyvalues[keyvalue] = data
+ return keyvalues
def get_model(self):
"""Return the entity's model.
@@ -393,10 +374,11 @@ def get_model(self):
``None`` if the entity has no model.
:rtype: Model
"""
- if not self.model_name:
+ model_name = self.model_name
+ if not model_name:
return None
- return Model(self.model_name)
+ return Model(model_name)
def set_model(self, model):
"""Set the entity's model to the given model.
@@ -447,365 +429,6 @@ def _set_parent(self, parent):
.. seealso:: :meth:`get_parent` and :meth:`set_parent`""")
- def get_property_bool(self, name):
- """Return the boolean property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: bool
- """
- return self._get_property(name, 'bool')
-
- def get_property_color(self, name):
- """Return the Color property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: Color
- """
- return self._get_property(name, 'Color')
-
- def get_property_edict(self, name):
- """Return the Edict property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: Edict
- """
- return self._get_property(name, 'Edict')
-
- def get_property_float(self, name):
- """Return the float property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: float
- """
- return self._get_property(name, 'float')
-
- def get_property_int(self, name):
- """Return the integer property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: int
- """
- return self._get_property(name, 'int')
-
- def get_property_interval(self, name):
- """Return the Interval property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: Interval
- """
- return self._get_property(name, 'Interval')
-
- def get_property_pointer(self, name):
- """Return the pointer property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: Pointer
- """
- return self._get_property(name, 'pointer')
-
- def get_property_quaternion(self, name):
- """Return the Quaternion property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: Quaternion
- """
- return self._get_property(name, 'Quaternion')
-
- def get_property_short(self, name):
- """Return the short property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: int
- """
- return self._get_property(name, 'short')
-
- def get_property_ushort(self, name):
- """Return the ushort property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: int
- """
- return self._get_property(name, 'ushort')
-
- def get_property_string(self, name):
- """Return the string property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: str
- """
- return self._get_property(name, 'string_array')
-
- def get_property_string_pointer(self, name):
- """Return the string property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: str
- """
- return self._get_property(name, 'string_pointer')
-
- def get_property_char(self, name):
- """Return the char property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: str
- """
- return self._get_property(name, 'char')
-
- def get_property_uchar(self, name):
- """Return the uchar property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: int
- """
- return self._get_property(name, 'uchar')
-
- def get_property_uint(self, name):
- """Return the uint property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: int
- """
- return self._get_property(name, 'uint')
-
- def get_property_vector(self, name):
- """Return the Vector property.
-
- :param str name:
- Name of the property to retrieve.
- :rtype: Vector
- """
- return self._get_property(name, 'Vector')
-
- def _get_property(self, name, prop_type):
- """Verify the type and return the property."""
- # Loop through all entity server classes
- for server_class in self.server_classes:
-
- # Is the name a member of the current server class?
- if name not in server_class.properties:
- continue
-
- # Is the type the correct type?
- if prop_type != server_class.properties[name].prop_type:
- raise TypeError('Property "{0}" is of type {1} not {2}'.format(
- name, server_class.properties[name].prop_type, prop_type))
-
- # Return the property for the entity
- return getattr(
- make_object(server_class._properties, self.pointer), name)
-
- # Raise an error if the property name was not found
- raise ValueError(
- 'Property "{0}" not found for entity type "{1}"'.format(
- name, self.classname))
-
- def set_property_bool(self, name, value):
- """Set the boolean property.
-
- :param str name:
- Name of the property to set.
- :param bool value:
- The value to set.
- """
- self._set_property(name, 'bool', value)
-
- def set_property_color(self, name, value):
- """Set the Color property.
-
- :param str name:
- Name of the property to set.
- :param Color value:
- The value to set.
- """
- self._set_property(name, 'Color', value)
-
- def set_property_edict(self, name, value):
- """Set the Edict property.
-
- :param str name:
- Name of the property to set.
- :param Edict value:
- The value to set.
- """
- self._set_property(name, 'Edict', value)
-
- def set_property_float(self, name, value):
- """Set the float property.
-
- :param str name:
- Name of the property to set.
- :param float value:
- The value to set.
- """
- self._set_property(name, 'float', value)
-
- def set_property_int(self, name, value):
- """Set the integer property.
-
- :param str name:
- Name of the property to set.
- :param int value:
- The value to set.
- """
- self._set_property(name, 'int', value)
-
- def set_property_interval(self, name, value):
- """Set the Interval property.
-
- :param str name:
- Name of the property to set.
- :param Interval value:
- The value to set.
- """
- self._set_property(name, 'Interval', value)
-
- def set_property_pointer(self, name, value):
- """Set the pointer property.
-
- :param str name:
- Name of the property to set.
- :param Pointer value:
- The value to set.
- """
- self._set_property(name, 'pointer', value)
-
- def set_property_quaternion(self, name, value):
- """Set the Quaternion property.
-
- :param str name:
- Name of the property to set.
- :param Quaternion value:
- The value to set.
- """
- self._set_property(name, 'Quaternion', value)
-
- def set_property_short(self, name, value):
- """Set the short property.
-
- :param str name:
- Name of the property to set.
- :param int value:
- The value to set.
- """
- self._set_property(name, 'short', value)
-
- def set_property_ushort(self, name, value):
- """Set the ushort property.
-
- :param str name:
- Name of the property to set.
- :param int value:
- The value to set.
- """
- self._set_property(name, 'ushort', value)
-
- def set_property_string(self, name, value):
- """Set the string property.
-
- :param str name:
- Name of the property to set.
- :param str value:
- The value to set.
- """
- self._set_property(name, 'string_array', value)
-
- def set_property_string_pointer(self, name, value):
- """Set the string property.
-
- :param str name:
- Name of the property to set.
- :param str value:
- The value to set.
- """
- self._set_property(name, 'string_pointer', value)
-
- def set_property_char(self, name, value):
- """Set the char property.
-
- :param str name:
- Name of the property to set.
- :param str value:
- The value to set.
- """
- self._set_property(name, 'char', value)
-
- def set_property_uchar(self, name, value):
- """Set the uchar property.
-
- :param str name:
- Name of the property to set.
- :param int value:
- The value to set.
- """
- self._set_property(name, 'uchar', value)
-
- def set_property_uint(self, name, value):
- """Set the uint property.
-
- :param str name:
- Name of the property to set.
- :param int value:
- The value to set.
- """
- self._set_property(name, 'uint', value)
-
- def set_property_vector(self, name, value):
- """Set the Vector property.
-
- :param str name:
- Name of the property to set.
- :param Vector value:
- The value to set.
- """
- self._set_property(name, 'Vector', value)
-
- def _set_property(self, name, prop_type, value):
- """Verify the type and set the property."""
- # Loop through all entity server classes
- for server_class in self.server_classes:
-
- # Is the name a member of the current server class?
- if name not in server_class.properties:
- continue
-
- # Is the type the correct type?
- if prop_type != server_class.properties[name].prop_type:
- raise TypeError('Property "{0}" is of type {1} not {2}'.format(
- name, server_class.properties[name].prop_type, prop_type))
-
- # Set the property for the entity
- setattr(make_object(
- server_class._properties, self.pointer), name, value)
-
- # Is the property networked?
- if server_class.properties[name].networked:
-
- # Notify the change of state
- self.edict.state_changed()
-
- # No need to go further
- return
-
- # Raise an error if the property name was not found
- raise ValueError(
- 'Property "{0}" not found for entity type "{1}"'.format(
- name, self.classname))
-
def delay(
self, delay, callback, args=(), kwargs=None,
cancel_on_level_end=False):
@@ -827,18 +450,21 @@ def delay(
The delay instance.
:rtype: Delay
"""
+ # Get the index of the entity
+ index = self.index
+
# TODO: Ideally, we want to subclass Delay and cleanup on cancel() too
# in case the caller manually cancel the returned Delay.
def _callback(*args, **kwargs):
"""Called when the delay is executed."""
# Remove the delay from the global dictionary...
- _entity_delays[self.index].remove(delay)
+ _entity_delays[index].remove(delay)
# Was this the last pending delay for the entity?
- if not _entity_delays[self.index]:
+ if not _entity_delays[index]:
# Remove the entity from the dictionary...
- del _entity_delays[self.index]
+ del _entity_delays[index]
# Call the callback...
callback(*args, **kwargs)
@@ -847,14 +473,15 @@ def _callback(*args, **kwargs):
delay = Delay(delay, _callback, args, kwargs, cancel_on_level_end)
# Add the delay to the dictionary...
- _entity_delays[self.index].add(delay)
+ _entity_delays[index].add(delay)
# Return the delay instance...
return delay
- def repeat(self, callback, args=(), kwargs=None, cancel_on_level_end=False):
+ def repeat(
+ self, callback, args=(), kwargs=None,
+ cancel_on_level_end=False):
"""Create the repeat which will be stopped after removing the entity.
-
:param callback:
A callable object that should be called at the end of each loop.
:param tuple args:
@@ -879,29 +506,27 @@ def repeat(self, callback, args=(), kwargs=None, cancel_on_level_end=False):
# Return the repeat instance...
return repeat
+ def get_output(self, name):
+ """Return the output instance matching the given name.
+
+ :parma str name:
+ Name of the output.
+ :rtype: BaseEntityOutput
+ :raise KeyError:
+ Raised if the output instance wasn't found.
+ """
+ return self.outputs[name]
+
def get_input(self, name):
"""Return the input function matching the given name.
:parma str name:
Name of the input function.
:rtype: InputFunction
- :raise ValueError:
+ :raise KeyError:
Raised if the input function wasn't found.
"""
- # Loop through each server class for the entity
- for server_class in self.server_classes:
-
- # Does the current server class contain the input?
- if name in server_class.inputs:
-
- # Return the InputFunction instance for the given input name
- return getattr(
- make_object(server_class._inputs, self.pointer), name)
-
- # If no server class contains the input, raise an error
- raise ValueError(
- 'Unknown input "{0}" for entity type "{1}".'.format(
- name, self.classname))
+ return self.inputs[name]
def call_input(self, name, *args, **kwargs):
"""Call the input function matching the given name.
@@ -979,7 +604,7 @@ def emit_sound(
sound.play(*recipients)
def is_in_solid(
- self, mask=ContentMasks.ALL, generator=BaseEntityGenerator):
+ self, mask=ContentMasks.ALL, generator=None):
"""Return whether or not the entity is in solid.
:param ContentMasks mask:
@@ -989,15 +614,26 @@ def is_in_solid(
:class:`BaseEntity` instances that are ignored by the ray.
:rtype: bool
"""
+ # Get the entity's origin
+ origin = self.origin
+
# Get a Ray object of the entity physic box
- ray = Ray(self.origin, self.origin, self.mins, self.maxs)
+ ray = Ray(origin, origin, self.mins, self.maxs)
# Get a new GameTrace instance
trace = GameTrace()
# Do the trace
- engine_trace.trace_ray(ray, mask, TraceFilterSimple(
- generator()), trace)
+ if generator is None:
+
+ # No need to trace against anything but the world if we are going
+ # to filter out everything regardless.
+ engine_trace.clip_ray_to_entity(
+ ray, mask, BaseEntity(WORLD_ENTITY_INDEX), trace
+ )
+ else:
+ engine_trace.trace_ray(ray, mask, TraceFilterSimple(
+ generator()), trace)
# Return whether or not the trace did hit
return trace.did_hit()
@@ -1147,48 +783,34 @@ def set_parent(self, parent, attachment=INVALID_ATTACHMENT_INDEX):
# =============================================================================
# >> LISTENERS
# =============================================================================
-@OnEntityDeleted
+# NOTE: This callback is called by sp_main.cpp after all registered entity
+# deletion listeners have been called.
def _on_entity_deleted(base_entity):
"""Called when an entity is removed.
:param BaseEntity base_entity:
The removed entity.
"""
- # Make sure the entity is networkable...
- if not base_entity.is_networked():
+ try:
+ # Get the index of the entity...
+ index = base_entity.index
+ except ValueError:
return
- # Get the index of the entity...
- index = base_entity.index
-
- # Cleanup the cache
- for cls in _entity_classes:
- cls.cache.pop(index, None)
+ # Loop through all delays...
+ for delay in _entity_delays.pop(index, ()):
- # Was delay registered for this entity?
- if index in _entity_delays:
- # Loop through all delays...
- for delay in _entity_delays[index]:
-
- # Make sure the delay is still running...
- if not delay.running:
- continue
-
- # Cancel the delay...
+ # Cancel the delay...
+ with suppress(ValueError):
delay.cancel()
- # Remove the entity from the dictionary...
- del _entity_delays[index]
+ # Loop through all repeats...
+ for repeat in _entity_repeats.pop(index, ()):
- # Was repeat registered for this entity?
- if index in _entity_repeats:
- # Loop through all repeats...
- for repeat in _entity_repeats[index]:
+ # Stop the repeat if running
+ if repeat.status is RepeatStatus.RUNNING:
+ repeat.stop()
- # Stop the repeat if running
- if repeat.status is RepeatStatus.RUNNING:
- repeat.stop()
-
- # Remove the entity from the dictionary...
- del _entity_repeats[index]
-
+ # Invalidate the internal entity caches for this entity
+ for cls in _entity_classes:
+ cls.cache.pop(index, None)
diff --git a/addons/source-python/packages/source-python/entities/classes.py b/addons/source-python/packages/source-python/entities/classes.py
index 72dcc57c4..3ce665fc7 100644
--- a/addons/source-python/packages/source-python/entities/classes.py
+++ b/addons/source-python/packages/source-python/entities/classes.py
@@ -21,6 +21,7 @@
from core import GameConfigObj
from core import PLATFORM
# Entities
+from _entities._entity import BaseEntity
from entities import ServerClassGenerator
from entities.datamaps import _supported_input_types
from entities.datamaps import EntityProperty
@@ -35,6 +36,7 @@
from memory import Convention
from memory import DataType
from memory import get_object_pointer
+from memory import make_object
from memory.helpers import Type
from memory.manager import CustomType
from memory.manager import TypeManager
@@ -375,7 +377,10 @@ def _find_properties(self, table, base_name='', base_offset=0):
for prop in table:
# Is the property a base class or excluded?
- if prop.name == 'baseclass' or prop.flags & SendPropFlags.EXCLUDE:
+ if (prop.name == 'baseclass' or
+ prop.is_exclude_prop() or
+ prop.flags & SendPropFlags.COLLAPSIBLE
+ ):
continue
# Get the name of the property using the given base name
@@ -456,7 +461,7 @@ def _add_input(self, instance, name, desc, contents):
return
# Add the input to the inputs dictionary
- instance.inputs[name] = self.input(name, desc)
+ instance.inputs[name] = self.input(desc)
# Is the input a named input?
if name in contents:
@@ -516,10 +521,8 @@ def fset(pointer, value):
return property(fget, fset)
@staticmethod
- def input(name, desc):
+ def input(desc):
"""Input type DataMap object."""
- argument_type = desc.type
-
def fget(pointer):
"""Retrieve the InputFunction instance."""
func = desc.function
@@ -536,7 +539,7 @@ def fget(pointer):
(DataType.POINTER, DataType.POINTER),
DataType.VOID)
- return InputFunction(name, argument_type, function, pointer)
+ return InputFunction(desc, make_object(BaseEntity, pointer))
return property(fget)
diff --git a/addons/source-python/packages/source-python/entities/datamaps.py b/addons/source-python/packages/source-python/entities/datamaps.py
index fa417a231..f1a88b2ca 100644
--- a/addons/source-python/packages/source-python/entities/datamaps.py
+++ b/addons/source-python/packages/source-python/entities/datamaps.py
@@ -38,6 +38,7 @@
from _entities._datamaps import FTYPEDESC_VIEW_OWN_TEAM
from _entities._datamaps import FTYPEDESC_VIEW_NEVER
from _entities._datamaps import InputData
+from _entities._datamaps import InputFunction
from _entities._datamaps import Interval
from _entities._datamaps import TypeDescription
from _entities._datamaps import Variant
@@ -48,8 +49,10 @@
# =============================================================================
# Set all to an empty list
__all__ = ('DataMap',
+ 'EntityProperty',
'FieldType',
'InputData',
+ 'InputFunction',
'Interval',
'TypeDescription',
'TypeDescriptionFlags',
@@ -120,51 +123,3 @@ def prop_type(self):
def networked(self):
"""Return whether the property is networked."""
return self._networked
-
-
-class InputFunction(Function):
- """Class used to create and call an Input type function."""
-
- def __init__(self, name, argument_type, function, this):
- """Instantiate the function instance and store the base attributes."""
- super().__init__(function)
-
- self._name = name
- self._argument_type = argument_type
- self._this = this
-
- def __call__(self, value=None, caller=None, activator=None):
- """Call the stored function with the values given."""
- # Is the type not VOID but no value was given?
- if value is None and self._argument_type != FieldType.VOID:
- raise ValueError(
- 'Must provide a value for {0}'.format(self._name))
-
- # Is the type VOID but a value was given?
- if value is not None and self._argument_type == FieldType.VOID:
- raise ValueError(
- '{0} is type Void. Do not pass a value.'.format(
- self._name))
-
- # Get an InputData instance
- inputdata = InputData()
-
- # Does the caller need set?
- if caller is not None:
- inputdata.caller = caller
-
- # Does the activator need set?
- if activator is not None:
- inputdata.activator = activator
-
- # Does the function require a value?
- if self._argument_type != FieldType.VOID:
-
- # Set the value
- getattr(
- inputdata.value,
- 'set_{0}'.format(_supported_input_types[
- self._argument_type]))(value)
-
- # Call the function
- super().__call__(self._this, inputdata)
diff --git a/addons/source-python/packages/source-python/entities/dictionary.py b/addons/source-python/packages/source-python/entities/dictionary.py
index 8cbbb7827..922e361dd 100644
--- a/addons/source-python/packages/source-python/entities/dictionary.py
+++ b/addons/source-python/packages/source-python/entities/dictionary.py
@@ -5,6 +5,10 @@
# ============================================================================
# >> IMPORTS
# ============================================================================
+# Python Imports
+# ContextLib
+from contextlib import suppress
+
# Source.Python Imports
# Core
from core import AutoUnload
@@ -46,20 +50,22 @@ def __init__(self, factory=Entity, *args, **kwargs):
def __missing__(self, index):
"""Add and return the entity instance for the given index."""
- instance = self[index] = self._factory(index, *self._args,
- **self._kwargs)
+ instance = self._factory(index, *self._args, **self._kwargs)
+
+ # Only cache entities that are not marked for deletion.
+ # This is required, because if someone request an entity instance
+ # after we invalidated our cache but before the engine processed
+ # the deletion we would now have an invalid instance in the cache.
+ if not instance.is_marked_for_deletion():
+ self[index] = instance
+
return instance
def __delitem__(self, index):
"""Remove the given index from the dictionary."""
- # Is the given index not in the dictionary?
- if index not in self:
-
- # If so, no need to go further...
- return
-
# Remove the given index from the dictionary...
- super().__delitem__(index)
+ with suppress(KeyError):
+ super().__delitem__(index)
def from_inthandle(self, inthandle):
"""Get an entity instance from an inthandle.
@@ -75,26 +81,18 @@ def on_automatically_removed(self, index):
def _on_entity_deleted(self, base_entity):
"""OnEntityDeleted listener callback."""
- # Is the entity networkable?
- if not base_entity.is_networked():
-
- # No, so skip it...
- return
-
- # Get the index of the entity...
- index = base_entity.index
-
- # Is the index not in the dictionary?
- if index not in self:
-
- # No need to go further...
+ try:
+ # Get the index of the entity...
+ index = base_entity.index
+ except ValueError:
return
- # Call the deletion callback for the index...
- self.on_automatically_removed(index)
+ if index in self:
+ # Call the deletion callback for the index...
+ self.on_automatically_removed(index)
- # Remove the index from the dictionary...
- super().__delitem__(index)
+ # Remove the index from the dictionary...
+ super().__delitem__(index)
def _unload_instance(self):
"""Unregister our OnEntityDeleted listener."""
diff --git a/addons/source-python/packages/source-python/entities/helpers.py b/addons/source-python/packages/source-python/entities/helpers.py
index ccc9e9b9e..1d67fbf7b 100644
--- a/addons/source-python/packages/source-python/entities/helpers.py
+++ b/addons/source-python/packages/source-python/entities/helpers.py
@@ -2,6 +2,18 @@
"""Provides helper functions to convert from one type to another."""
+# =============================================================================
+# >> IMPORTS
+# =============================================================================
+# Python Imports
+# WeakRef
+from weakref import proxy
+
+# Source.Python Imports
+# Core
+from core.cache import CachedProperty
+
+
# =============================================================================
# >> FORWARD IMPORTS
# =============================================================================
@@ -89,7 +101,7 @@ def __init__(self, wrapped_self, wrapper):
func = wrapped_self.__getattr__(wrapper.__name__)
super().__init__(func._manager, func._type_name, func, func._this)
self.wrapper = wrapper
- self.wrapped_self = wrapped_self
+ self.wrapped_self = proxy(wrapped_self)
def __call__(self, *args, **kwargs):
return super().__call__(
@@ -110,7 +122,7 @@ def skip_hooks(self, *args, **kwargs):
def wrap_entity_mem_func(wrapper):
"""A decorator to wrap an entity memory function."""
- def inner(wrapped_self):
- return EntityMemFuncWrapper(wrapped_self, wrapper)
-
- return property(inner, doc=wrapper.__doc__)
+ return CachedProperty(
+ lambda self: EntityMemFuncWrapper(self, wrapper),
+ doc=wrapper.__doc__
+ )
diff --git a/addons/source-python/packages/source-python/memory/helpers.py b/addons/source-python/packages/source-python/memory/helpers.py
index 4b5514034..4f7ec86c4 100644
--- a/addons/source-python/packages/source-python/memory/helpers.py
+++ b/addons/source-python/packages/source-python/memory/helpers.py
@@ -315,7 +315,10 @@ class MemberFunction(Function):
def __init__(self, manager, return_type, func, this):
"""Initialize the instance."""
- super().__init__(func)
+ self._function = func
+ super().__init__(
+ func.address, func.convention, func.arguments, func.return_type
+ )
# This should always hold a TypeManager instance
self._manager = manager
diff --git a/addons/source-python/packages/source-python/players/_base.py b/addons/source-python/packages/source-python/players/_base.py
old mode 100644
new mode 100755
index 80c918133..3cfcda061
--- a/addons/source-python/packages/source-python/players/_base.py
+++ b/addons/source-python/packages/source-python/players/_base.py
@@ -14,6 +14,8 @@
from bitbuffers import BitBufferWrite
# Core
from core import GAME_NAME
+from core.cache import cached_property
+from core.cache import cached_result
# Engines
from engines.server import server
from engines.server import engine_server
@@ -89,7 +91,6 @@ def __init__(self, index, caching=True):
"""
PlayerMixin.__init__(self, index)
Entity.__init__(self, index)
- object.__setattr__(self, '_playerinfo', None)
@classmethod
def from_userid(cls, userid, caching=None):
@@ -103,10 +104,9 @@ def from_userid(cls, userid, caching=None):
"""
return cls(index_from_userid(userid), caching=caching)
- @property
+ @cached_property
def net_info(self):
"""Return the player's network channel information.
-
:return:
``None`` if no network channel information exists. E. g. if the
player is a bot.
@@ -114,7 +114,7 @@ def net_info(self):
"""
return engine_server.get_player_net_info(self.index)
- @property
+ @cached_property
def raw_steamid(self):
"""Return the player's unformatted SteamID.
@@ -130,18 +130,15 @@ def permissions(self):
"""
return auth_manager.get_player_permissions_from_steamid(self.steamid)
- @property
+ @cached_property
def playerinfo(self):
"""Return player information.
:rtype: PlayerInfo
"""
- if self._playerinfo is None:
- playerinfo = playerinfo_from_index(self.index)
- object.__setattr__(self, '_playerinfo', playerinfo)
- return self._playerinfo
+ return playerinfo_from_index(self.index)
- @property
+ @cached_property
def userid(self):
"""Return the player's userid.
@@ -149,7 +146,7 @@ def userid(self):
"""
return self.playerinfo.userid
- @property
+ @cached_property
def steamid(self):
"""Return the player's SteamID.
@@ -170,7 +167,7 @@ def set_name(self, name):
name = property(get_name, set_name)
- @property
+ @cached_property
def client(self):
"""Return the player's client instance.
@@ -178,7 +175,7 @@ def client(self):
"""
return server.get_client(self.index - 1)
- @property
+ @cached_property
def base_client(self):
"""Return the player's base client instance.
@@ -187,7 +184,7 @@ def base_client(self):
from players import BaseClient
return make_object(BaseClient, get_object_pointer(self.client) - 4)
- @property
+ @cached_property
def uniqueid(self):
"""Return the player's unique ID.
@@ -195,7 +192,7 @@ def uniqueid(self):
"""
return uniqueid_from_playerinfo(self.playerinfo)
- @property
+ @cached_property
def address(self):
"""Return the player's IP address and port.
@@ -221,6 +218,7 @@ def is_fake_client(self):
"""
return self.playerinfo.is_fake_client()
+ @cached_result
def is_hltv(self):
"""Return whether the player is HLTV.
@@ -228,6 +226,7 @@ def is_hltv(self):
"""
return self.playerinfo.is_hltv()
+ @cached_result
def is_bot(self):
"""Return whether the player is a bot.
@@ -262,8 +261,7 @@ def set_team(self, value):
team = property(get_team, set_team)
- @property
- def language(self):
+ def get_language(self):
"""Return the player's language.
If the player is a bot, an empty string will be returned.
@@ -272,6 +270,12 @@ def language(self):
"""
return get_client_language(self.index)
+ # Only cache the language property for games it is already cached
+ if GAME_NAME in ('csgo',):
+ language = cached_property(get_language, doc=get_language.__doc__)
+ else:
+ language = property(get_language, doc=get_language.__doc__)
+
def get_trace_ray(self, mask=ContentMasks.ALL, trace_filter=None):
"""Return the player's current trace data.
diff --git a/addons/source-python/packages/source-python/players/_language/base.py b/addons/source-python/packages/source-python/players/_language/base.py
index 701af488a..a7a726632 100644
--- a/addons/source-python/packages/source-python/players/_language/base.py
+++ b/addons/source-python/packages/source-python/players/_language/base.py
@@ -18,9 +18,8 @@ def get_client_language(index):
:param int index: Index of the client.
"""
- from players.helpers import playerinfo_from_index
- playerinfo = playerinfo_from_index(index)
- if playerinfo.is_fake_client() or 'BOT' in playerinfo.steamid:
+ from players.entity import Player
+ if Player(index).is_bot():
return ''
return engine_server.get_client_convar_value(index, 'cl_language')
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a58d6d205..c6df2fd77 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -153,6 +153,15 @@ Set(SOURCEPYTHON_CORE_MODULE_SOURCES
core/modules/core/core_wrap.cpp
)
+Set(SOURCEPYTHON_CORE_CACHE_MODULE_HEADERS
+ core/modules/core/core_cache.h
+)
+
+Set(SOURCEPYTHON_CORE_CACHE_MODULE_SOURCES
+ core/modules/core/core_cache.cpp
+ core/modules/core/core_cache_wrap.cpp
+)
+
# ------------------------------------------------------------------
# Cvars module.
# ------------------------------------------------------------------
@@ -466,6 +475,9 @@ Set(SOURCEPYTHON_WEAPONS_MODULE_SOURCES
# All module source files
# ------------------------------------------------------------------
Set(SOURCEPYTHON_MODULE_FILES
+ ${SOURCEPYTHON_CORE_CACHE_MODULE_HEADERS}
+ ${SOURCEPYTHON_CORE_CACHE_MODULE_SOURCES}
+
# CFunctionInfo must be exposed at first
${SOURCEPYTHON_MEMORY_MODULE_HEADERS}
${SOURCEPYTHON_MEMORY_MODULE_SOURCES}
diff --git a/src/core/modules/core/core_cache.cpp b/src/core/modules/core/core_cache.cpp
new file mode 100644
index 000000000..dae5ea823
--- /dev/null
+++ b/src/core/modules/core/core_cache.cpp
@@ -0,0 +1,339 @@
+/**
+* =============================================================================
+* Source Python
+* Copyright (C) 2012-2019 Source Python Development Team. All rights reserved.
+* =============================================================================
+*
+* This program is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, version 3.0, as published by the
+* Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+* details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see .
+*
+* As a special exception, the Source Python Team gives you permission
+* to link the code of this program (as well as its derivative works) to
+* "Half-Life 2," the "Source Engine," and any Game MODs that run on software
+* by the Valve Corporation. You must obey the GNU General Public License in
+* all respects for all other code used. Additionally, the Source.Python
+* Development Team grants this exception to all derivative works.
+*/
+
+//-----------------------------------------------------------------------------
+// Includes.
+//-----------------------------------------------------------------------------
+#include "core_cache.h"
+#include "export_main.h"
+
+
+//-----------------------------------------------------------------------------
+// CCachedProperty class.
+//-----------------------------------------------------------------------------
+CCachedProperty::CCachedProperty(
+ object fget=object(), object fset=object(), object fdel=object(), object doc=object(),
+ bool unbound=false, boost::python::tuple args=boost::python::tuple(), dict kwargs=dict())
+{
+ set_getter(fget);
+ set_setter(fset);
+ set_deleter(fdel);
+
+ m_doc = doc;
+ m_bUnbound = unbound;
+
+ m_args = args;
+ m_kwargs = kwargs;
+}
+
+
+object CCachedProperty::_callable_check(object function, const char *szName)
+{
+ if (!function.is_none() && !PyCallable_Check(function.ptr()))
+ BOOST_RAISE_EXCEPTION(
+ PyExc_TypeError,
+ "The given %s function is not callable.", szName
+ );
+
+ return function;
+}
+
+object CCachedProperty::_prepare_value(object value)
+{
+ if (!PyGen_Check(value.ptr()))
+ return value;
+
+ if (getattr(value, "gi_frame").is_none())
+ BOOST_RAISE_EXCEPTION(
+ PyExc_ValueError,
+ "The given generator is exhausted."
+ );
+
+ list values;
+ while (true)
+ {
+ try
+ {
+ values.append(value.attr("__next__")());
+ }
+ catch(...)
+ {
+ if (!PyErr_ExceptionMatches(PyExc_StopIteration))
+ throw_error_already_set();
+
+ PyErr_Clear();
+ break;
+ }
+ }
+
+ return values;
+}
+
+void CCachedProperty::_invalidate_cache(PyObject *pRef)
+{
+ try
+ {
+ m_cache[handle<>(pRef)].del();
+ }
+ catch (...)
+ {
+ if (!PyErr_ExceptionMatches(PyExc_KeyError))
+ throw_error_already_set();
+
+ PyErr_Clear();
+ }
+}
+
+void CCachedProperty::_delete_cache(object instance)
+{
+ try
+ {
+ if (m_bUnbound)
+ m_cache[
+ handle<>(
+ PyWeakref_NewRef(instance.ptr(), NULL)
+ )
+ ].del();
+ else
+ {
+ dict cache = extract(instance.attr("__dict__"));
+ cache[m_name].del();
+ }
+ }
+ catch (...)
+ {
+ if (!PyErr_ExceptionMatches(PyExc_KeyError))
+ throw_error_already_set();
+
+ PyErr_Clear();
+ }
+}
+
+
+object CCachedProperty::get_getter()
+{
+ return m_fget;
+}
+
+object CCachedProperty::set_getter(object fget)
+{
+ m_fget = _callable_check(fget, "getter");
+ return fget;
+}
+
+
+object CCachedProperty::get_setter()
+{
+ return m_fset;
+}
+
+object CCachedProperty::set_setter(object fset)
+{
+ m_fset = _callable_check(fset, "setter");
+ return fset;
+}
+
+
+object CCachedProperty::get_deleter()
+{
+ return m_fdel;
+}
+
+object CCachedProperty::set_deleter(object fdel)
+{
+ m_fdel = _callable_check(fdel, "deleter");
+ return fdel;
+}
+
+
+str CCachedProperty::get_name()
+{
+ return m_name;
+}
+
+object CCachedProperty::get_owner()
+{
+ return m_owner();
+}
+
+
+object CCachedProperty::get_cached_value(object instance)
+{
+ if (!m_name)
+ BOOST_RAISE_EXCEPTION(
+ PyExc_AttributeError,
+ "Unable to retrieve the value of an unbound property."
+ );
+
+ object value;
+
+ if (m_bUnbound)
+ value = m_cache[
+ handle<>(
+ PyWeakref_NewRef(instance.ptr(), NULL)
+ )
+ ];
+ else
+ {
+ dict cache = extract(instance.attr("__dict__"));
+ value = cache[m_name];
+ }
+
+ return value;
+}
+
+void CCachedProperty::set_cached_value(object instance, object value)
+{
+ if (!m_name)
+ BOOST_RAISE_EXCEPTION(
+ PyExc_AttributeError,
+ "Unable to assign the value of an unbound property."
+ );
+
+ if (m_bUnbound)
+ m_cache[handle<>(
+ PyWeakref_NewRef(
+ instance.ptr(),
+ make_function(
+ boost::bind(&CCachedProperty::_invalidate_cache, this, _1),
+ default_call_policies(),
+ boost::mpl::vector2()
+ ).ptr()
+ )
+ )] = _prepare_value(value);
+ else
+ {
+ dict cache = extract(instance.attr("__dict__"));
+ cache[m_name] = _prepare_value(value);
+ }
+}
+
+
+void CCachedProperty::__set_name__(object owner, str name)
+{
+ m_name = name;
+ m_owner = object(handle<>(PyWeakref_NewRef(owner.ptr(), NULL)));
+}
+
+object CCachedProperty::__get__(object self, object instance, object owner=object())
+{
+ if (instance.is_none())
+ return self;
+
+ CCachedProperty &pSelf = extract(self);
+ object value;
+
+ try
+ {
+ value = pSelf.get_cached_value(instance);
+ }
+ catch (...)
+ {
+ if (!PyErr_ExceptionMatches(PyExc_KeyError))
+ throw_error_already_set();
+
+ PyErr_Clear();
+
+ object getter = pSelf.get_getter();
+ if (getter.is_none())
+ BOOST_RAISE_EXCEPTION(
+ PyExc_AttributeError,
+ "Unable to retrieve the value of a property that have no getter function."
+ );
+
+ value = getter(
+ *(make_tuple(handle<>(borrowed(instance.ptr()))) + pSelf.m_args),
+ **pSelf.m_kwargs
+ );
+
+ pSelf.set_cached_value(instance, value);
+ }
+
+ return value;
+}
+
+
+void CCachedProperty::__set__(object instance, object value)
+{
+ if (m_fset.is_none())
+ BOOST_RAISE_EXCEPTION(
+ PyExc_AttributeError,
+ "Unable to assign the value of a property that have no setter function."
+ );
+
+ object result = m_fset(
+ *(make_tuple(handle<>(borrowed(instance.ptr())), value) + m_args),
+ **m_kwargs
+ );
+
+ if (!result.is_none())
+ set_cached_value(instance, result);
+ else
+ _delete_cache(instance);
+}
+
+void CCachedProperty::__delete__(object instance)
+{
+ if (!m_fdel.is_none())
+ m_fdel(
+ *(make_tuple(handle<>(borrowed(instance.ptr()))) + m_args),
+ **m_kwargs
+ );
+
+ _delete_cache(instance);
+}
+
+object CCachedProperty::__call__(object self, object fget)
+{
+ CCachedProperty &pSelf = extract(self);
+ pSelf.set_getter(fget);
+ return self;
+}
+
+object CCachedProperty::__getitem__(str item)
+{
+ return m_kwargs[item];
+}
+
+void CCachedProperty::__setitem__(str item, object value)
+{
+ m_kwargs[item] = value;
+}
+
+
+CCachedProperty *CCachedProperty::wrap_descriptor(
+ object descriptor, object owner, str name,
+ bool unbound, boost::python::tuple args, dict kwargs)
+{
+ CCachedProperty *pProperty = new CCachedProperty(
+ descriptor.attr("__get__"), descriptor.attr("__set__"), descriptor.attr("__delete__"),
+ descriptor.attr("__doc__"), unbound, args, kwargs
+ );
+
+ pProperty->__set_name__(owner, name);
+
+ return pProperty;
+}
diff --git a/src/core/modules/core/core_cache.h b/src/core/modules/core/core_cache.h
new file mode 100644
index 000000000..3ed44bbea
--- /dev/null
+++ b/src/core/modules/core/core_cache.h
@@ -0,0 +1,100 @@
+/**
+* =============================================================================
+* Source Python
+* Copyright (C) 2012-2019 Source Python Development Team. All rights reserved.
+* =============================================================================
+*
+* This program is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, version 3.0, as published by the
+* Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+* details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see .
+*
+* As a special exception, the Source Python Team gives you permission
+* to link the code of this program (as well as its derivative works) to
+* "Half-Life 2," the "Source Engine," and any Game MODs that run on software
+* by the Valve Corporation. You must obey the GNU General Public License in
+* all respects for all other code used. Additionally, the Source.Python
+* Development Team grants this exception to all derivative works.
+*/
+
+#ifndef _CORE_CACHE_H
+#define _CORE_CACHE_H
+
+//-----------------------------------------------------------------------------
+// Includes.
+//-----------------------------------------------------------------------------
+#include "boost/python.hpp"
+using namespace boost::python;
+
+
+//-----------------------------------------------------------------------------
+// CCachedProperty class.
+//-----------------------------------------------------------------------------
+class CCachedProperty
+{
+public:
+ CCachedProperty(
+ object fget, object fset, object fdel, object doc, bool unbound,
+ boost::python::tuple args, dict kwargs
+ );
+
+ static object _callable_check(object function, const char *szName);
+ static object _prepare_value(object value);
+ void _invalidate_cache(PyObject *pRef);
+ void _delete_cache(object instance);
+
+ object get_getter();
+ object set_getter(object fget);
+
+ object get_setter();
+ object set_setter(object fget);
+
+ object get_deleter();
+ object set_deleter(object fget);
+
+ str get_name();
+ object get_owner();
+
+ object get_cached_value(object instance);
+ void set_cached_value(object instance, object value);
+
+ void __set_name__(object owner, str name);
+ static object __get__(object self, object instance, object owner);
+ void __set__(object instance, object value);
+ void __delete__(object instance);
+ static object __call__(object self, object fget);
+ object __getitem__(str item);
+ void __setitem__(str item, object value);
+
+ static CCachedProperty *wrap_descriptor(
+ object descriptor, object owner=object(), str name=str(),
+ bool unbound=false, boost::python::tuple args=boost::python::tuple(), dict kwargs=dict()
+ );
+
+private:
+ object m_fget;
+ object m_fset;
+ object m_fdel;
+
+ str m_name;
+ object m_owner;
+
+ bool m_bUnbound;
+ dict m_cache;
+
+public:
+ object m_doc;
+
+ boost::python::tuple m_args;
+ dict m_kwargs;
+};
+
+
+#endif // _CORE_CACHE_H
diff --git a/src/core/modules/core/core_cache_wrap.cpp b/src/core/modules/core/core_cache_wrap.cpp
new file mode 100644
index 000000000..4fd3f5bb8
--- /dev/null
+++ b/src/core/modules/core/core_cache_wrap.cpp
@@ -0,0 +1,412 @@
+/**
+* =============================================================================
+* Source Python
+* Copyright (C) 2012-2019 Source Python Development Team. All rights reserved.
+* =============================================================================
+*
+* This program is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, version 3.0, as published by the
+* Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+* details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see .
+*
+* As a special exception, the Source Python Team gives you permission
+* to link the code of this program (as well as its derivative works) to
+* "Half-Life 2," the "Source Engine," and any Game MODs that run on software
+* by the Valve Corporation. You must obey the GNU General Public License in
+* all respects for all other code used. Additionally, the Source.Python
+* Development Team grants this exception to all derivative works.
+*/
+
+//-----------------------------------------------------------------------------
+// Includes.
+//-----------------------------------------------------------------------------
+#include "export_main.h"
+#include "sp_main.h"
+#include "core_cache.h"
+
+
+//-----------------------------------------------------------------------------
+// Forward declarations.
+//-----------------------------------------------------------------------------
+static void export_cached_property(scope);
+
+
+//-----------------------------------------------------------------------------
+// Declare the _core._cache module.
+//-----------------------------------------------------------------------------
+DECLARE_SP_SUBMODULE(_core, _cache)
+{
+ export_cached_property(_cache);
+}
+
+
+//-----------------------------------------------------------------------------
+// Exports CachedProperty.
+//-----------------------------------------------------------------------------
+void export_cached_property(scope _cache)
+{
+ class_ CachedProperty(
+ "CachedProperty",
+ init