From 01c11bbbb585beaebb0391e44e181439aa73ce4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Fri, 31 May 2019 18:10:43 -0400 Subject: [PATCH 01/20] Added engine_import implementation. --- .../modules/entities.engines.csgo.csgo.rst | 7 - .../modules/entities.engines.csgo.rst | 10 - .../developing/modules/entities.engines.rst | 18 - .../developing/modules/players._base.rst | 12 - .../modules/players.engines.bms.rst | 10 - .../modules/players.engines.csgo.rst | 10 - .../modules/players.engines.l4d2.rst | 10 - .../players.engines.orangebox.cstrike.rst | 7 - .../modules/players.engines.orangebox.rst | 18 - .../developing/modules/players.engines.rst | 19 - .../modules/weapons.engines.csgo.csgo.rst | 7 - .../modules/weapons.engines.csgo.rst | 10 - .../developing/modules/weapons.engines.rst | 18 - .../packages/source-python/core/__init__.py | 70 +- .../packages/source-python/entities/_base.py | 1049 ---------------- .../{engines/csgo/csgo.py => csgo/entity.py} | 7 +- .../entities/engines/__init__.py | 3 - .../entities/engines/csgo/__init__.py | 16 - .../packages/source-python/entities/entity.py | 1075 ++++++++++++++++- .../packages/source-python/players/_base.py | 1013 ---------------- .../bms/__init__.py => bms/entity.py} | 14 +- .../csgo/__init__.py => csgo/entity.py} | 15 +- .../source-python/players/engines/__init__.py | 3 - .../packages/source-python/players/entity.py | 1021 +++++++++++++++- .../l4d2/__init__.py => l4d2/entity.py} | 14 +- .../cstrike/entity.py} | 9 +- .../__init__.py => orangebox/entity.py} | 14 +- .../packages/source-python/weapons/_base.py | 171 --- .../{engines/csgo/csgo.py => csgo/entity.py} | 12 +- .../source-python/weapons/engines/__init__.py | 3 - .../weapons/engines/csgo/__init__.py | 16 - .../packages/source-python/weapons/entity.py | 178 ++- 32 files changed, 2272 insertions(+), 2587 deletions(-) delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/players._base.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.csgo.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.cstrike.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/weapons.engines.rst delete mode 100644 addons/source-python/packages/source-python/entities/_base.py rename addons/source-python/packages/source-python/entities/{engines/csgo/csgo.py => csgo/entity.py} (95%) delete mode 100644 addons/source-python/packages/source-python/entities/engines/__init__.py delete mode 100644 addons/source-python/packages/source-python/entities/engines/csgo/__init__.py delete mode 100644 addons/source-python/packages/source-python/players/_base.py rename addons/source-python/packages/source-python/players/{engines/bms/__init__.py => bms/entity.py} (57%) rename addons/source-python/packages/source-python/players/{engines/csgo/__init__.py => csgo/entity.py} (93%) delete mode 100644 addons/source-python/packages/source-python/players/engines/__init__.py rename addons/source-python/packages/source-python/players/{engines/l4d2/__init__.py => l4d2/entity.py} (58%) rename addons/source-python/packages/source-python/players/{engines/orangebox/cstrike.py => orangebox/cstrike/entity.py} (90%) rename addons/source-python/packages/source-python/players/{engines/orangebox/__init__.py => orangebox/entity.py} (53%) delete mode 100644 addons/source-python/packages/source-python/weapons/_base.py rename addons/source-python/packages/source-python/weapons/{engines/csgo/csgo.py => csgo/entity.py} (86%) delete mode 100644 addons/source-python/packages/source-python/weapons/engines/__init__.py delete mode 100644 addons/source-python/packages/source-python/weapons/engines/csgo/__init__.py diff --git a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst deleted file mode 100644 index 815567661..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst +++ /dev/null @@ -1,7 +0,0 @@ -entities.engines.csgo.csgo module -================================== - -.. automodule:: entities.engines.csgo.csgo - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst deleted file mode 100644 index be709464e..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst +++ /dev/null @@ -1,10 +0,0 @@ -entities.engines.csgo package -============================== - -Module contents ---------------- - -.. automodule:: entities.engines.csgo - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst deleted file mode 100644 index be5f64c36..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst +++ /dev/null @@ -1,18 +0,0 @@ -entities.engines package -========================= - -Subpackages ------------ - -.. toctree:: - :titlesonly: - - entities.engines.csgo - -Module contents ---------------- - -.. automodule:: entities.engines - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players._base.rst b/addons/source-python/docs/source-python/source/developing/modules/players._base.rst deleted file mode 100644 index 3bc89ece6..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/players._base.rst +++ /dev/null @@ -1,12 +0,0 @@ -players._base module -==================== - -.. automodule:: players._base - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: _players.PlayerMixin - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst deleted file mode 100644 index f4c7dced5..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst +++ /dev/null @@ -1,10 +0,0 @@ -players.engines.bms package -============================ - -Module contents ---------------- - -.. automodule:: players.engines.bms - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.csgo.rst deleted file mode 100644 index 1488f1652..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/players.engines.csgo.rst +++ /dev/null @@ -1,10 +0,0 @@ -players.engines.csgo package -============================= - -Module contents ---------------- - -.. automodule:: players.engines.csgo - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst deleted file mode 100644 index 8be8d9e41..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst +++ /dev/null @@ -1,10 +0,0 @@ -players.engines.l4d2 package -============================= - -Module contents ---------------- - -.. automodule:: players.engines.l4d2 - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.cstrike.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.cstrike.rst deleted file mode 100644 index 21c8e2f0e..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.cstrike.rst +++ /dev/null @@ -1,7 +0,0 @@ -players.engines.orangebox.cstrike module -========================================= - -.. automodule:: players.engines.orangebox.cstrike - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.rst deleted file mode 100644 index 4ce92e8ba..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/players.engines.orangebox.rst +++ /dev/null @@ -1,18 +0,0 @@ -players.engines.orangebox package -================================== - -Submodules ----------- - -.. toctree:: - :titlesonly: - - players.engines.orangebox.cstrike - -Module contents ---------------- - -.. automodule:: players.engines.orangebox - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.rst deleted file mode 100644 index 421add012..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/players.engines.rst +++ /dev/null @@ -1,19 +0,0 @@ -players.engines package -======================== - -Subpackages ------------ - -.. toctree:: - :titlesonly: - - players.engines.csgo - players.engines.orangebox - -Module contents ---------------- - -.. automodule:: players.engines - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst deleted file mode 100644 index 24483b9c5..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst +++ /dev/null @@ -1,7 +0,0 @@ -weapons.engines.csgo.csgo module -================================= - -.. automodule:: weapons.engines.csgo.csgo - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.rst deleted file mode 100644 index 835494fee..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.rst +++ /dev/null @@ -1,10 +0,0 @@ -weapons.engines.csgo package -============================= - -Module contents ---------------- - -.. automodule:: weapons.engines.csgo - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.rst b/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.rst deleted file mode 100644 index 2b05ebc31..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.rst +++ /dev/null @@ -1,18 +0,0 @@ -weapons.engines package -======================== - -Subpackages ------------ - -.. toctree:: - :titlesonly: - - weapons.engines.csgo - -Module contents ---------------- - -.. automodule:: weapons.engines - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 0dd8baf8f..6a7d9897e 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -12,17 +12,23 @@ from collections import defaultdict # Contextlib from contextlib import contextmanager +# Functools +from functools import update_wrapper # Hashlib import hashlib # Inspect -from inspect import getmodule from inspect import currentframe +from inspect import getmodule +from inspect import isclass +from inspect import isfunction # OS from os import sep # Path from path import Path # Platform from platform import system +# Runpy +from runpy import run_path # Sys import sys # Urllib @@ -70,9 +76,11 @@ 'console_message', 'create_checksum', 'echo_console', + 'engine_import', 'get_core_modules', 'get_interface', 'get_public_ip', + 'get_wrapped', 'ignore_unicode_errors', 'server_output', ) @@ -330,4 +338,62 @@ def check_info_output(output): if lines[-1].startswith('-'): lines.pop() - return create_checksum(''.join(lines)) != checksum \ No newline at end of file + return create_checksum(''.join(lines)) != checksum + + +def engine_import(): + """Import engine/game specific objects. + + :raise ImportError: + If it was not called from global scope. + """ + f = currentframe().f_back + if f.f_locals is not f.f_globals: + raise ImportError( + '"engine_import" must only be called from global scopes.') + caller = getmodule(f) + directory, name = Path(caller.__file__).splitpath() + for subfolder in (SOURCE_ENGINE, GAME_NAME): + directory /= subfolder + if not directory.isdir(): + break + path = directory / name + if not path.isfile(): + continue + for attr, obj in run_path(path, f.f_globals, caller.__name__).items(): + if isclass(obj): + base = obj.__base__ + if (obj.__name__ == base.__name__ and + base.__module__ == caller.__name__): + for k, v in obj.__dict__.items(): + if (k == '__doc__' and + getattr(base, '__doc__', None) is not None): + continue + if isfunction(v) and hasattr(base, k): + func = getattr(base, k) + if isfunction(func): + update_wrapper(v, func) + setattr(base, k, v) + continue + if hasattr(caller, attr): + o = getattr(caller, attr) + if o is obj: + continue + elif isfunction(o): + update_wrapper(obj, o) + setattr(caller, attr, obj) + +def get_wrapped(func): + """Returns the wrapped function of a wrapper function. + + :param function func: + The wrapper function to get the wrapped function from. + :raise TypeError: + If the given wrapper is not a function. + :return: + The wrapped function or ``None`` if the given wrapper is not wrapping + any function. + """ + if not isfunction(func): + raise TypeError(f'"{func}" is not a function.') + return getattr(func, '__wrapped__', None) diff --git a/addons/source-python/packages/source-python/entities/_base.py b/addons/source-python/packages/source-python/entities/_base.py deleted file mode 100644 index 1b65d91b9..000000000 --- a/addons/source-python/packages/source-python/entities/_base.py +++ /dev/null @@ -1,1049 +0,0 @@ -# ../entities/_base.py - -"""Provides a base class to interact with a specific entity.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Python Imports -# Collections -from collections import defaultdict -# Contextlib -from contextlib import suppress - -# Source.Python Imports -# Core -from core import GAME_NAME -# 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 -from engines.sound import SoundFlags -from engines.sound import StreamSound -from engines.sound import VOL_NORM -from engines.trace import engine_trace -from engines.trace import ContentMasks -from engines.trace import GameTrace -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 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.tick import Delay -# Mathlib -from mathlib import NULL_VECTOR -# Memory -from memory import make_object -# Players -from players.constants import HitGroup -# Studio -from studio.cache import model_cache -from studio.constants import INVALID_ATTACHMENT_INDEX - - -# ============================================================================= -# >> GLOBAL VARIABLES -# ============================================================================= -# Get a list of projectiles for the game -_projectile_weapons = [weapon.name for weapon in WeaponClassIter('grenade')] - -# Get a dictionary to store the delays -_entity_delays = defaultdict(set) - - -# ============================================================================= -# >> CLASSES -# ============================================================================= -class Entity(BaseEntity): - """Class used to interact directly with entities. - - Beside the standard way of doing stuff via methods and properties this - class also provides dynamic attributes that depend on the entity that is - being accessed with this class. You can print all dynamic properties by - iterating over the following generators: - - 1. :attr:`properties` - 2. :attr:`inputs` - 3. :attr:`outputs` - 4. :attr:`keyvalues` - """ - - def __init__(self, index): - """Initialize the Entity instance. - - :param int index: - The entity index to wrap. - """ - # Initialize the object - super().__init__(index) - - # Set the entity's base attributes - object.__setattr__(self, '_index', index) - - def __hash__(self): - """Return a hash value based on the entity index.""" - # Required for sets, because we have implemented __eq__ - return hash(self.index) - - 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: - - # Does the current server class contain the given attribute? - if hasattr(server_class, attr): - - # Return the attribute's value - return getattr(make_object(server_class, self.pointer), attr) - - # 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.""" - # Is the given attribute a property? - if (attr in super().__dir__() and isinstance( - getattr(self.__class__, attr, None), property)): - - # Set the property's value - object.__setattr__(self, attr, value) - - # No need to go further - return - - # Loop through all of the entity's server classes - for server_class in self.server_classes: - - # 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) - - # No need to go further - return - - # If the attribute is not found, just set the attribute - super().__setattr__(attr, value) - - def __dir__(self): - """Return an alphabetized list of attributes for the instance.""" - # Get the base attributes - attributes = set(super().__dir__()) - - # Loop through all server classes for the entity - for server_class in self.server_classes: - - # Loop through all of the server class' attributes - for attr in dir(server_class): - - # Add the attribute if it is not private - if not attr.startswith('_'): - attributes.add(attr) - - # 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): - """Create an entity instance from an inthandle. - - :param int inthandle: - The inthandle. - :rtype: Entity - """ - return cls(index_from_inthandle(inthandle)) - - @classmethod - def _obj(cls, ptr): - """Return an entity instance of the given pointer.""" - return cls(index_from_pointer(ptr)) - - @property - def index(self): - """Return the entity's index. - - :rtype: int - """ - return self._index - - @property - def owner(self): - """Return the entity's owner. - - :return: - None if the entity has no owner. - :rtype: Entity - """ - try: - return Entity(index_from_inthandle(self.owner_handle)) - except (ValueError, OverflowError): - return None - - @property - def server_classes(self): - """Yield all server classes for the entity.""" - yield from server_classes.get_entity_server_classes(self) - - @property - def properties(self): - """Iterate over all descriptors available for the entity.""" - for server_class in self.server_classes: - yield from server_class.properties - - @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 - def outputs(self): - """Iterate over all outputs available for the entity.""" - for server_class in self.server_classes: - yield from server_class.outputs - - @property - def keyvalues(self): - """Iterate over all entity keyvalues available for the entity. - - .. note:: - - An entity might also have hardcoded keyvalues that can't be listed - with this property. - """ - for server_class in self.server_classes: - yield from server_class.keyvalues - - def get_model(self): - """Return the entity's model. - - :return: - ``None`` if the entity has no model. - :rtype: Model - """ - if not self.model_name: - return None - - return Model(self.model_name) - - def set_model(self, model): - """Set the entity's model to the given model. - - :param Model model: - The model to set. - """ - self.model_index = model.index - self.model_name = model.path - - model = property( - get_model, set_model, - doc="""Property to get/set the entity's model. - - .. seealso:: :meth:`get_model` and :meth:`set_model`""") - - def get_parent(self): - """Return the entity's parent. - - :rtype: Entity - """ - # Get the parent handle... - parent_inthandle = self.parent_inthandle - - # Does the entity have no parent? - if parent_inthandle == -1: - return None - - # Return the parent of the entity... - return Entity(index_from_inthandle(parent_inthandle)) - - def _set_parent(self, parent): - """Set the parent of the entity. - - :param Entity parent: - The parent of the entity. If None, actual parent will be cleared. - """ - # Clear the actual parent if None was given... - if parent is None: - self.clear_parent() - - # Set the parent of the entity... - self.set_parent(parent) - - parent = property( - get_parent, _set_parent, - doc="""Property to get/set the parent of the entity. - - .. 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): - """Execute a callback after the given delay. - - :param float delay: - The delay in seconds. - :param callback: - A callable object that should be called after the delay expired. - :param tuple args: - Arguments that should be passed to the callback. - :param dict kwargs: - Keyword arguments that should be passed to the callback. - :param bool cancel_on_level_end: - Whether or not to cancel the delay at the end of the map. - :raise ValueError: - If the given callback is not callable. - :return: - The delay instance. - :rtype: Delay - """ - # 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) - - # Was this the last pending delay for the entity? - if not _entity_delays[self.index]: - - # Remove the entity from the dictionary... - del _entity_delays[self.index] - - # Call the callback... - callback(*args, **kwargs) - - # Get the delay instance... - delay = Delay(delay, _callback, args, kwargs, cancel_on_level_end) - - # Add the delay to the dictionary... - _entity_delays[self.index].add(delay) - - # Return the delay instance... - return delay - - 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: - 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)) - - def call_input(self, name, *args, **kwargs): - """Call the input function matching the given name. - - :param str name: - Name of the input function. - :param args: - Optional arguments that should be passed to the input function. - :param kwargs: - Optional keyword arguments that should be passed to the input - function. - :raise ValueError: - Raised if the input function wasn't found. - """ - self.get_input(name)(*args, **kwargs) - - def emit_sound( - self, sample, recipients=(), volume=VOL_NORM, - attenuation=Attenuation.NONE, channel=Channel.AUTO, - flags=SoundFlags.NO_FLAGS, pitch=Pitch.NORMAL, origin=NULL_VECTOR, - direction=NULL_VECTOR, origins=(), update_positions=True, - sound_time=0.0, speaker_entity=INVALID_ENTITY_INDEX, - download=False, stream=False): - """Emit a sound from this entity. - - :param str sample: - Sound file relative to the ``sounds`` directory. - :param RecipientFilter recipients: - Recipients to emit the sound to. - :param int index: - Index of the entity to emit the sound from. - :param float volume: - Volume of the sound. - :param Attenuation attenuation: - How far the sound should reaches. - :param int channel: - Channel to emit the sound with. - :param SoundFlags flags: - Flags of the sound. - :param Pitch pitch: - Pitch of the sound. - :param Vector origin: - Origin of the sound. - :param Vector direction: - Direction of the sound. - :param tuple origins: - Origins of the sound. - :param bool update_positions: - Whether or not the positions should be updated. - :param float sound_time: - Time to play the sound for. - :param int speaker_entity: - Index of the speaker entity. - :param bool download: - Whether or not the sample should be added to the downloadables. - :param bool stream: - Whether or not the sound should be streamed. - """ - # Get the correct Sound class... - if not stream: - sound_class = Sound - else: - sound_class = StreamSound - - # Get the sound... - sound = sound_class(sample, self.index, volume, attenuation, channel, - flags, pitch, origin, direction, origins, update_positions, - sound_time, speaker_entity, download) - - # Make sure we have a tuple as recipients... - if not isinstance(recipients, tuple): - recipients = (recipients,) - - # Emit the sound to the given recipients... - sound.play(*recipients) - - def is_in_solid( - self, mask=ContentMasks.ALL, generator=BaseEntityGenerator): - """Return whether or not the entity is in solid. - - :param ContentMasks mask: - Contents the ray can possibly collide with. - :param generator: - A callable that returns an iterable which contains - :class:`BaseEntity` instances that are ignored by the ray. - :rtype: bool - """ - # Get a Ray object of the entity physic box - ray = Ray(self.origin, self.origin, self.mins, self.maxs) - - # Get a new GameTrace instance - trace = GameTrace() - - # Do the trace - engine_trace.trace_ray(ray, mask, TraceFilterSimple( - generator()), trace) - - # Return whether or not the trace did hit - return trace.did_hit() - - def take_damage( - self, damage, damage_type=DamageTypes.GENERIC, attacker_index=None, - weapon_index=None, hitgroup=HitGroup.GENERIC, skip_hooks=False, - **kwargs): - """Deal damage to the entity. - - :param int damage: - Amount of damage to deal. - :param DamageTypes damage_type: - Type of the dealed damage. - :param int attacker_index: - If not None, the index will be used as the attacker. - :param int weapon_index: - If not None, the index will be used as the weapon. This method - also tries to retrieve the attacker from the weapon, if - ``attacker_index`` wasn't set. - :param HitGroup hitgroup: - The hitgroup where the damage should be applied. - :param bool skip_hooks: - If True, the damage will be dealed directly by skipping any - registered hooks. - """ - # Import Entity classes - # Doing this in the global scope causes cross import errors - from weapons.entity import Weapon - - # Is the game supported? - if not hasattr(self, 'on_take_damage'): - - # Raise an error if not supported - raise NotImplementedError( - '"take_damage" is not implemented for {0}'.format(GAME_NAME)) - - # Store values for later use - attacker = None - weapon = None - - # Was an attacker given? - if attacker_index is not None: - - # Try to get the Entity instance of the attacker - with suppress(ValueError): - attacker = Entity(attacker_index) - - # Was a weapon given? - if weapon_index is not None: - - # Try to get the Weapon instance of the weapon - with suppress(ValueError): - weapon = Weapon(weapon_index) - - # Is there a weapon but no attacker? - if attacker is None and weapon is not None: - - # Try to get the attacker based off of the weapon's owner - with suppress(ValueError, OverflowError): - attacker_index = index_from_inthandle(weapon.owner_handle) - attacker = Entity(attacker_index) - - # Is there an attacker but no weapon? - if attacker is not None and weapon is None: - - # Try to use the attacker's active weapon - with suppress(AttributeError): - weapon = attacker.active_weapon - - # Try to set the hitgroup - with suppress(AttributeError): - self.hitgroup = hitgroup - - # Get a TakeDamageInfo instance - take_damage_info = TakeDamageInfo() - - # Is there a valid weapon? - if weapon is not None: - - # Is the weapon a projectile? - if weapon.classname in _projectile_weapons: - - # Set the inflictor to the weapon's index - take_damage_info.inflictor = weapon.index - - # Is the weapon not a projectile and the attacker is valid? - elif attacker_index is not None: - - # Set the inflictor to the attacker's index - take_damage_info.inflictor = attacker_index - - # Set the weapon to the weapon's index - take_damage_info.weapon = weapon.index - - # Is the attacker valid? - if attacker_index is not None: - - # Set the attacker to the attacker's index - take_damage_info.attacker = attacker_index - - # Set the damage amount - take_damage_info.damage = damage - - # Set the damage type value - take_damage_info.type = damage_type - - # Loop through the given keywords - for item in kwargs: - - # Set the offset's value - setattr(take_damage_info, item, kwargs[item]) - - if skip_hooks: - self.on_take_damage.skip_hooks(take_damage_info) - else: - self.on_take_damage(take_damage_info) - - @wrap_entity_mem_func - def teleport(self, origin=None, angle=None, velocity=None): - """Change the origin, angle and/or velocity of the entity. - - :param Vector origin: - New location of the entity. - :param QAngle angle: - New angle of the entity. - :param Vector velocity: - New velocity of the entity. - """ - return [origin, angle, velocity] - - @wrap_entity_mem_func - def set_parent(self, parent, attachment=INVALID_ATTACHMENT_INDEX): - """Set the parent of the entity. - - :param Pointer parent: - The parent. - :param str attachment: - The attachment name/index. - """ - if not isinstance(attachment, int): - attachment = self.lookup_attachment(attachment) - - return [parent, attachment] - - -# ============================================================================= -# >> LISTENERS -# ============================================================================= -@OnEntityDeleted -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(): - return - - # Get the index of the entity... - index = base_entity.index - - # Was no delay registered for this entity? - if index not in _entity_delays: - return - - # 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... - delay.cancel() - - # Remove the entity from the dictionary... - del _entity_delays[index] diff --git a/addons/source-python/packages/source-python/entities/engines/csgo/csgo.py b/addons/source-python/packages/source-python/entities/csgo/entity.py similarity index 95% rename from addons/source-python/packages/source-python/entities/engines/csgo/csgo.py rename to addons/source-python/packages/source-python/entities/csgo/entity.py index a980da921..ec54caefd 100644 --- a/addons/source-python/packages/source-python/entities/engines/csgo/csgo.py +++ b/addons/source-python/packages/source-python/entities/csgo/entity.py @@ -1,4 +1,4 @@ -# ../entities/engines/csgo/csgo.py +# ../entities/csgo/entity.py """Provides CS:GO game specific Entity based functionality.""" @@ -6,8 +6,7 @@ # >> IMPORTS # ============================================================================= # Source.Python -from entities import BaseEntityGenerator -from . import Entity as _Entity +# Weapons from weapons.manager import weapon_manager @@ -30,7 +29,7 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class Entity(_Entity): +class Entity(Entity): """Class used to interact directly with entities.""" @classmethod diff --git a/addons/source-python/packages/source-python/entities/engines/__init__.py b/addons/source-python/packages/source-python/entities/engines/__init__.py deleted file mode 100644 index 51c945f50..000000000 --- a/addons/source-python/packages/source-python/entities/engines/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# ../entities/engines/__init__.py - -"""Provides engine specific Entity based functionality.""" diff --git a/addons/source-python/packages/source-python/entities/engines/csgo/__init__.py b/addons/source-python/packages/source-python/entities/engines/csgo/__init__.py deleted file mode 100644 index 44d71e612..000000000 --- a/addons/source-python/packages/source-python/entities/engines/csgo/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# ../entities/engines/csgo/__init__.py - -"""Provides CS:GO engine specific Entity based functionality.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python -from entities._base import Entity as _Entity - - -# ============================================================================= -# >> CLASSES -# ============================================================================= -class Entity(_Entity): - """Class used to interact directly with entities.""" diff --git a/addons/source-python/packages/source-python/entities/entity.py b/addons/source-python/packages/source-python/entities/entity.py index 6021e65f9..56bb25e97 100644 --- a/addons/source-python/packages/source-python/entities/entity.py +++ b/addons/source-python/packages/source-python/entities/entity.py @@ -5,18 +5,57 @@ # ============================================================================= # >> IMPORTS # ============================================================================= -from importlib import import_module -from core import GAME_NAME -from core import SOURCE_ENGINE -from paths import SP_PACKAGES_PATH - +# Python Imports +# Collections +from collections import defaultdict +# Contextlib +from contextlib import suppress -# ============================================================================= -# >> FORWARD IMPORTS -# ============================================================================= # Source.Python Imports +# Core +from core import GAME_NAME +from core import engine_import +# 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 +from engines.sound import SoundFlags +from engines.sound import StreamSound +from engines.sound import VOL_NORM +from engines.trace import engine_trace +from engines.trace import ContentMasks +from engines.trace import GameTrace +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 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.tick import Delay +# Mathlib +from mathlib import NULL_VECTOR +# Memory +from memory import make_object +# Players +from players.constants import HitGroup +# Studio +from studio.cache import model_cache +from studio.constants import INVALID_ATTACHMENT_INDEX # ============================================================================= @@ -30,30 +69,996 @@ # ============================================================================= # >> GLOBAL VARIABLES # ============================================================================= -if SP_PACKAGES_PATH.joinpath( - 'entities', 'engines', SOURCE_ENGINE, GAME_NAME + '.py' -).isfile(): - - # Import the game-specific 'Entity' class - Entity = import_module( - 'entities.engines.{engine}.{game}'.format( - engine=SOURCE_ENGINE, - game=GAME_NAME, - ) - ).Entity - -elif SP_PACKAGES_PATH.joinpath( - 'entities', 'engines', SOURCE_ENGINE, '__init__.py' -).isfile(): - - # Import the engine-specific 'Entity' class - Entity = import_module( - 'entities.engines.{engine}'.format( - engine=SOURCE_ENGINE, - ) - ).Entity - -else: - - # Import the base 'Entity' class - from entities._base import Entity +# Get a list of projectiles for the game +_projectile_weapons = [weapon.name for weapon in WeaponClassIter('grenade')] + +# Get a dictionary to store the delays +_entity_delays = defaultdict(set) + + +# ============================================================================= +# >> CLASSES +# ============================================================================= +class Entity(BaseEntity): + """Class used to interact directly with entities. + + Beside the standard way of doing stuff via methods and properties this + class also provides dynamic attributes that depend on the entity that is + being accessed with this class. You can print all dynamic properties by + iterating over the following generators: + + 1. :attr:`properties` + 2. :attr:`inputs` + 3. :attr:`outputs` + 4. :attr:`keyvalues` + """ + + def __init__(self, index): + """Initialize the Entity instance. + + :param int index: + The entity index to wrap. + """ + # Initialize the object + super().__init__(index) + + # Set the entity's base attributes + object.__setattr__(self, '_index', index) + + def __hash__(self): + """Return a hash value based on the entity index.""" + # Required for sets, because we have implemented __eq__ + return hash(self.index) + + 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: + + # Does the current server class contain the given attribute? + if hasattr(server_class, attr): + + # Return the attribute's value + return getattr(make_object(server_class, self.pointer), attr) + + # 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.""" + # Is the given attribute a property? + if (attr in super().__dir__() and isinstance( + getattr(self.__class__, attr, None), property)): + + # Set the property's value + object.__setattr__(self, attr, value) + + # No need to go further + return + + # Loop through all of the entity's server classes + for server_class in self.server_classes: + + # 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) + + # No need to go further + return + + # If the attribute is not found, just set the attribute + super().__setattr__(attr, value) + + def __dir__(self): + """Return an alphabetized list of attributes for the instance.""" + # Get the base attributes + attributes = set(super().__dir__()) + + # Loop through all server classes for the entity + for server_class in self.server_classes: + + # Loop through all of the server class' attributes + for attr in dir(server_class): + + # Add the attribute if it is not private + if not attr.startswith('_'): + attributes.add(attr) + + # 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): + """Create an entity instance from an inthandle. + + :param int inthandle: + The inthandle. + :rtype: Entity + """ + return cls(index_from_inthandle(inthandle)) + + @classmethod + def _obj(cls, ptr): + """Return an entity instance of the given pointer.""" + return cls(index_from_pointer(ptr)) + + @property + def index(self): + """Return the entity's index. + + :rtype: int + """ + return self._index + + @property + def owner(self): + """Return the entity's owner. + + :return: + None if the entity has no owner. + :rtype: Entity + """ + try: + return Entity(index_from_inthandle(self.owner_handle)) + except (ValueError, OverflowError): + return None + + @property + def server_classes(self): + """Yield all server classes for the entity.""" + yield from server_classes.get_entity_server_classes(self) + + @property + def properties(self): + """Iterate over all descriptors available for the entity.""" + for server_class in self.server_classes: + yield from server_class.properties + + @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 + def outputs(self): + """Iterate over all outputs available for the entity.""" + for server_class in self.server_classes: + yield from server_class.outputs + + @property + def keyvalues(self): + """Iterate over all entity keyvalues available for the entity. + + .. note:: + + An entity might also have hardcoded keyvalues that can't be listed + with this property. + """ + for server_class in self.server_classes: + yield from server_class.keyvalues + + def get_model(self): + """Return the entity's model. + + :return: + ``None`` if the entity has no model. + :rtype: Model + """ + if not self.model_name: + return None + + return Model(self.model_name) + + def set_model(self, model): + """Set the entity's model to the given model. + + :param Model model: + The model to set. + """ + self.model_index = model.index + self.model_name = model.path + + model = property( + get_model, set_model, + doc="""Property to get/set the entity's model. + + .. seealso:: :meth:`get_model` and :meth:`set_model`""") + + def get_parent(self): + """Return the entity's parent. + + :rtype: Entity + """ + # Get the parent handle... + parent_inthandle = self.parent_inthandle + + # Does the entity have no parent? + if parent_inthandle == -1: + return None + + # Return the parent of the entity... + return Entity(index_from_inthandle(parent_inthandle)) + + def _set_parent(self, parent): + """Set the parent of the entity. + + :param Entity parent: + The parent of the entity. If None, actual parent will be cleared. + """ + # Clear the actual parent if None was given... + if parent is None: + self.clear_parent() + + # Set the parent of the entity... + self.set_parent(parent) + + parent = property( + get_parent, _set_parent, + doc="""Property to get/set the parent of the entity. + + .. 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): + """Execute a callback after the given delay. + + :param float delay: + The delay in seconds. + :param callback: + A callable object that should be called after the delay expired. + :param tuple args: + Arguments that should be passed to the callback. + :param dict kwargs: + Keyword arguments that should be passed to the callback. + :param bool cancel_on_level_end: + Whether or not to cancel the delay at the end of the map. + :raise ValueError: + If the given callback is not callable. + :return: + The delay instance. + :rtype: Delay + """ + # 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) + + # Was this the last pending delay for the entity? + if not _entity_delays[self.index]: + + # Remove the entity from the dictionary... + del _entity_delays[self.index] + + # Call the callback... + callback(*args, **kwargs) + + # Get the delay instance... + delay = Delay(delay, _callback, args, kwargs, cancel_on_level_end) + + # Add the delay to the dictionary... + _entity_delays[self.index].add(delay) + + # Return the delay instance... + return delay + + 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: + 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)) + + def call_input(self, name, *args, **kwargs): + """Call the input function matching the given name. + + :param str name: + Name of the input function. + :param args: + Optional arguments that should be passed to the input function. + :param kwargs: + Optional keyword arguments that should be passed to the input + function. + :raise ValueError: + Raised if the input function wasn't found. + """ + self.get_input(name)(*args, **kwargs) + + def emit_sound( + self, sample, recipients=(), volume=VOL_NORM, + attenuation=Attenuation.NONE, channel=Channel.AUTO, + flags=SoundFlags.NO_FLAGS, pitch=Pitch.NORMAL, origin=NULL_VECTOR, + direction=NULL_VECTOR, origins=(), update_positions=True, + sound_time=0.0, speaker_entity=INVALID_ENTITY_INDEX, + download=False, stream=False): + """Emit a sound from this entity. + + :param str sample: + Sound file relative to the ``sounds`` directory. + :param RecipientFilter recipients: + Recipients to emit the sound to. + :param int index: + Index of the entity to emit the sound from. + :param float volume: + Volume of the sound. + :param Attenuation attenuation: + How far the sound should reaches. + :param int channel: + Channel to emit the sound with. + :param SoundFlags flags: + Flags of the sound. + :param Pitch pitch: + Pitch of the sound. + :param Vector origin: + Origin of the sound. + :param Vector direction: + Direction of the sound. + :param tuple origins: + Origins of the sound. + :param bool update_positions: + Whether or not the positions should be updated. + :param float sound_time: + Time to play the sound for. + :param int speaker_entity: + Index of the speaker entity. + :param bool download: + Whether or not the sample should be added to the downloadables. + :param bool stream: + Whether or not the sound should be streamed. + """ + # Get the correct Sound class... + if not stream: + sound_class = Sound + else: + sound_class = StreamSound + + # Get the sound... + sound = sound_class(sample, self.index, volume, attenuation, channel, + flags, pitch, origin, direction, origins, update_positions, + sound_time, speaker_entity, download) + + # Make sure we have a tuple as recipients... + if not isinstance(recipients, tuple): + recipients = (recipients,) + + # Emit the sound to the given recipients... + sound.play(*recipients) + + def is_in_solid( + self, mask=ContentMasks.ALL, generator=BaseEntityGenerator): + """Return whether or not the entity is in solid. + + :param ContentMasks mask: + Contents the ray can possibly collide with. + :param generator: + A callable that returns an iterable which contains + :class:`BaseEntity` instances that are ignored by the ray. + :rtype: bool + """ + # Get a Ray object of the entity physic box + ray = Ray(self.origin, self.origin, self.mins, self.maxs) + + # Get a new GameTrace instance + trace = GameTrace() + + # Do the trace + engine_trace.trace_ray(ray, mask, TraceFilterSimple( + generator()), trace) + + # Return whether or not the trace did hit + return trace.did_hit() + + def take_damage( + self, damage, damage_type=DamageTypes.GENERIC, attacker_index=None, + weapon_index=None, hitgroup=HitGroup.GENERIC, skip_hooks=False, + **kwargs): + """Deal damage to the entity. + + :param int damage: + Amount of damage to deal. + :param DamageTypes damage_type: + Type of the dealed damage. + :param int attacker_index: + If not None, the index will be used as the attacker. + :param int weapon_index: + If not None, the index will be used as the weapon. This method + also tries to retrieve the attacker from the weapon, if + ``attacker_index`` wasn't set. + :param HitGroup hitgroup: + The hitgroup where the damage should be applied. + :param bool skip_hooks: + If True, the damage will be dealed directly by skipping any + registered hooks. + """ + # Import Entity classes + # Doing this in the global scope causes cross import errors + from weapons.entity import Weapon + + # Is the game supported? + if not hasattr(self, 'on_take_damage'): + + # Raise an error if not supported + raise NotImplementedError( + '"take_damage" is not implemented for {0}'.format(GAME_NAME)) + + # Store values for later use + attacker = None + weapon = None + + # Was an attacker given? + if attacker_index is not None: + + # Try to get the Entity instance of the attacker + with suppress(ValueError): + attacker = Entity(attacker_index) + + # Was a weapon given? + if weapon_index is not None: + + # Try to get the Weapon instance of the weapon + with suppress(ValueError): + weapon = Weapon(weapon_index) + + # Is there a weapon but no attacker? + if attacker is None and weapon is not None: + + # Try to get the attacker based off of the weapon's owner + with suppress(ValueError, OverflowError): + attacker_index = index_from_inthandle(weapon.owner_handle) + attacker = Entity(attacker_index) + + # Is there an attacker but no weapon? + if attacker is not None and weapon is None: + + # Try to use the attacker's active weapon + with suppress(AttributeError): + weapon = attacker.active_weapon + + # Try to set the hitgroup + with suppress(AttributeError): + self.hitgroup = hitgroup + + # Get a TakeDamageInfo instance + take_damage_info = TakeDamageInfo() + + # Is there a valid weapon? + if weapon is not None: + + # Is the weapon a projectile? + if weapon.classname in _projectile_weapons: + + # Set the inflictor to the weapon's index + take_damage_info.inflictor = weapon.index + + # Is the weapon not a projectile and the attacker is valid? + elif attacker_index is not None: + + # Set the inflictor to the attacker's index + take_damage_info.inflictor = attacker_index + + # Set the weapon to the weapon's index + take_damage_info.weapon = weapon.index + + # Is the attacker valid? + if attacker_index is not None: + + # Set the attacker to the attacker's index + take_damage_info.attacker = attacker_index + + # Set the damage amount + take_damage_info.damage = damage + + # Set the damage type value + take_damage_info.type = damage_type + + # Loop through the given keywords + for item in kwargs: + + # Set the offset's value + setattr(take_damage_info, item, kwargs[item]) + + if skip_hooks: + self.on_take_damage.skip_hooks(take_damage_info) + else: + self.on_take_damage(take_damage_info) + + @wrap_entity_mem_func + def teleport(self, origin=None, angle=None, velocity=None): + """Change the origin, angle and/or velocity of the entity. + + :param Vector origin: + New location of the entity. + :param QAngle angle: + New angle of the entity. + :param Vector velocity: + New velocity of the entity. + """ + return [origin, angle, velocity] + + @wrap_entity_mem_func + def set_parent(self, parent, attachment=INVALID_ATTACHMENT_INDEX): + """Set the parent of the entity. + + :param Pointer parent: + The parent. + :param str attachment: + The attachment name/index. + """ + if not isinstance(attachment, int): + attachment = self.lookup_attachment(attachment) + + return [parent, attachment] + + +# ============================================================================= +# >> LISTENERS +# ============================================================================= +@OnEntityDeleted +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(): + return + + # Get the index of the entity... + index = base_entity.index + + # Was no delay registered for this entity? + if index not in _entity_delays: + return + + # 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... + delay.cancel() + + # Remove the entity from the dictionary... + del _entity_delays[index] + + +# ============================================================================= +# >> ENGINE/GAME IMPORTS +# ============================================================================= +engine_import() diff --git a/addons/source-python/packages/source-python/players/_base.py b/addons/source-python/packages/source-python/players/_base.py deleted file mode 100644 index f1e901180..000000000 --- a/addons/source-python/packages/source-python/players/_base.py +++ /dev/null @@ -1,1013 +0,0 @@ -# ../players/_base.py - -"""Provides a class used to interact with a specific player.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Python Imports -# Math -import math - -# Source.Python Imports -# Bitbuffers -from bitbuffers import BitBufferWrite -# Core -from core import GAME_NAME -# Engines -from engines.server import server -from engines.server import engine_server -from engines.server import queue_server_command -from engines.server import server_game_dll -from engines.sound import Attenuation -from engines.sound import Channel -from engines.sound import Pitch -from engines.sound import Sound -from engines.sound import SoundFlags -from engines.sound import SOUND_FROM_WORLD -from engines.sound import StreamSound -from engines.sound import VOL_NORM -from engines.trace import engine_trace -from engines.trace import ContentMasks -from engines.trace import GameTrace -from engines.trace import MAX_TRACE_LENGTH -from engines.trace import Ray -from engines.trace import TraceFilterSimple -# Entities -from entities import ServerClassGenerator -from entities.constants import CollisionGroup -from entities.constants import EntityEffects -from entities.constants import INVALID_ENTITY_INDEX -from entities.constants import MoveType -from entities.constants import TakeDamage -from entities.entity import Entity -from entities.helpers import edict_from_index -from entities.helpers import index_from_inthandle -from entities.helpers import wrap_entity_mem_func -from entities.props import SendPropType -# Filters -from filters.entities import EntityIter -# Mathlib -from mathlib import NULL_VECTOR -from mathlib import Vector -from mathlib import QAngle -# Memory -from memory import get_object_pointer -from memory import make_object -# Players -from _players import PlayerMixin -from players.constants import PlayerStates -from players.helpers import address_from_playerinfo -from players.helpers import get_client_language -from players.helpers import playerinfo_from_index -from players.helpers import index_from_userid -from players.helpers import uniqueid_from_playerinfo -from players.voice import mute_manager -# Weapons -from weapons.default import NoWeaponManager -from weapons.entity import Weapon -from weapons.manager import weapon_manager -# Auth -from auth.manager import auth_manager - - -# ============================================================================= -# >> CLASSES -# ============================================================================= -class Player(Entity, PlayerMixin): - """Class used to interact directly with players.""" - - def __init__(self, index): - """Initialize the object. - - :param int index: - A valid player index. - :raise ValueError: - Raised if the index is invalid. - """ - super().__init__(index) - object.__setattr__(self, '_playerinfo', None) - - @classmethod - def from_userid(cls, userid): - """Create an instance from a userid. - - :param int userid: - The userid. - :rtype: Player - """ - return cls(index_from_userid(userid)) - - @property - def raw_steamid(self): - """Return the player's unformatted SteamID. - - :rtype: SteamID - """ - return engine_server.get_client_steamid(self.edict) - - @property - def permissions(self): - """Return the player's permissions. - - :rtype: PlayerPermissions - """ - return auth_manager.get_player_permissions_from_steamid(self.steamid) - - @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 - - @property - def userid(self): - """Return the player's userid. - - :rtype: int - """ - return self.playerinfo.userid - - @property - def steamid(self): - """Return the player's SteamID. - - :rtype: str - """ - return self.playerinfo.steamid - - def get_name(self): - """Return the player's name. - - :rtype: str - """ - return self.playerinfo.name - - def set_name(self, name): - """Set the player's name.""" - self.base_client.set_name(name) - - name = property(get_name, set_name) - - @property - def client(self): - """Return the player's client instance. - - :rtype: Client - """ - return server.get_client(self.index - 1) - - @property - def base_client(self): - """Return the player's base client instance. - - :rtype: BaseClient - """ - from players import BaseClient - return make_object(BaseClient, get_object_pointer(self.client) - 4) - - @property - def uniqueid(self): - """Return the player's unique ID. - - :rtype: str - """ - return uniqueid_from_playerinfo(self.playerinfo) - - @property - def address(self): - """Return the player's IP address and port. - - If the player is a bot, an empty string will be returned. - - :return: - The IP address. E.g. '127.0.0.1:27015' - :rtype: str - """ - return address_from_playerinfo(self.playerinfo) - - def is_connected(self): - """Return whether the player is connected. - - :rtype: bool - """ - return self.playerinfo.is_connected() - - def is_fake_client(self): - """Return whether the player is a fake client. - - :rtype: bool - """ - return self.playerinfo.is_fake_client() - - def is_hltv(self): - """Return whether the player is HLTV. - - :rtype: bool - """ - return self.playerinfo.is_hltv() - - def is_bot(self): - """Return whether the player is a bot. - - :rtype: bool - """ - return self.is_fake_client() or self.steamid == 'BOT' - - def is_in_a_vehicle(self): - """Return whether the player is in a vehicle. - - :rtype: bool - """ - return self.playerinfo.is_in_a_vehicle() - - def is_observer(self): - """Return whether the player is an observer. - - :rtype: bool - """ - return self.playerinfo.is_observer() - - def get_team(self): - """Return the player's team. - - :rtype: int - """ - return self.playerinfo.team - - def set_team(self, value): - """Set the players team.""" - self.playerinfo.team = value - - team = property(get_team, set_team) - - @property - def language(self): - """Return the player's language. - - If the player is a bot, an empty string will be returned. - - :rtype: str - """ - return get_client_language(self.index) - - def get_trace_ray(self, mask=ContentMasks.ALL, trace_filter=None): - """Return the player's current trace data. - - :param ContentMasks mask: - Will be passed to the trace filter. - :param TraceFilter trace_filter: - The trace filter to use. If ``None`` was given - :class:`engines.trace.TraceFilterSimple` will be used. - :rtype: GameTrace - """ - # Get the eye location of the player - start_vec = self.eye_location - - # Calculate the greatest possible distance - end_vec = start_vec + self.view_vector * MAX_TRACE_LENGTH - - # Create a new trace object - trace = GameTrace() - - # Start the trace - engine_trace.trace_ray( - Ray(start_vec, end_vec), mask, TraceFilterSimple( - (self,)) if trace_filter is None else trace_filter, - trace - ) - - # Return the trace data - return trace - - def get_view_coordinates(self): - """Return the coordinates the player is currently looking at. - - Return None if the player is not looking at anything. - - :rtype: Vector - """ - # Get the player's current trace data - trace = self.get_trace_ray() - - # Return the end position of the trace if it hit something - return trace.end_position if trace.did_hit() else None - - def set_view_coordinates(self, coords): - """Force the player to look at the given coordinates. - - :param Vector coords: - The coordinates the player should look at. - """ - coord_eye_vec = coords - self.eye_location - - # Calculate the y angle value - atan = math.degrees(math.atan(coord_eye_vec.y / coord_eye_vec.x)) - if coord_eye_vec.x < 0: - y_angle = atan + 180 - elif coord_eye_vec.y < 0: - y_angle = atan + 360 - else: - y_angle = atan - - # Calculate the x angle value - x_angle = 0 - math.degrees(math.atan(coord_eye_vec.z / math.sqrt( - coord_eye_vec.y ** 2 + coord_eye_vec.x ** 2))) - - # Set the new angle - self.teleport(None, QAngle(x_angle, y_angle, self.rotation.z), None) - - view_coordinates = property(get_view_coordinates, set_view_coordinates) - - def get_view_entity(self): - """Return the entity that the player is looking at. - - Return None if the player is not looking at an entity. - - :rtype: Entity - """ - # Get the player's current trace data - trace = self.get_trace_ray() - - # Did the trace hit? - if not trace.did_hit(): - return None - - # Return the hit entity as an Entity instance - return Entity(trace.entity_index) - - def set_view_entity(self, entity): - """Force the player to look at the origin of the given entity. - - :param Entity entity: - The entity the player should look at. - """ - self.view_coordinates = entity.origin - - view_entity = property(get_view_entity, set_view_entity) - - def get_view_player(self): - """Return the player that the player is looking at. - - Return None if the player is not looking at a player. - - :rtype: Player - """ - # Get the entity that the player is looking at - entity = self.view_entity - - # Return a Player instance of the player or None if not a player - return ( - Player(entity.index) if entity is not None and - entity.is_player() else None) - - def set_view_player(self, player): - """Force the player to look at the other player's eye location. - - :param Player player: The other player. - """ - self.view_coordinates = player.eye_location - - view_player = property(get_view_player, set_view_player) - - def set_eye_location(self, eye_location): - """Set the player's eye location.""" - self.teleport(eye_location - self.view_offset, None, None) - - eye_location = property(Entity.get_eye_location, set_eye_location) - - def get_view_angle(self): - """Return the player's view angle. - - :rtype: QAngle - """ - return super().view_angle - - def set_view_angle(self, angle): - """Set the player's view angle.""" - # Make sure that only QAngle objects are passed. Otherwise you can - # easily crash the server or cause unexpected behaviour - assert isinstance(angle, QAngle) - self.teleport(None, angle, None) - - view_angle = property(get_view_angle, set_view_angle) - - def push(self, horiz_mul, vert_mul, vert_override=False): - """Push the player along his view vector. - - :param float horiz_mul: - Horizontal multiplier. - :param float vert_mul: - Vertical multiplier. - :param bool vert_override: - If ``True``, ``vert_mul`` will be used as a static value and not - as a multiplier. - """ - x, y, z = tuple(self.view_vector) - self.base_velocity = Vector( - x * horiz_mul, y * horiz_mul, - z * vert_mul if not vert_override else vert_mul) - - def client_command(self, command, server_side=False): - """Execute a command on the client. - - :param str command: - The command to execute. - :param bool server_side: - If ``True`` the command will be emulated by the server. - """ - engine_server.client_command(self.edict, command, server_side) - - def slay(self): - """Slay the player.""" - self.client_command('kill', True) - - def say(self, message): - """Force the player to say something in the global chat. - - :param str message: - The text the player should say. - """ - self.client_command('say {0}'.format(message), True) - - def say_team(self, message): - """Force the player to say something in the team chat. - - :param str message: - The text the player should say. - """ - self.client_command('say_team {0}'.format(message), True) - - def mute(self, receivers=None): - """Mute the player. - - See players.voice.mute_manager.mute_player for more information. - """ - mute_manager.mute_player(self.index, receivers) - - def unmute(self, receivers=None): - """Unmute the player. - - See players.voice.mute_manager.unmute_player for more information. - """ - mute_manager.unmute_player(self.index, receivers) - - def is_muted(self, receivers=None): - """Return True if the player is currently muted. - - See players.voice.mute_manager.is_muted for more information. - """ - return mute_manager.is_muted(self.index, receivers) - - def set_noclip(self, enable): - """Enable/disable noclip mode. - - Noclip mode gives the player the ability to fly through the map. - - :param bool enable: - If ``True`` noclip mode will be enabled. - """ - if enable: - self.move_type = MoveType.NOCLIP - else: - self.move_type = MoveType.WALK - - def get_noclip(self): - """Return whether noclip mode is enabled. - - :rtype: bool - """ - return self.move_type == MoveType.NOCLIP - - noclip = property(get_noclip, set_noclip) - - def set_jetpack(self, enable): - """Enable/disable jetpack mode. - - Jetpack mode gives the player the ability to use a jetpack. - - :param bool enable: - If ``True`` jetpack mode will be enabled. - """ - if enable: - self.move_type = MoveType.FLY - else: - self.move_type = MoveType.WALK - - def get_jetpack(self): - """Return whether jetpack mode is enabled. - - :rtype: bool - """ - return self.move_type == MoveType.FLY - - jetpack = property(get_jetpack, set_jetpack) - - def set_godmode(self, enable): - """Enable/disable god mode. - - Godmode makes the player invulnerable. - - :param bool enable: - If ``True`` god mode will be enabled. - - .. todo:: - - Add m_takedamage to the data files. Which name do we want to use? - We can't use take_damage. - """ - if enable: - self.set_property_uchar('m_takedamage', TakeDamage.NO) - else: - self.set_property_uchar('m_takedamage', TakeDamage.YES) - - def get_godmode(self): - """Return whether god mode is enabled. - - :rtype: bool - - .. todo:: - - Add m_takedamage to the data files. Which name do we want to use? - We can't use take_damage. - """ - return self.get_property_uchar('m_takedamage') == TakeDamage.NO - - godmode = property(get_godmode, set_godmode) - - def set_noblock(self, enable): - """Enable/disable noblock mode. - - Noblock mode assigns a new collision group to the player that doesn't - block other players. That means players can run through each other. - - :param bool enable: - If ``True`` noblock mode will be enabled. - """ - if enable: - self.collision_group = CollisionGroup.DEBRIS_TRIGGER - else: - self.collision_group = CollisionGroup.PLAYER - - def get_noblock(self): - """Return whether noblock mode is enabled. - - :rtype: bool - """ - return self.collision_group == CollisionGroup.DEBRIS_TRIGGER - - noblock = property(get_noblock, set_noblock) - - def set_frozen(self, enable): - """Enable/disable frozen mode. - - Frozen mode makes the player unable to move, look and shoot. - - :param bool enable: - If ``True`` frozen mode will be enabled. - """ - if enable: - self.flags |= PlayerStates.FROZEN - else: - self.flags &= ~PlayerStates.FROZEN - - def get_frozen(self): - """Return whether frozen mode is enabled. - - :rtype: bool - """ - return bool(self.flags & PlayerStates.FROZEN) - - frozen = property(get_frozen, set_frozen) - - def set_stuck(self, enable): - """Enable/disable stuck mode. - - Stuck mode forces the player to stay exactly at his current position - even if he is currently in the air. He's still able to look and shoot. - - :param bool enable: - If ``True`` stuck mode will be enabled. - """ - if enable: - self.move_type = MoveType.NONE - else: - self.move_type = MoveType.WALK - - def get_stuck(self): - """Return whether stuck mode is enabled. - - :rtype: bool - """ - return self.move_type == MoveType.NONE - - stuck = property(get_stuck, set_stuck) - - def get_flashlight(self): - """Return whether or not the flashlight of the player is turned on. - - :rtype: bool - """ - return bool(self.effects & EntityEffects.DIMLIGHT) - - def set_flashlight(self, enable): - """Turn on/off the flashlight of the player. - - :param bool enable: - ``True`` to turn on, ``False`` to turn off. - """ - if enable: - self.effects |= EntityEffects.DIMLIGHT - else: - self.effects &= ~EntityEffects.DIMLIGHT - - flashlight = property(get_flashlight, set_flashlight) - - def send_convar_value(self, cvar_name, value): - """Send a convar value. - - :param str cvar_name: - Name of the convar. - :param str value: - Value to send. - """ - buffer_size = 256 - buffer = BitBufferWrite(buffer_size) - buffer.write_ubit_long(5, 6) - buffer.write_byte(1) - buffer.write_string(cvar_name) - buffer.write_string(str(value)) - self.client.net_channel.send_data(buffer) - - @property - def spectators(self): - """Return all players observing this player. - - :return: - The generator yields :class:`players.entity.Player` objects. - :rtype: generator - """ - from filters.players import PlayerIter - for other in PlayerIter('dead'): - if self.inthandle == other.observer_target: - yield other - - def kick(self, message=''): - """Kick the player from the server. - - :param str message: - A message the kicked player will receive. - """ - message = message.rstrip() - if message: - self.client.disconnect(message) - else: - queue_server_command('kickid', self.userid, message) - - def ban(self, duration=0, kick=True, write_ban=True): - """Ban a player from the server. - - :param int duration: - Duration of the ban in minutes. Use 0 for permament. - :param bool kick: - If ``True``, the player will be kicked as well. - :param bool write_ban: - If ``True``, the ban will be written to ``cfg/banned_users.cfg``. - """ - queue_server_command( - 'banid', duration, self.userid, 'kick' if kick else '') - if write_ban: - queue_server_command('writeid') - - def play_sound( - self, sample, volume=VOL_NORM, attenuation=Attenuation.NONE, - channel=Channel.AUTO, flags=SoundFlags.NO_FLAGS, - pitch=Pitch.NORMAL, origin=NULL_VECTOR, direction=NULL_VECTOR, - origins=(), update_positions=True, sound_time=0.0, - speaker_entity=INVALID_ENTITY_INDEX, download=False, - stream=False): - """Play a sound to the player. - - :param str sample: - Sound file relative to the ``sounds`` directory. - :param float volume: - Volume of the sound. - :param Attenuation attenuation: - How far the sound should reaches. - :param int channel: - Channel to emit the sound with. - :param SoundFlags flags: - Flags of the sound. - :param Pitch pitch: - Pitch of the sound. - :param Vector origin: - Origin of the sound. - :param Vector direction: - Direction of the sound. - :param tuple origins: - Origins of the sound. - :param bool update_positions: - Whether or not the positions should be updated. - :param float sound_time: - Time to play the sound for. - :param int speaker_entity: - Index of the speaker entity. - :param bool download: - Whether or not the sample should be added to the downloadables. - :param bool stream: - Whether or not the sound should be streamed. - """ - # Don't bother playing sounds to bots... - if self.is_fake_client(): - return - - # Get the correct Sound class... - if not stream: - sound_class = Sound - else: - sound_class = StreamSound - - # Get the sound... - sound = sound_class(sample, SOUND_FROM_WORLD, volume, attenuation, - channel, flags, pitch, origin, direction, origins, - update_positions, sound_time, speaker_entity, download) - - # Play the sound to the player... - sound.play(self.index) - - def spawn(self, force=False): - """Spawn the player. - - :param bool force: - Whether or not the spawn should be forced. - """ - # Is the player spawnable? - if not force and (self.team <= 1 or not self.dead): - return - - # Spawn the player... - super().spawn() - - # ========================================================================= - # >> PLAYER WEAPON FUNCTIONALITY - # ========================================================================= - @property - def primary(self): - """Return the player's primary weapon. - - :rtype: Weapon - """ - return self.get_weapon(is_filters='primary') - - @property - def secondary(self): - """Return the player's secondary weapon. - - :rtype: Weapon - """ - return self.get_weapon(is_filters='secondary') - - def get_active_weapon(self): - """Return the player's active weapon. - - :return: - ``None`` if the player does not have an active weapon. - :rtype: Weapon - """ - try: - index = index_from_inthandle(self.active_weapon_handle) - except (ValueError, OverflowError): - return None - - return Weapon(index) - - def set_active_weapon(self, weapon): - """Set the player's active weapon. - - :param Weapon weapon: - The weapon to set as active. - """ - self.active_weapon_handle = weapon.inthandle - - active_weapon = property(get_active_weapon, set_active_weapon) - - def get_weapon(self, classname=None, is_filters=None, not_filters=None): - """Return the first found weapon for the given arguments. - - :rtype: Weapon - """ - # Loop through all weapons for the given arguments - for weapon in self.weapons(classname, is_filters, not_filters): - - # Return the first found weapon - return weapon - - # If no weapon is found, return None - return None - - def weapons(self, classname=None, is_filters=None, not_filters=None): - """Iterate over the player's weapons for the given arguments. - - :return: - A generator of :class:`weapons.entity.Weapon` objects. - :rtype: generator - """ - # Loop through all the players weapons for the given arguments - for index in self.weapon_indexes(classname, is_filters, not_filters): - - # Yield the current weapon - yield Weapon(index) - - def weapon_indexes( - self, classname=None, is_filters=None, not_filters=None): - """Iterate over the player's weapon indexes for the given arguments. - - :return: - A generator of indexes. - :rtype: generator - """ - # Is the weapon array supported for the current game? - if _weapon_prop_length is None: - return - - # Loop through the length of the weapon array - for offset in range(_weapon_prop_length): - - # Get the player's current weapon at this offset - handle = self.get_property_int( - '{base}{offset:03d}'.format( - base=weapon_manager.myweapons, - offset=offset, - ) - ) - - # Try to get the index of the handle - try: - index = index_from_inthandle(handle) - except (ValueError, OverflowError): - continue - - # Get the weapon's classname - weapon_class = edict_from_index(index).classname - - # Was a classname given and the current - # weapon is not of that classname? - if classname is not None and weapon_class != classname: - - # Do not yield this index - continue - - # Import WeaponClassIter to use its functionality - from filters.weapons import WeaponClassIter - - # Was a weapon type given and the - # current weapon is not of that type? - if not (is_filters is None and not_filters is None): - if weapon_class not in map( - lambda value: value.name, - WeaponClassIter(is_filters, not_filters)): - - # Do not yield this index - continue - - # Yield the index - yield index - - def has_c4(self): - """Raise an error because this method is game specific.""" - raise NotImplementedError( - 'Method not supported for game "{game}".'.format(game=GAME_NAME) - ) - - def get_projectile_ammo(self, projectile): - """Return the player's ammo value of the given projectile. - - :param str projectile: - The name of the projectile to get the ammo of. - :rtype: int - """ - return self.get_property_int( - '{base}{prop:03d}'.format( - base=weapon_manager.ammoprop, - prop=weapon_manager[projectile].ammoprop, - ) - ) - - def set_projectile_ammo(self, projectile, value): - """Set the player's ammo value for the given projectile. - - :param str projectile: - The name of the projectile to set the ammo of. - :param int value: - The value to set the projectile's ammo to. - """ - self.set_property_int( - '{base}{prop:03d}'.format( - base=weapon_manager.ammoprop, - prop=weapon_manager[projectile].ammoprop, - ), - value, - ) - - def projectile_indexes(self, projectile): - """Yield all indexes of the given projectile for the player. - - :param str projectile: - The name of the projectile to find indexes of. - """ - if projectile in weapon_manager.projectiles: - for entity in EntityIter(projectile): - if entity.owner == self: - yield entity.index - else: - yield from self.weapon_indexes(weapon_manager[projectile].name) - - def restrict_weapons(self, *weapons): - """Restrict the weapon for the player. - - :param str weapons: - A weapon or any number of weapons to add as restricted for the - player. - """ - from weapons.restrictions import weapon_restriction_handler - weapon_restriction_handler.add_player_restrictions(self, *weapons) - - def unrestrict_weapons(self, *weapons): - """Restrict the weapon for the player. - - :param str weapons: - A weapon or any number of weapons to remove as restricted for the - player. - """ - from weapons.restrictions import weapon_restriction_handler - weapon_restriction_handler.remove_player_restrictions(self, *weapons) - - def is_weapon_restricted(self, weapon): - """Return whether the player is restricted from the given weapon. - - :param str weapon: - The name of the weapon to check against restriction. - :rtype: bool - """ - from weapons.restrictions import weapon_restriction_manager - return weapon_restriction_manager.is_player_restricted(self, weapon) - - @wrap_entity_mem_func - def drop_weapon(self, weapon, target=None, velocity=None): - """Drop a weapon. - - :param Pointer weapon: - Weapon to drop. - :param Vector target: - Target location to drop the weapon at. - :param Vector velocity: - Velocity to use to drop the weapon. - """ - return [weapon, target, velocity] - - -# ============================================================================= -# >> HELPER FUNCTIONS -# ============================================================================= -def _find_weapon_prop_length(table): - """Loop through a prop table to find the myweapons property length. - - :rtype: int - """ - # Loop through the props in the table - for item in table: - - # Is this the m_hMyWeapons prop? - if item.name == weapon_manager.myweapons[:~0]: - - # If so, return the length of the prop table - return len(item.data_table) - - # Is the current prop a table? - if item.type == SendPropType.DATATABLE: - - # Loop through the table - _find_weapon_prop_length(item.data_table) - -# Default the weapon prop length to None -_weapon_prop_length = None - -# Is the game supported? -if not isinstance(weapon_manager, NoWeaponManager): - - # Loop through all ServerClass objects - for _current_class in ServerClassGenerator(): - - # Loop through the ServerClass' props - _weapon_prop_length = _find_weapon_prop_length(_current_class.table) - - # Was m_hMyWeapons found? - if _weapon_prop_length is not None: - - # No need to continue looping - break diff --git a/addons/source-python/packages/source-python/players/engines/bms/__init__.py b/addons/source-python/packages/source-python/players/bms/entity.py similarity index 57% rename from addons/source-python/packages/source-python/players/engines/bms/__init__.py rename to addons/source-python/packages/source-python/players/bms/entity.py index 987063197..26b999ddf 100644 --- a/addons/source-python/packages/source-python/players/engines/bms/__init__.py +++ b/addons/source-python/packages/source-python/players/bms/entity.py @@ -1,21 +1,11 @@ -# ../players/engines/bms/__init__.py +# ../players/bms/entity.py """Provides BM:S specific Player based functionality.""" -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python Imports -# Entities -from entities.helpers import wrap_entity_mem_func -# Players -from players._base import Player as _Player - - # ============================================================================= # >> CLASSES # ============================================================================= -class Player(_Player): +class Player(Player): """Class used to interact directly with players.""" @wrap_entity_mem_func diff --git a/addons/source-python/packages/source-python/players/engines/csgo/__init__.py b/addons/source-python/packages/source-python/players/csgo/entity.py similarity index 93% rename from addons/source-python/packages/source-python/players/engines/csgo/__init__.py rename to addons/source-python/packages/source-python/players/csgo/entity.py index 47f7dcaef..e397b1986 100644 --- a/addons/source-python/packages/source-python/players/engines/csgo/__init__.py +++ b/addons/source-python/packages/source-python/players/csgo/entity.py @@ -1,4 +1,4 @@ -# ../players/engines/csgo/__init__.py +# ../players/csgo/entity.py """Provides CS:GO specific Player based functionality.""" @@ -6,16 +6,8 @@ # >> IMPORTS # ============================================================================= # Source.Python Imports -# Bitbuffers -from bitbuffers import BitBufferWrite # ConVars from cvars import ConVar -# Engines -from engines.server import engine_server -# Entities -from entities.helpers import wrap_entity_mem_func -# Filters -from filters.entities import EntityIter # Memory from memory import NULL from memory import get_virtual_function @@ -23,10 +15,7 @@ # Messages from _messages import ProtobufMessage # Players -from players._base import Player as _Player from players.constants import LifeState -# Weapons -from weapons.manager import weapon_manager # ============================================================================= @@ -38,7 +27,7 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class Player(_Player): +class Player(Player): """Class used to interact directly with players.""" def _get_kills(self): diff --git a/addons/source-python/packages/source-python/players/engines/__init__.py b/addons/source-python/packages/source-python/players/engines/__init__.py deleted file mode 100644 index 18c85e258..000000000 --- a/addons/source-python/packages/source-python/players/engines/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# ../players/engines/__init__.py - -"""Provides engine specific Player based functionality.""" diff --git a/addons/source-python/packages/source-python/players/entity.py b/addons/source-python/packages/source-python/players/entity.py index 86928b5e1..238613655 100644 --- a/addons/source-python/packages/source-python/players/entity.py +++ b/addons/source-python/packages/source-python/players/entity.py @@ -1,14 +1,75 @@ -# ../players/entity.py +# ../players/_base.py """Provides a class used to interact with a specific player.""" # ============================================================================= # >> IMPORTS # ============================================================================= -from importlib import import_module +# Python Imports +# Math +import math + +# Source.Python Imports +# Bitbuffers +from bitbuffers import BitBufferWrite +# Core from core import GAME_NAME -from core import SOURCE_ENGINE -from paths import SP_PACKAGES_PATH +from core import engine_import +# Engines +from engines.server import server +from engines.server import engine_server +from engines.server import queue_server_command +from engines.server import server_game_dll +from engines.sound import Attenuation +from engines.sound import Channel +from engines.sound import Pitch +from engines.sound import Sound +from engines.sound import SoundFlags +from engines.sound import SOUND_FROM_WORLD +from engines.sound import StreamSound +from engines.sound import VOL_NORM +from engines.trace import engine_trace +from engines.trace import ContentMasks +from engines.trace import GameTrace +from engines.trace import MAX_TRACE_LENGTH +from engines.trace import Ray +from engines.trace import TraceFilterSimple +# Entities +from entities import ServerClassGenerator +from entities.constants import CollisionGroup +from entities.constants import EntityEffects +from entities.constants import INVALID_ENTITY_INDEX +from entities.constants import MoveType +from entities.constants import TakeDamage +from entities.entity import Entity +from entities.helpers import edict_from_index +from entities.helpers import index_from_inthandle +from entities.helpers import wrap_entity_mem_func +from entities.props import SendPropType +# Filters +from filters.entities import EntityIter +# Mathlib +from mathlib import NULL_VECTOR +from mathlib import Vector +from mathlib import QAngle +# Memory +from memory import get_object_pointer +from memory import make_object +# Players +from _players import PlayerMixin +from players.constants import PlayerStates +from players.helpers import address_from_playerinfo +from players.helpers import get_client_language +from players.helpers import playerinfo_from_index +from players.helpers import index_from_userid +from players.helpers import uniqueid_from_playerinfo +from players.voice import mute_manager +# Weapons +from weapons.default import NoWeaponManager +from weapons.entity import Weapon +from weapons.manager import weapon_manager +# Auth +from auth.manager import auth_manager # ============================================================================= @@ -19,32 +80,942 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# >> CLASSES # ============================================================================= -if SP_PACKAGES_PATH.joinpath( - 'players', 'engines', SOURCE_ENGINE, GAME_NAME + '.py' -).isfile(): +class Player(Entity, PlayerMixin): + """Class used to interact directly with players.""" + + def __init__(self, index): + """Initialize the object. + + :param int index: + A valid player index. + :raise ValueError: + Raised if the index is invalid. + """ + super().__init__(index) + object.__setattr__(self, '_playerinfo', None) + + @classmethod + def from_userid(cls, userid): + """Create an instance from a userid. + + :param int userid: + The userid. + :rtype: Player + """ + return cls(index_from_userid(userid)) + + @property + def raw_steamid(self): + """Return the player's unformatted SteamID. + + :rtype: SteamID + """ + return engine_server.get_client_steamid(self.edict) + + @property + def permissions(self): + """Return the player's permissions. + + :rtype: PlayerPermissions + """ + return auth_manager.get_player_permissions_from_steamid(self.steamid) + + @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 + + @property + def userid(self): + """Return the player's userid. + + :rtype: int + """ + return self.playerinfo.userid + + @property + def steamid(self): + """Return the player's SteamID. + + :rtype: str + """ + return self.playerinfo.steamid + + def get_name(self): + """Return the player's name. + + :rtype: str + """ + return self.playerinfo.name + + def set_name(self, name): + """Set the player's name.""" + self.base_client.set_name(name) + + name = property(get_name, set_name) + + @property + def client(self): + """Return the player's client instance. + + :rtype: Client + """ + return server.get_client(self.index - 1) + + @property + def base_client(self): + """Return the player's base client instance. + + :rtype: BaseClient + """ + from players import BaseClient + return make_object(BaseClient, get_object_pointer(self.client) - 4) + + @property + def uniqueid(self): + """Return the player's unique ID. + + :rtype: str + """ + return uniqueid_from_playerinfo(self.playerinfo) + + @property + def address(self): + """Return the player's IP address and port. + + If the player is a bot, an empty string will be returned. + + :return: + The IP address. E.g. '127.0.0.1:27015' + :rtype: str + """ + return address_from_playerinfo(self.playerinfo) + + def is_connected(self): + """Return whether the player is connected. + + :rtype: bool + """ + return self.playerinfo.is_connected() + + def is_fake_client(self): + """Return whether the player is a fake client. + + :rtype: bool + """ + return self.playerinfo.is_fake_client() + + def is_hltv(self): + """Return whether the player is HLTV. + + :rtype: bool + """ + return self.playerinfo.is_hltv() + + def is_bot(self): + """Return whether the player is a bot. + + :rtype: bool + """ + return self.is_fake_client() or self.steamid == 'BOT' + + def is_in_a_vehicle(self): + """Return whether the player is in a vehicle. + + :rtype: bool + """ + return self.playerinfo.is_in_a_vehicle() + + def is_observer(self): + """Return whether the player is an observer. + + :rtype: bool + """ + return self.playerinfo.is_observer() + + def get_team(self): + """Return the player's team. + + :rtype: int + """ + return self.playerinfo.team + + def set_team(self, value): + """Set the players team.""" + self.playerinfo.team = value + + team = property(get_team, set_team) + + @property + def language(self): + """Return the player's language. + + If the player is a bot, an empty string will be returned. + + :rtype: str + """ + return get_client_language(self.index) + + def get_trace_ray(self, mask=ContentMasks.ALL, trace_filter=None): + """Return the player's current trace data. + + :param ContentMasks mask: + Will be passed to the trace filter. + :param TraceFilter trace_filter: + The trace filter to use. If ``None`` was given + :class:`engines.trace.TraceFilterSimple` will be used. + :rtype: GameTrace + """ + # Get the eye location of the player + start_vec = self.eye_location + + # Calculate the greatest possible distance + end_vec = start_vec + self.view_vector * MAX_TRACE_LENGTH + + # Create a new trace object + trace = GameTrace() + + # Start the trace + engine_trace.trace_ray( + Ray(start_vec, end_vec), mask, TraceFilterSimple( + (self,)) if trace_filter is None else trace_filter, + trace + ) + + # Return the trace data + return trace + + def get_view_coordinates(self): + """Return the coordinates the player is currently looking at. + + Return None if the player is not looking at anything. + + :rtype: Vector + """ + # Get the player's current trace data + trace = self.get_trace_ray() + + # Return the end position of the trace if it hit something + return trace.end_position if trace.did_hit() else None + + def set_view_coordinates(self, coords): + """Force the player to look at the given coordinates. + + :param Vector coords: + The coordinates the player should look at. + """ + coord_eye_vec = coords - self.eye_location + + # Calculate the y angle value + atan = math.degrees(math.atan(coord_eye_vec.y / coord_eye_vec.x)) + if coord_eye_vec.x < 0: + y_angle = atan + 180 + elif coord_eye_vec.y < 0: + y_angle = atan + 360 + else: + y_angle = atan + + # Calculate the x angle value + x_angle = 0 - math.degrees(math.atan(coord_eye_vec.z / math.sqrt( + coord_eye_vec.y ** 2 + coord_eye_vec.x ** 2))) + + # Set the new angle + self.teleport(None, QAngle(x_angle, y_angle, self.rotation.z), None) + + view_coordinates = property(get_view_coordinates, set_view_coordinates) + + def get_view_entity(self): + """Return the entity that the player is looking at. + + Return None if the player is not looking at an entity. + + :rtype: Entity + """ + # Get the player's current trace data + trace = self.get_trace_ray() + + # Did the trace hit? + if not trace.did_hit(): + return None + + # Return the hit entity as an Entity instance + return Entity(trace.entity_index) + + def set_view_entity(self, entity): + """Force the player to look at the origin of the given entity. + + :param Entity entity: + The entity the player should look at. + """ + self.view_coordinates = entity.origin + + view_entity = property(get_view_entity, set_view_entity) + + def get_view_player(self): + """Return the player that the player is looking at. + + Return None if the player is not looking at a player. + + :rtype: Player + """ + # Get the entity that the player is looking at + entity = self.view_entity + + # Return a Player instance of the player or None if not a player + return ( + Player(entity.index) if entity is not None and + entity.is_player() else None) + + def set_view_player(self, player): + """Force the player to look at the other player's eye location. + + :param Player player: The other player. + """ + self.view_coordinates = player.eye_location + + view_player = property(get_view_player, set_view_player) + + def set_eye_location(self, eye_location): + """Set the player's eye location.""" + self.teleport(eye_location - self.view_offset, None, None) + + eye_location = property(Entity.get_eye_location, set_eye_location) + + def get_view_angle(self): + """Return the player's view angle. + + :rtype: QAngle + """ + return super().view_angle + + def set_view_angle(self, angle): + """Set the player's view angle.""" + # Make sure that only QAngle objects are passed. Otherwise you can + # easily crash the server or cause unexpected behaviour + assert isinstance(angle, QAngle) + self.teleport(None, angle, None) + + view_angle = property(get_view_angle, set_view_angle) + + def push(self, horiz_mul, vert_mul, vert_override=False): + """Push the player along his view vector. + + :param float horiz_mul: + Horizontal multiplier. + :param float vert_mul: + Vertical multiplier. + :param bool vert_override: + If ``True``, ``vert_mul`` will be used as a static value and not + as a multiplier. + """ + x, y, z = tuple(self.view_vector) + self.base_velocity = Vector( + x * horiz_mul, y * horiz_mul, + z * vert_mul if not vert_override else vert_mul) + + def client_command(self, command, server_side=False): + """Execute a command on the client. + + :param str command: + The command to execute. + :param bool server_side: + If ``True`` the command will be emulated by the server. + """ + engine_server.client_command(self.edict, command, server_side) + + def slay(self): + """Slay the player.""" + self.client_command('kill', True) + + def say(self, message): + """Force the player to say something in the global chat. + + :param str message: + The text the player should say. + """ + self.client_command('say {0}'.format(message), True) + + def say_team(self, message): + """Force the player to say something in the team chat. + + :param str message: + The text the player should say. + """ + self.client_command('say_team {0}'.format(message), True) + + def mute(self, receivers=None): + """Mute the player. + + See players.voice.mute_manager.mute_player for more information. + """ + mute_manager.mute_player(self.index, receivers) + + def unmute(self, receivers=None): + """Unmute the player. + + See players.voice.mute_manager.unmute_player for more information. + """ + mute_manager.unmute_player(self.index, receivers) + + def is_muted(self, receivers=None): + """Return True if the player is currently muted. + + See players.voice.mute_manager.is_muted for more information. + """ + return mute_manager.is_muted(self.index, receivers) + + def set_noclip(self, enable): + """Enable/disable noclip mode. + + Noclip mode gives the player the ability to fly through the map. + + :param bool enable: + If ``True`` noclip mode will be enabled. + """ + if enable: + self.move_type = MoveType.NOCLIP + else: + self.move_type = MoveType.WALK + + def get_noclip(self): + """Return whether noclip mode is enabled. + + :rtype: bool + """ + return self.move_type == MoveType.NOCLIP + + noclip = property(get_noclip, set_noclip) + + def set_jetpack(self, enable): + """Enable/disable jetpack mode. + + Jetpack mode gives the player the ability to use a jetpack. + + :param bool enable: + If ``True`` jetpack mode will be enabled. + """ + if enable: + self.move_type = MoveType.FLY + else: + self.move_type = MoveType.WALK - # Import the game-specific 'Player' class - Player = import_module( - 'players.engines.{engine}.{game}'.format( - engine=SOURCE_ENGINE, - game=GAME_NAME, + def get_jetpack(self): + """Return whether jetpack mode is enabled. + + :rtype: bool + """ + return self.move_type == MoveType.FLY + + jetpack = property(get_jetpack, set_jetpack) + + def set_godmode(self, enable): + """Enable/disable god mode. + + Godmode makes the player invulnerable. + + :param bool enable: + If ``True`` god mode will be enabled. + + .. todo:: + + Add m_takedamage to the data files. Which name do we want to use? + We can't use take_damage. + """ + if enable: + self.set_property_uchar('m_takedamage', TakeDamage.NO) + else: + self.set_property_uchar('m_takedamage', TakeDamage.YES) + + def get_godmode(self): + """Return whether god mode is enabled. + + :rtype: bool + + .. todo:: + + Add m_takedamage to the data files. Which name do we want to use? + We can't use take_damage. + """ + return self.get_property_uchar('m_takedamage') == TakeDamage.NO + + godmode = property(get_godmode, set_godmode) + + def set_noblock(self, enable): + """Enable/disable noblock mode. + + Noblock mode assigns a new collision group to the player that doesn't + block other players. That means players can run through each other. + + :param bool enable: + If ``True`` noblock mode will be enabled. + """ + if enable: + self.collision_group = CollisionGroup.DEBRIS_TRIGGER + else: + self.collision_group = CollisionGroup.PLAYER + + def get_noblock(self): + """Return whether noblock mode is enabled. + + :rtype: bool + """ + return self.collision_group == CollisionGroup.DEBRIS_TRIGGER + + noblock = property(get_noblock, set_noblock) + + def set_frozen(self, enable): + """Enable/disable frozen mode. + + Frozen mode makes the player unable to move, look and shoot. + + :param bool enable: + If ``True`` frozen mode will be enabled. + """ + if enable: + self.flags |= PlayerStates.FROZEN + else: + self.flags &= ~PlayerStates.FROZEN + + def get_frozen(self): + """Return whether frozen mode is enabled. + + :rtype: bool + """ + return bool(self.flags & PlayerStates.FROZEN) + + frozen = property(get_frozen, set_frozen) + + def set_stuck(self, enable): + """Enable/disable stuck mode. + + Stuck mode forces the player to stay exactly at his current position + even if he is currently in the air. He's still able to look and shoot. + + :param bool enable: + If ``True`` stuck mode will be enabled. + """ + if enable: + self.move_type = MoveType.NONE + else: + self.move_type = MoveType.WALK + + def get_stuck(self): + """Return whether stuck mode is enabled. + + :rtype: bool + """ + return self.move_type == MoveType.NONE + + stuck = property(get_stuck, set_stuck) + + def get_flashlight(self): + """Return whether or not the flashlight of the player is turned on. + + :rtype: bool + """ + return bool(self.effects & EntityEffects.DIMLIGHT) + + def set_flashlight(self, enable): + """Turn on/off the flashlight of the player. + + :param bool enable: + ``True`` to turn on, ``False`` to turn off. + """ + if enable: + self.effects |= EntityEffects.DIMLIGHT + else: + self.effects &= ~EntityEffects.DIMLIGHT + + flashlight = property(get_flashlight, set_flashlight) + + def send_convar_value(self, cvar_name, value): + """Send a convar value. + + :param str cvar_name: + Name of the convar. + :param str value: + Value to send. + """ + buffer_size = 256 + buffer = BitBufferWrite(buffer_size) + buffer.write_ubit_long(5, 6) + buffer.write_byte(1) + buffer.write_string(cvar_name) + buffer.write_string(str(value)) + self.client.net_channel.send_data(buffer) + + @property + def spectators(self): + """Return all players observing this player. + + :return: + The generator yields :class:`players.entity.Player` objects. + :rtype: generator + """ + from filters.players import PlayerIter + for other in PlayerIter('dead'): + if self.inthandle == other.observer_target: + yield other + + def kick(self, message=''): + """Kick the player from the server. + + :param str message: + A message the kicked player will receive. + """ + message = message.rstrip() + if message: + self.client.disconnect(message) + else: + queue_server_command('kickid', self.userid, message) + + def ban(self, duration=0, kick=True, write_ban=True): + """Ban a player from the server. + + :param int duration: + Duration of the ban in minutes. Use 0 for permament. + :param bool kick: + If ``True``, the player will be kicked as well. + :param bool write_ban: + If ``True``, the ban will be written to ``cfg/banned_users.cfg``. + """ + queue_server_command( + 'banid', duration, self.userid, 'kick' if kick else '') + if write_ban: + queue_server_command('writeid') + + def play_sound( + self, sample, volume=VOL_NORM, attenuation=Attenuation.NONE, + channel=Channel.AUTO, flags=SoundFlags.NO_FLAGS, + pitch=Pitch.NORMAL, origin=NULL_VECTOR, direction=NULL_VECTOR, + origins=(), update_positions=True, sound_time=0.0, + speaker_entity=INVALID_ENTITY_INDEX, download=False, + stream=False): + """Play a sound to the player. + + :param str sample: + Sound file relative to the ``sounds`` directory. + :param float volume: + Volume of the sound. + :param Attenuation attenuation: + How far the sound should reaches. + :param int channel: + Channel to emit the sound with. + :param SoundFlags flags: + Flags of the sound. + :param Pitch pitch: + Pitch of the sound. + :param Vector origin: + Origin of the sound. + :param Vector direction: + Direction of the sound. + :param tuple origins: + Origins of the sound. + :param bool update_positions: + Whether or not the positions should be updated. + :param float sound_time: + Time to play the sound for. + :param int speaker_entity: + Index of the speaker entity. + :param bool download: + Whether or not the sample should be added to the downloadables. + :param bool stream: + Whether or not the sound should be streamed. + """ + # Don't bother playing sounds to bots... + if self.is_fake_client(): + return + + # Get the correct Sound class... + if not stream: + sound_class = Sound + else: + sound_class = StreamSound + + # Get the sound... + sound = sound_class(sample, SOUND_FROM_WORLD, volume, attenuation, + channel, flags, pitch, origin, direction, origins, + update_positions, sound_time, speaker_entity, download) + + # Play the sound to the player... + sound.play(self.index) + + def spawn(self, force=False): + """Spawn the player. + + :param bool force: + Whether or not the spawn should be forced. + """ + # Is the player spawnable? + if not force and (self.team <= 1 or not self.dead): + return + + # Spawn the player... + super().spawn() + + # ========================================================================= + # >> PLAYER WEAPON FUNCTIONALITY + # ========================================================================= + @property + def primary(self): + """Return the player's primary weapon. + + :rtype: Weapon + """ + return self.get_weapon(is_filters='primary') + + @property + def secondary(self): + """Return the player's secondary weapon. + + :rtype: Weapon + """ + return self.get_weapon(is_filters='secondary') + + def get_active_weapon(self): + """Return the player's active weapon. + + :return: + ``None`` if the player does not have an active weapon. + :rtype: Weapon + """ + try: + index = index_from_inthandle(self.active_weapon_handle) + except (ValueError, OverflowError): + return None + + return Weapon(index) + + def set_active_weapon(self, weapon): + """Set the player's active weapon. + + :param Weapon weapon: + The weapon to set as active. + """ + self.active_weapon_handle = weapon.inthandle + + active_weapon = property(get_active_weapon, set_active_weapon) + + def get_weapon(self, classname=None, is_filters=None, not_filters=None): + """Return the first found weapon for the given arguments. + + :rtype: Weapon + """ + # Loop through all weapons for the given arguments + for weapon in self.weapons(classname, is_filters, not_filters): + + # Return the first found weapon + return weapon + + # If no weapon is found, return None + return None + + def weapons(self, classname=None, is_filters=None, not_filters=None): + """Iterate over the player's weapons for the given arguments. + + :return: + A generator of :class:`weapons.entity.Weapon` objects. + :rtype: generator + """ + # Loop through all the players weapons for the given arguments + for index in self.weapon_indexes(classname, is_filters, not_filters): + + # Yield the current weapon + yield Weapon(index) + + def weapon_indexes( + self, classname=None, is_filters=None, not_filters=None): + """Iterate over the player's weapon indexes for the given arguments. + + :return: + A generator of indexes. + :rtype: generator + """ + # Is the weapon array supported for the current game? + if _weapon_prop_length is None: + return + + # Loop through the length of the weapon array + for offset in range(_weapon_prop_length): + + # Get the player's current weapon at this offset + handle = self.get_property_int( + '{base}{offset:03d}'.format( + base=weapon_manager.myweapons, + offset=offset, + ) + ) + + # Try to get the index of the handle + try: + index = index_from_inthandle(handle) + except (ValueError, OverflowError): + continue + + # Get the weapon's classname + weapon_class = edict_from_index(index).classname + + # Was a classname given and the current + # weapon is not of that classname? + if classname is not None and weapon_class != classname: + + # Do not yield this index + continue + + # Import WeaponClassIter to use its functionality + from filters.weapons import WeaponClassIter + + # Was a weapon type given and the + # current weapon is not of that type? + if not (is_filters is None and not_filters is None): + if weapon_class not in map( + lambda value: value.name, + WeaponClassIter(is_filters, not_filters)): + + # Do not yield this index + continue + + # Yield the index + yield index + + def get_projectile_ammo(self, projectile): + """Return the player's ammo value of the given projectile. + + :param str projectile: + The name of the projectile to get the ammo of. + :rtype: int + """ + return self.get_property_int( + '{base}{prop:03d}'.format( + base=weapon_manager.ammoprop, + prop=weapon_manager[projectile].ammoprop, + ) ) - ).Player -elif SP_PACKAGES_PATH.joinpath( - 'players', 'engines', SOURCE_ENGINE, '__init__.py' -).isfile(): + def set_projectile_ammo(self, projectile, value): + """Set the player's ammo value for the given projectile. - # Import the engine-specific 'Player' class - Player = import_module( - 'players.engines.{engine}'.format( - engine=SOURCE_ENGINE, + :param str projectile: + The name of the projectile to set the ammo of. + :param int value: + The value to set the projectile's ammo to. + """ + self.set_property_int( + '{base}{prop:03d}'.format( + base=weapon_manager.ammoprop, + prop=weapon_manager[projectile].ammoprop, + ), + value, ) - ).Player -else: + def projectile_indexes(self, projectile): + """Yield all indexes of the given projectile for the player. - # Import the base 'Player' class - from players._base import Player + :param str projectile: + The name of the projectile to find indexes of. + """ + if projectile in weapon_manager.projectiles: + for entity in EntityIter(projectile): + if entity.owner == self: + yield entity.index + else: + yield from self.weapon_indexes(weapon_manager[projectile].name) + + def restrict_weapons(self, *weapons): + """Restrict the weapon for the player. + + :param str weapons: + A weapon or any number of weapons to add as restricted for the + player. + """ + from weapons.restrictions import weapon_restriction_handler + weapon_restriction_handler.add_player_restrictions(self, *weapons) + + def unrestrict_weapons(self, *weapons): + """Restrict the weapon for the player. + + :param str weapons: + A weapon or any number of weapons to remove as restricted for the + player. + """ + from weapons.restrictions import weapon_restriction_handler + weapon_restriction_handler.remove_player_restrictions(self, *weapons) + + def is_weapon_restricted(self, weapon): + """Return whether the player is restricted from the given weapon. + + :param str weapon: + The name of the weapon to check against restriction. + :rtype: bool + """ + from weapons.restrictions import weapon_restriction_manager + return weapon_restriction_manager.is_player_restricted(self, weapon) + + @wrap_entity_mem_func + def drop_weapon(self, weapon, target=None, velocity=None): + """Drop a weapon. + + :param Pointer weapon: + Weapon to drop. + :param Vector target: + Target location to drop the weapon at. + :param Vector velocity: + Velocity to use to drop the weapon. + """ + return [weapon, target, velocity] + + +# ============================================================================= +# >> HELPER FUNCTIONS +# ============================================================================= +def _find_weapon_prop_length(table): + """Loop through a prop table to find the myweapons property length. + + :rtype: int + """ + # Loop through the props in the table + for item in table: + + # Is this the m_hMyWeapons prop? + if item.name == weapon_manager.myweapons[:~0]: + + # If so, return the length of the prop table + return len(item.data_table) + + # Is the current prop a table? + if item.type == SendPropType.DATATABLE: + + # Loop through the table + _find_weapon_prop_length(item.data_table) + +# Default the weapon prop length to None +_weapon_prop_length = None + +# Is the game supported? +if not isinstance(weapon_manager, NoWeaponManager): + + # Loop through all ServerClass objects + for _current_class in ServerClassGenerator(): + + # Loop through the ServerClass' props + _weapon_prop_length = _find_weapon_prop_length(_current_class.table) + + # Was m_hMyWeapons found? + if _weapon_prop_length is not None: + + # No need to continue looping + break + + +# ============================================================================= +# >> ENGINE/GAME IMPORTS +# ============================================================================= +engine_import() diff --git a/addons/source-python/packages/source-python/players/engines/l4d2/__init__.py b/addons/source-python/packages/source-python/players/l4d2/entity.py similarity index 58% rename from addons/source-python/packages/source-python/players/engines/l4d2/__init__.py rename to addons/source-python/packages/source-python/players/l4d2/entity.py index 08d8de822..06003b368 100644 --- a/addons/source-python/packages/source-python/players/engines/l4d2/__init__.py +++ b/addons/source-python/packages/source-python/players/l4d2/entity.py @@ -1,21 +1,11 @@ -# ../players/engines/l4d2/__init__.py +# ../players/l4d2/entity.py """Provides L4D2 specific Player based functionality.""" -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python Imports -# Entities -from entities.helpers import wrap_entity_mem_func -# Players -from players._base import Player as _Player - - # ============================================================================= # >> CLASSES # ============================================================================= -class Player(_Player): +class Player(Player): """Class used to interact directly with players.""" @wrap_entity_mem_func diff --git a/addons/source-python/packages/source-python/players/engines/orangebox/cstrike.py b/addons/source-python/packages/source-python/players/orangebox/cstrike/entity.py similarity index 90% rename from addons/source-python/packages/source-python/players/engines/orangebox/cstrike.py rename to addons/source-python/packages/source-python/players/orangebox/cstrike/entity.py index 49c2c6e04..c78fe81a0 100644 --- a/addons/source-python/packages/source-python/players/engines/orangebox/cstrike.py +++ b/addons/source-python/packages/source-python/players/orangebox/cstrike/entity.py @@ -1,4 +1,4 @@ -# ../players/engines/orangebox/cstrike.py +# ../players/orangebox/cstrike/entity.py """Provides CS:S specific Player based functionality.""" @@ -6,24 +6,19 @@ # >> IMPORTS # ============================================================================= # Source.Python Imports -# Engines -from engines.server import engine_server # Entities from entities.entity import BaseEntity -# Filters -from filters.entities import EntityIter # Memory from memory import get_virtual_function from memory.hooks import PreHook # Players -from . import Player as _Player from players.constants import LifeState # ============================================================================= # >> CLASSES # ============================================================================= -class Player(_Player): +class Player(Player): """Class used to interact directly with players.""" def has_c4(self): diff --git a/addons/source-python/packages/source-python/players/engines/orangebox/__init__.py b/addons/source-python/packages/source-python/players/orangebox/entity.py similarity index 53% rename from addons/source-python/packages/source-python/players/engines/orangebox/__init__.py rename to addons/source-python/packages/source-python/players/orangebox/entity.py index 886a15523..ec618b45b 100644 --- a/addons/source-python/packages/source-python/players/engines/orangebox/__init__.py +++ b/addons/source-python/packages/source-python/players/orangebox/entity.py @@ -1,21 +1,11 @@ -# ../players/engines/orangebox/__init__.py +# ../players/orangebox/entity.py """Provides Orangebox specific Player based functionality.""" -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python Imports -# Entities -from entities.helpers import wrap_entity_mem_func -# Players -from players._base import Player as _Player - - # ============================================================================= # >> CLASSES # ============================================================================= -class Player(_Player): +class Player(Player): """Class used to interact directly with players.""" @wrap_entity_mem_func diff --git a/addons/source-python/packages/source-python/weapons/_base.py b/addons/source-python/packages/source-python/weapons/_base.py deleted file mode 100644 index 441a8924b..000000000 --- a/addons/source-python/packages/source-python/weapons/_base.py +++ /dev/null @@ -1,171 +0,0 @@ -# ../weapons/_base.py - -"""Provides simplified weapon functionality.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python Imports -# Entities -from entities.entity import Entity -# Weapons -from _weapons._entity import WeaponMixin -from weapons.manager import weapon_manager - - -# ============================================================================= -# >> GLOBAL VARIABLES -# ============================================================================= -# Add all the global variables to __all__ -__all__ = ('Weapon', - ) - - -# ============================================================================= -# >> CLASSES -# ============================================================================= -class Weapon(Entity, WeaponMixin): - """Allows easy usage of the weapon's attributes.""" - - def _validate_clip(self): - """Test if the weapon has a clip.""" - if ( - self.classname in weapon_manager and - weapon_manager[self.classname].clip is None - ) or self._clip == -1: - raise ValueError('Weapon does not have a clip.') - - def get_clip(self): - """Return the amount of ammo in the weapon's clip.""" - self._validate_clip() - return self._clip - - def set_clip(self, value): - """Set the amount of ammo in the weapon's clip.""" - self._validate_clip() - self._clip = value - - # Set the "clip" property methods - clip = property( - get_clip, set_clip, - doc="""Property to get/set the weapon's clip.""") - - def _validate_ammo(self): - """Test if the weapon has a valid ammoprop and an owner.""" - if ( - self.classname in weapon_manager and - weapon_manager[self.classname].ammoprop is None - ) or self.ammoprop == -1: - raise ValueError( - 'Unable to get ammoprop for {weapon}'.format( - weapon=self.classname - ) - ) - - player = self.owner - if player is None: - raise ValueError('Unable to get the owner of the weapon.') - - return player - - def get_ammo(self): - """Return the amount of ammo the player has for the weapon.""" - player = self._validate_ammo() - return player.get_property_int( - '{base}{prop:03d}'.format( - base=weapon_manager.ammoprop, - prop=self.ammoprop, - ) - ) - - def set_ammo(self, value): - """Set the player's ammo property for the weapon.""" - player = self._validate_ammo() - player.set_property_int( - '{base}{prop:03d}'.format( - base=weapon_manager.ammoprop, - prop=self.ammoprop, - ), - value, - ) - - # Set the "ammo" property methods - ammo = property( - get_ammo, set_ammo, - doc="""Property to get/set the weapon's ammo.""") - - def _validate_secondary_fire_clip(self): - """Test if the weapon has a secondary fire clip.""" - if self._secondary_fire_clip == -1: - raise ValueError('Weapon does not have a secondary fire clip.') - - def get_secondary_fire_clip(self): - """Return the amount of ammo in the weapon's secondary fire clip.""" - self._validate_secondary_fire_clip() - return self._secondary_fire_clip - - def set_secondary_fire_clip(self, value): - """Set the amount of ammo in the weapon's secondary fire clip.""" - self._validate_secondary_fire_clip() - self._secondary_fire_clip = value - - # Set the "secondary_fire_clip" property methods - secondary_fire_clip = property( - get_secondary_fire_clip, set_secondary_fire_clip, - doc="""Property to get/set the weapon's secondary fire clip.""") - - def _validate_secondary_fire_ammo(self): - """Test if the weapon has a valid secondary fire ammoprop and an owner.""" - if self.secondary_fire_ammoprop == -1: - raise ValueError( - 'Unable to get secondary fire ammoprop for {0}'.format( - self.classname)) - - player = self.owner - if player is None: - raise ValueError('Unable to get the owner of the weapon.') - - return player - - def get_secondary_fire_ammo(self): - """Return the secondary fire ammo the player has for the weapon.""" - player = self._validate_secondary_fire_ammo() - return player.get_property_int( - '{base}{prop:03d}'.format( - base=weapon_manager.ammoprop, - prop=self.secondary_fire_ammoprop, - ) - ) - - def set_secondary_fire_ammo(self, value): - """Set the player's secondary fire ammo property for the weapon.""" - player = self._validate_secondary_fire_ammo() - player.set_property_int( - '{base}{prop:03d}'.format( - base=weapon_manager.ammoprop, - prop=self.secondary_fire_ammoprop, - ), - value, - ) - - # Set the "secondary_fire_ammo" property methods - secondary_fire_ammo = property( - get_secondary_fire_ammo, set_secondary_fire_ammo, - doc="""Property to get/set the weapon's secondary fire ammo.""") - - @property - def weapon_name(self): - """Return the full class name of the weapon.""" - return self.classname - - def remove(self): - """Remove the weapon.""" - # Avoid a cyclic import - from players.entity import Player - - owner = self.owner - if owner is not None and owner.is_player(): - player = Player(owner.index) - player.drop_weapon(self) - - super().remove() diff --git a/addons/source-python/packages/source-python/weapons/engines/csgo/csgo.py b/addons/source-python/packages/source-python/weapons/csgo/entity.py similarity index 86% rename from addons/source-python/packages/source-python/weapons/engines/csgo/csgo.py rename to addons/source-python/packages/source-python/weapons/csgo/entity.py index 946157c1a..1db841849 100644 --- a/addons/source-python/packages/source-python/weapons/engines/csgo/csgo.py +++ b/addons/source-python/packages/source-python/weapons/csgo/entity.py @@ -1,15 +1,7 @@ -# ../weapons/engines/csgo/csgo.py +# ../weapons/csgo/entity.py """Provides CS:GO game specific weapon functionality.""" -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python -from . import Weapon as _Weapon -from weapons.manager import weapon_manager - - # ============================================================================= # >> GLOBAL VARIABLES # ============================================================================= @@ -23,7 +15,7 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class Weapon(_Weapon): +class Weapon(Weapon): """Allows easy usage of the weapon's attributes.""" def get_ammo(self): diff --git a/addons/source-python/packages/source-python/weapons/engines/__init__.py b/addons/source-python/packages/source-python/weapons/engines/__init__.py deleted file mode 100644 index 5dea91ccc..000000000 --- a/addons/source-python/packages/source-python/weapons/engines/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# ../weapons/engines/__init__.py - -"""Provides engine specific Weapon based functionality.""" diff --git a/addons/source-python/packages/source-python/weapons/engines/csgo/__init__.py b/addons/source-python/packages/source-python/weapons/engines/csgo/__init__.py deleted file mode 100644 index a20906621..000000000 --- a/addons/source-python/packages/source-python/weapons/engines/csgo/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# ../weapons/engines/csgo/__init__.py - -"""Provides CS:GO engine specific weapon functionality.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python -from weapons._base import Weapon as _Weapon - - -# ============================================================================= -# >> CLASSES -# ============================================================================= -class Weapon(_Weapon): - """Allows easy usage of the weapon's attributes.""" diff --git a/addons/source-python/packages/source-python/weapons/entity.py b/addons/source-python/packages/source-python/weapons/entity.py index a7069a791..f8b4ca746 100644 --- a/addons/source-python/packages/source-python/weapons/entity.py +++ b/addons/source-python/packages/source-python/weapons/entity.py @@ -5,10 +5,14 @@ # ============================================================================= # >> IMPORTS # ============================================================================= -from importlib import import_module -from core import GAME_NAME -from core import SOURCE_ENGINE -from paths import SP_PACKAGES_PATH +# Source.Python Imports +# Core +from core import engine_import +# Entities +from entities.entity import Entity +# Weapons +from _weapons._entity import WeaponMixin +from weapons.manager import weapon_manager # ============================================================================= @@ -19,32 +23,156 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# >> CLASSES # ============================================================================= -if SP_PACKAGES_PATH.joinpath( - 'weapons', 'engines', SOURCE_ENGINE, GAME_NAME + '.py' -).isfile(): +class Weapon(Entity, WeaponMixin): + """Allows easy usage of the weapon's attributes.""" - # Import the game-specific 'Weapon' class - Weapon = import_module( - 'weapons.engines.{engine}.{game}'.format( - engine=SOURCE_ENGINE, - game=GAME_NAME, + def _validate_clip(self): + """Test if the weapon has a clip.""" + if ( + self.classname in weapon_manager and + weapon_manager[self.classname].clip is None + ) or self._clip == -1: + raise ValueError('Weapon does not have a clip.') + + def get_clip(self): + """Return the amount of ammo in the weapon's clip.""" + self._validate_clip() + return self._clip + + def set_clip(self, value): + """Set the amount of ammo in the weapon's clip.""" + self._validate_clip() + self._clip = value + + # Set the "clip" property methods + clip = property( + get_clip, set_clip, + doc="""Property to get/set the weapon's clip.""") + + def _validate_ammo(self): + """Test if the weapon has a valid ammoprop and an owner.""" + if ( + self.classname in weapon_manager and + weapon_manager[self.classname].ammoprop is None + ) or self.ammoprop == -1: + raise ValueError( + 'Unable to get ammoprop for {weapon}'.format( + weapon=self.classname + ) + ) + + player = self.owner + if player is None: + raise ValueError('Unable to get the owner of the weapon.') + + return player + + def get_ammo(self): + """Return the amount of ammo the player has for the weapon.""" + player = self._validate_ammo() + return player.get_property_int( + '{base}{prop:03d}'.format( + base=weapon_manager.ammoprop, + prop=self.ammoprop, + ) ) - ).Weapon -elif SP_PACKAGES_PATH.joinpath( - 'weapons', 'engines', SOURCE_ENGINE, '__init__.py' -).isfile(): + def set_ammo(self, value): + """Set the player's ammo property for the weapon.""" + player = self._validate_ammo() + player.set_property_int( + '{base}{prop:03d}'.format( + base=weapon_manager.ammoprop, + prop=self.ammoprop, + ), + value, + ) + + # Set the "ammo" property methods + ammo = property( + get_ammo, set_ammo, + doc="""Property to get/set the weapon's ammo.""") + + def _validate_secondary_fire_clip(self): + """Test if the weapon has a secondary fire clip.""" + if self._secondary_fire_clip == -1: + raise ValueError('Weapon does not have a secondary fire clip.') + + def get_secondary_fire_clip(self): + """Return the amount of ammo in the weapon's secondary fire clip.""" + self._validate_secondary_fire_clip() + return self._secondary_fire_clip + + def set_secondary_fire_clip(self, value): + """Set the amount of ammo in the weapon's secondary fire clip.""" + self._validate_secondary_fire_clip() + self._secondary_fire_clip = value + + # Set the "secondary_fire_clip" property methods + secondary_fire_clip = property( + get_secondary_fire_clip, set_secondary_fire_clip, + doc="""Property to get/set the weapon's secondary fire clip.""") - # Import the engine-specific 'Weapon' class - Weapon = import_module( - 'weapons.engines.{engine}'.format( - engine=SOURCE_ENGINE, + def _validate_secondary_fire_ammo(self): + """Test if the weapon has a valid secondary fire ammoprop and an owner.""" + if self.secondary_fire_ammoprop == -1: + raise ValueError( + 'Unable to get secondary fire ammoprop for {0}'.format( + self.classname)) + + player = self.owner + if player is None: + raise ValueError('Unable to get the owner of the weapon.') + + return player + + def get_secondary_fire_ammo(self): + """Return the secondary fire ammo the player has for the weapon.""" + player = self._validate_secondary_fire_ammo() + return player.get_property_int( + '{base}{prop:03d}'.format( + base=weapon_manager.ammoprop, + prop=self.secondary_fire_ammoprop, + ) ) - ).Weapon -else: + def set_secondary_fire_ammo(self, value): + """Set the player's secondary fire ammo property for the weapon.""" + player = self._validate_secondary_fire_ammo() + player.set_property_int( + '{base}{prop:03d}'.format( + base=weapon_manager.ammoprop, + prop=self.secondary_fire_ammoprop, + ), + value, + ) + + # Set the "secondary_fire_ammo" property methods + secondary_fire_ammo = property( + get_secondary_fire_ammo, set_secondary_fire_ammo, + doc="""Property to get/set the weapon's secondary fire ammo.""") + + @property + def weapon_name(self): + """Return the full class name of the weapon.""" + return self.classname - # Import the base 'Weapon' class - from weapons._base import Weapon + def remove(self): + """Remove the weapon.""" + # Avoid a cyclic import + from players.entity import Player + + owner = self.owner + if owner is not None and owner.is_player(): + player = Player(owner.index) + player.drop_weapon(self) + + super().remove() + + +# ============================================================================= +# >> ENGINE/GAME IMPORTS +# ============================================================================= +engine_import() From 3096e598fb0bf4fc26aabb0e2d58bf8391b2e8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Sat, 1 Jun 2019 08:40:56 -0400 Subject: [PATCH 02/20] Added "ENGINE_IMPORT_SKIPPABLES" containing names to always skip when loading engine/game specific files. Added "skippables" as an optional parameter to "engine_import" to allow skipping names explicitly. Support "." format. Added "skip_privates" as an optional parameter to "engine_import" to allow skipping private names (e.g internal callbacks, etc.). Caller's docstring is now only overwritten if it was originally undocumented. Engine/game's "__all__" attribute is now merged with the caller's instead of being overwritten. --- .../packages/source-python/core/__init__.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 6a7d9897e..6c2ef9e7a 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -69,6 +69,7 @@ 'WeakAutoUnload', 'GAME_NAME', 'OutputReturn', + 'ENGINE_IMPORT_SKIPPABLES', 'PLATFORM', 'SOURCE_ENGINE', 'SOURCE_ENGINE_BRANCH', @@ -98,6 +99,16 @@ # Get the sp.core logger core_logger = _sp_logger.core +# Names to always skip when loading engine/game specific files. +ENGINE_IMPORT_SKIPPABLES = ( + '__builtins__', + '__cached__', + '__file__', + '__loader__', + '__package__', + '__spec__' +) + # ============================================================================= # >> CLASSES @@ -341,9 +352,13 @@ def check_info_output(output): return create_checksum(''.join(lines)) != checksum -def engine_import(): +def engine_import(skippables=(), skip_privates=True): """Import engine/game specific objects. + :param tuple skippables: + Names to skip when loading engine/game specific files. + :param bool skip_privates: + Whether or not private names should be skipped. :raise ImportError: If it was not called from global scope. """ @@ -351,6 +366,7 @@ def engine_import(): if f.f_locals is not f.f_globals: raise ImportError( '"engine_import" must only be called from global scopes.') + skippables = ENGINE_IMPORT_SKIPPABLES + skippables caller = getmodule(f) directory, name = Path(caller.__file__).splitpath() for subfolder in (SOURCE_ENGINE, GAME_NAME): @@ -361,11 +377,23 @@ def engine_import(): if not path.isfile(): continue for attr, obj in run_path(path, f.f_globals, caller.__name__).items(): - if isclass(obj): + if attr in skippables: + continue + if (attr == '__doc__' and + getattr(caller, '__doc__', None) is not None): + continue + if attr == '__all__': + if hasattr(caller, '__all__'): + obj = tuple(sorted(set(obj + getattr(caller, '__all__')))) + elif skip_privates and attr.startswith('_'): + continue + elif isclass(obj): base = obj.__base__ if (obj.__name__ == base.__name__ and base.__module__ == caller.__name__): for k, v in obj.__dict__.items(): + if f'{attr}.{k}' in skippables: + continue if (k == '__doc__' and getattr(base, '__doc__', None) is not None): continue From 739870a86c15e3d34c688cea76c0a4620b3b10b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Tue, 11 Jun 2019 21:02:07 -0400 Subject: [PATCH 03/20] Added an example for engine_import. --- .../packages/source-python/core/__init__.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 6c2ef9e7a..bc3500c94 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -361,6 +361,57 @@ def engine_import(skippables=(), skip_privates=True): Whether or not private names should be skipped. :raise ImportError: If it was not called from global scope. + + For example you have the following files: + + ``../packages/some_module.py`` + .. code:: python + + class SomeClass(object): + def some_method(self): + raise NotImplementedError('Not implemented on this game.') + + engine_import() + + ``../packages/orangebox/some_module.py`` + .. code:: python + + from some_module import SomeClass + + class SomeClass(SomeClass): + def some_method(self): + return 'OrangeBox' + + ``../packages/orangebox/cstrike/some_module.py`` + + .. code:: python + + from some_module import SomeClass + + class SomeClass(SomeClass): + def some_method(self): + return 'Counter-Strike: Source' + + ``../plugins/plugin/plugin.py`` + .. code:: python + + from some_module import SomeClass + + print(SomeClass().some_method()) + + Loading the `plugin` plugin would prints the following for + ``Counter-Strike: Source``: + .. code:: python + + Counter-Strike: Source + + The following for every other ``OrangeBox`` games: + .. code:: python + + OrangeBox + + And would raise a ``NotImplementedError`` error by default + for any other games. """ f = currentframe().f_back if f.f_locals is not f.f_globals: From ac114bc7339bde662172358fce76c867ec59ad31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Tue, 11 Jun 2019 21:05:12 -0400 Subject: [PATCH 04/20] NoWeaponManager will now raise a NotImplementedError into __getattr__, __getitem__ and __setitem__ rather than __getattribute__. --- .../packages/source-python/weapons/default.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/addons/source-python/packages/source-python/weapons/default.py b/addons/source-python/packages/source-python/weapons/default.py index 683cbbe75..1ac766813 100644 --- a/addons/source-python/packages/source-python/weapons/default.py +++ b/addons/source-python/packages/source-python/weapons/default.py @@ -23,7 +23,7 @@ class NoWeaponManager(dict): """Default class to use if no game specific weapon ini file is found.""" - def __getattribute__(self, attr): + def __getattr__(self, attr): """Raise an error when trying to get an attribute.""" raise NotImplementedError( 'No support for game "{0}"'.format(GAME_NAME)) @@ -32,3 +32,13 @@ def __setattr__(self, attr, value): """Raise an error when trying to set an attribute.""" raise NotImplementedError( 'No support for game "{0}"'.format(GAME_NAME)) + + def __getitem__(self, item): + """Raise an error when trying to get an attribute.""" + raise NotImplementedError( + 'No support for game "{0}"'.format(GAME_NAME)) + + def __setitem__(self, item, value): + """Raise an error when trying to set an attribute.""" + raise NotImplementedError( + 'No support for game "{0}"'.format(GAME_NAME)) From 28ae74dfb412b4305ad2d64eaf1049454da3975a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Tue, 11 Jun 2019 21:58:30 -0400 Subject: [PATCH 05/20] Added missing declaration for the _weapons module. --- src/CMakeLists.txt | 1 + src/core/modules/weapons/weapons_wrap.cpp | 38 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/core/modules/weapons/weapons_wrap.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a58d6d205..0ca545b5f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -460,6 +460,7 @@ Set(SOURCEPYTHON_WEAPONS_MODULE_SOURCES core/modules/weapons/weapons_scripts_wrap.cpp core/modules/weapons/weapons_entity.cpp core/modules/weapons/weapons_entity_wrap.cpp + core/modules/weapons/weapons_wrap.cpp ) # ------------------------------------------------------------------ diff --git a/src/core/modules/weapons/weapons_wrap.cpp b/src/core/modules/weapons/weapons_wrap.cpp new file mode 100644 index 000000000..db4ca9ee5 --- /dev/null +++ b/src/core/modules/weapons/weapons_wrap.cpp @@ -0,0 +1,38 @@ +/** +* ============================================================================= +* 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" + + +//----------------------------------------------------------------------------- +// Declare the _weapons module. +//----------------------------------------------------------------------------- +DECLARE_SP_MODULE(_weapons) +{ +} From 21d09acd88b0d548544e9e0cdd24b5f9af761446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Tue, 11 Jun 2019 22:01:27 -0400 Subject: [PATCH 06/20] =?UTF-8?q?=20=E2=86=92=20<4=20spaces>.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0ca545b5f..8664a31e6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -460,7 +460,7 @@ Set(SOURCEPYTHON_WEAPONS_MODULE_SOURCES core/modules/weapons/weapons_scripts_wrap.cpp core/modules/weapons/weapons_entity.cpp core/modules/weapons/weapons_entity_wrap.cpp - core/modules/weapons/weapons_wrap.cpp + core/modules/weapons/weapons_wrap.cpp ) # ------------------------------------------------------------------ From b5d35667b4537b4aa237ac289118643ae1e134ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Tue, 11 Jun 2019 22:21:12 -0400 Subject: [PATCH 07/20] The imported files are inheriting the caller's scope so importing the class in the example of engine_import was redundant. --- addons/source-python/packages/source-python/core/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index eaa729e7f..f7f3e6324 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -380,8 +380,6 @@ def some_method(self): ``../packages/orangebox/some_module.py`` .. code:: python - from some_module import SomeClass - class SomeClass(SomeClass): def some_method(self): return 'OrangeBox' @@ -390,8 +388,6 @@ def some_method(self): .. code:: python - from some_module import SomeClass - class SomeClass(SomeClass): def some_method(self): return 'Counter-Strike: Source' From e95924ed25089046a0cd8eb409dde5d06959e694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Tue, 11 Jun 2019 22:36:41 -0400 Subject: [PATCH 08/20] Improved documentation of engine_import by adding information about get_wrapped used to emulate hierarchical calls. --- .../packages/source-python/core/__init__.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index f7f3e6324..a43770f77 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -412,6 +412,28 @@ def some_method(self): And would raise a ``NotImplementedError`` error by default for any other games. + + It is important to keep in mind that the extension classes are not added + to the base class hierarchy. Every attributes are dynamically injected + into the original class meaning that you cannot use tools such as + ``super``. To call the wrapped method you need to use the ``get_wrapped`` + function. Based on the example given above, if you had the following file: + + ``../packages/orangebox/cstrike/some_module.py`` + + .. code:: python + + from core import get_wrapped + + class SomeClass(SomeClass): + def some_method(self): + engine = get_wrapped(SomeClass.some_method)(self) + return f'Counter-Strike: Source is an {engine} game.' + + This would prints the following on ``Counter-Strike: Source``: + .. code:: python + + Counter-Strike: Source is an OrangeBox game. """ f = currentframe().f_back if f.f_locals is not f.f_globals: From d311073d8059dac6aa2353971fe7f8912589b70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Wed, 12 Jun 2019 02:30:39 -0400 Subject: [PATCH 09/20] Fixed wrappers for method descriptors. Fixed some Entity/Player's extension methods for CS:GO. --- .../packages/source-python/core/__init__.py | 16 ++++++++-------- .../source-python/entities/csgo/entity.py | 8 +++++--- .../source-python/players/csgo/entity.py | 6 +++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index a43770f77..3eb45aa76 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -20,7 +20,7 @@ from inspect import currentframe from inspect import getmodule from inspect import isclass -from inspect import isfunction +from inspect import isroutine # OS from os import sep # Path @@ -470,18 +470,18 @@ def some_method(self): if (k == '__doc__' and getattr(base, '__doc__', None) is not None): continue - if isfunction(v) and hasattr(base, k): + if isroutine(v) and hasattr(base, k): func = getattr(base, k) - if isfunction(func): - update_wrapper(v, func) + if isroutine(func): + update_wrapper(getattr(v, '__func__', v), func) setattr(base, k, v) continue if hasattr(caller, attr): o = getattr(caller, attr) if o is obj: continue - elif isfunction(o): - update_wrapper(obj, o) + elif isroutine(o): + update_wrapper(getattr(obj, '__func__', obj), o) setattr(caller, attr, obj) def get_wrapped(func): @@ -495,6 +495,6 @@ def get_wrapped(func): The wrapped function or ``None`` if the given wrapper is not wrapping any function. """ - if not isfunction(func): + if not isroutine(func): raise TypeError(f'"{func}" is not a function.') - return getattr(func, '__wrapped__', None) + return getattr(getattr(func, '__func__', func), '__wrapped__', None) diff --git a/addons/source-python/packages/source-python/entities/csgo/entity.py b/addons/source-python/packages/source-python/entities/csgo/entity.py index ec54caefd..99ab15bad 100644 --- a/addons/source-python/packages/source-python/entities/csgo/entity.py +++ b/addons/source-python/packages/source-python/entities/csgo/entity.py @@ -6,6 +6,8 @@ # >> IMPORTS # ============================================================================= # Source.Python +# Core +from core import get_wrapped # Weapons from weapons.manager import weapon_manager @@ -37,10 +39,10 @@ def create(cls, classname): index = _weapon_names_for_definition.get(classname) if index is not None: parent_class = _weapon_parents.get(classname) - entity = super().create(parent_class or classname) + entity = get_wrapped(Entity.create)(parent_class or classname) entity.item_definition_index = index else: - entity = super().create(classname) + entity = get_wrapped(Entity.create)(classname) return entity @classmethod @@ -63,4 +65,4 @@ def find(cls, classname): 'm_AttributeManager.m_Item.m_iItemDefinitionIndex' ) in (index, 0): return cls(entity.index) - return super().find(classname) + return get_wrapped(Entity.find)(classname) diff --git a/addons/source-python/packages/source-python/players/csgo/entity.py b/addons/source-python/packages/source-python/players/csgo/entity.py index e397b1986..9d6fdae42 100644 --- a/addons/source-python/packages/source-python/players/csgo/entity.py +++ b/addons/source-python/packages/source-python/players/csgo/entity.py @@ -32,7 +32,7 @@ class Player(Player): def _get_kills(self): """Return the number of kills the player has.""" - return super().kills + return self.__getattr__('kills') def _set_kills(self, value): """Set the number of kills the player has.""" @@ -43,7 +43,7 @@ def _set_kills(self, value): def _get_deaths(self): """Return the number of deaths the player has.""" - return super().deaths + return self.__getattr__('deaths') def _set_deaths(self, value): """Set the number of deaths the player has.""" @@ -54,7 +54,7 @@ def _set_deaths(self, value): def _get_assists(self): """Return the number of assists the player has.""" - return super().assists + return self.__getattr__('assists') def _set_assists(self, value): """Set the number of assists the player has.""" From c3e92d4e454238bd7402e7f08af26f39b6a00427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Wed, 12 Jun 2019 04:52:10 -0400 Subject: [PATCH 10/20] Added an optional self parameter to get_wrapped in order to properly bind the wrapped method before returning it. --- .../packages/source-python/core/__init__.py | 11 +++++++++-- .../packages/source-python/entities/csgo/entity.py | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 3eb45aa76..acc324437 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -31,6 +31,8 @@ from runpy import run_path # Sys import sys +# Types +from types import MethodType # Urllib from urllib.request import urlopen # Weakref @@ -484,11 +486,13 @@ def some_method(self): update_wrapper(getattr(obj, '__func__', obj), o) setattr(caller, attr, obj) -def get_wrapped(func): +def get_wrapped(func, self=None): """Returns the wrapped function of a wrapper function. :param function func: The wrapper function to get the wrapped function from. + :param object self: + Object or class to bind the wrapped method to. :raise TypeError: If the given wrapper is not a function. :return: @@ -497,4 +501,7 @@ def get_wrapped(func): """ if not isroutine(func): raise TypeError(f'"{func}" is not a function.') - return getattr(getattr(func, '__func__', func), '__wrapped__', None) + wrapped = getattr(getattr(func, '__func__', func), '__wrapped__', None) + if isinstance(wrapped, MethodType) and self is not None: + wrapped = MethodType(getattr(wrapped, '__func__', wrapped), self) + return wrapped diff --git a/addons/source-python/packages/source-python/entities/csgo/entity.py b/addons/source-python/packages/source-python/entities/csgo/entity.py index 99ab15bad..efc17dda4 100644 --- a/addons/source-python/packages/source-python/entities/csgo/entity.py +++ b/addons/source-python/packages/source-python/entities/csgo/entity.py @@ -39,10 +39,10 @@ def create(cls, classname): index = _weapon_names_for_definition.get(classname) if index is not None: parent_class = _weapon_parents.get(classname) - entity = get_wrapped(Entity.create)(parent_class or classname) + entity = get_wrapped(Entity.create, cls)(parent_class or classname) entity.item_definition_index = index else: - entity = get_wrapped(Entity.create)(classname) + entity = get_wrapped(Entity.create, cls)(classname) return entity @classmethod @@ -65,4 +65,4 @@ def find(cls, classname): 'm_AttributeManager.m_Item.m_iItemDefinitionIndex' ) in (index, 0): return cls(entity.index) - return get_wrapped(Entity.find)(classname) + return get_wrapped(Entity.find, cls)(classname) From 985109d50be33b48daaf122109896698460dcad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Wed, 12 Jun 2019 22:16:27 -0400 Subject: [PATCH 11/20] Fixed a TypeError when the __all__ attribute is not a tuple and made sure that the original container type is retained after the merge has been performed. --- addons/source-python/packages/source-python/core/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index acc324437..68357aadc 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -459,7 +459,9 @@ def some_method(self): continue if attr == '__all__': if hasattr(caller, '__all__'): - obj = tuple(sorted(set(obj + getattr(caller, '__all__')))) + a = getattr(caller, '__all__') + t = type(a) + obj = t(sorted(set(t(obj) + a))) elif skip_privates and attr.startswith('_'): continue elif isclass(obj): From f6cc81f4aa5b27b25c83d2aec45b04186feb502a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Wed, 12 Jun 2019 22:43:02 -0400 Subject: [PATCH 12/20] Removed the players._language module. --- .../players/_language/__init__.py | 15 ----------- .../source-python/players/_language/base.py | 26 ------------------- .../{_language/cache.py => csgo/helpers.py} | 2 +- .../packages/source-python/players/helpers.py | 25 +++++++++++++++++- 4 files changed, 25 insertions(+), 43 deletions(-) delete mode 100644 addons/source-python/packages/source-python/players/_language/__init__.py delete mode 100644 addons/source-python/packages/source-python/players/_language/base.py rename addons/source-python/packages/source-python/players/{_language/cache.py => csgo/helpers.py} (98%) diff --git a/addons/source-python/packages/source-python/players/_language/__init__.py b/addons/source-python/packages/source-python/players/_language/__init__.py deleted file mode 100644 index 4b0e02346..000000000 --- a/addons/source-python/packages/source-python/players/_language/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# ../players/_language/__init__.py - -"""Provides a function to get a player's language.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python Imports -# Core -from core import GAME_NAME -# Players -if GAME_NAME in ('csgo', ): - from .cache import get_client_language -else: - from .base import get_client_language diff --git a/addons/source-python/packages/source-python/players/_language/base.py b/addons/source-python/packages/source-python/players/_language/base.py deleted file mode 100644 index 701af488a..000000000 --- a/addons/source-python/packages/source-python/players/_language/base.py +++ /dev/null @@ -1,26 +0,0 @@ -# ../players/_language/base.py - -"""Provides a base way to get a player's language.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python Imports -# Engines -from engines.server import engine_server - - -# ============================================================================= -# >> FUNCTIONS -# ============================================================================= -def get_client_language(index): - """Return the language of the given client. - - :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: - return '' - - return engine_server.get_client_convar_value(index, 'cl_language') diff --git a/addons/source-python/packages/source-python/players/_language/cache.py b/addons/source-python/packages/source-python/players/csgo/helpers.py similarity index 98% rename from addons/source-python/packages/source-python/players/_language/cache.py rename to addons/source-python/packages/source-python/players/csgo/helpers.py index b80ccf3c6..82dbec387 100644 --- a/addons/source-python/packages/source-python/players/_language/cache.py +++ b/addons/source-python/packages/source-python/players/csgo/helpers.py @@ -1,4 +1,4 @@ -# ../players/_language/cache.py +# ../players/csgo/helpers.py """Provides a way to get a player's language using a cache.""" diff --git a/addons/source-python/packages/source-python/players/helpers.py b/addons/source-python/packages/source-python/players/helpers.py index 4700fea57..90e73b3c2 100644 --- a/addons/source-python/packages/source-python/players/helpers.py +++ b/addons/source-python/packages/source-python/players/helpers.py @@ -6,11 +6,12 @@ # >> IMPORTS # ============================================================================= # Source.Python Imports +# Core +from core import engine_import # Engines from engines.server import engine_server # Players from players import PlayerGenerator -from players._language import get_client_language # ============================================================================= @@ -89,3 +90,25 @@ 'userid_from_playerinfo', 'userid_from_pointer', ) + + +# ============================================================================= +# >> FUNCTIONS +# ============================================================================= +def get_client_language(index): + """Return the language of the given client. + + :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: + return '' + + return engine_server.get_client_convar_value(index, 'cl_language') + + +# ============================================================================= +# >> ENGINE/GAME IMPORTS +# ============================================================================= +engine_import() From d8ff646342e44c3b25dfa7513e1fe8aff5cb697c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Wed, 12 Jun 2019 22:45:07 -0400 Subject: [PATCH 13/20] Removed a redundant import. --- addons/source-python/packages/source-python/players/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/source-python/packages/source-python/players/helpers.py b/addons/source-python/packages/source-python/players/helpers.py index 90e73b3c2..4ae088ce7 100644 --- a/addons/source-python/packages/source-python/players/helpers.py +++ b/addons/source-python/packages/source-python/players/helpers.py @@ -100,7 +100,6 @@ 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: return '' From 9be41d656517a99d0cdd925d70cbb198483785bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Thu, 13 Jun 2019 19:25:43 -0400 Subject: [PATCH 14/20] Added default fallback for Player.give_named_item and Player.has_c4. Fixed docstrings being overwritten for injected properties. --- .../packages/source-python/core/__init__.py | 17 +++++-- .../packages/source-python/players/entity.py | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 68357aadc..847afd89a 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -12,6 +12,7 @@ from collections import defaultdict # Contextlib from contextlib import contextmanager +from contextlib import suppress # Functools from functools import update_wrapper # Hashlib @@ -474,10 +475,18 @@ def some_method(self): if (k == '__doc__' and getattr(base, '__doc__', None) is not None): continue - if isroutine(v) and hasattr(base, k): - func = getattr(base, k) - if isroutine(func): - update_wrapper(getattr(v, '__func__', v), func) + if hasattr(base, k): + if isroutine(v): + func = getattr(base, k) + if isroutine(func): + update_wrapper( + getattr(v, '__func__', v), func) + elif hasattr(v, '__doc__'): + doc = getattr( + getattr(base, k), '__doc__', None) + if doc is not None: + with suppress(AttributeError): + setattr(v, '__doc__', doc) setattr(base, k, v) continue if hasattr(caller, attr): diff --git a/addons/source-python/packages/source-python/players/entity.py b/addons/source-python/packages/source-python/players/entity.py index 238613655..dbaf0b6d2 100644 --- a/addons/source-python/packages/source-python/players/entity.py +++ b/addons/source-python/packages/source-python/players/entity.py @@ -972,6 +972,54 @@ def drop_weapon(self, weapon, target=None, velocity=None): """ return [weapon, target, velocity] + def give_named_item(self, item, *args, **kwargs): + """Give the player a named item. + + :param str item: + The name of the item to give to the player. + :param tuple *args: + Various arguments specific to the current game. + :param dict **kwargs: + Various keyword arguments specific to the current game. + :return: + The pointer of the given item. + :rtype: Pointer + + :raise NotImplementedError: + If this method is not available for the current game. + + .. note:: + + This method is only available for the following games: + + * Black Mesa: Source + * Counter-Strike: Global Offensive + * Couter-Strike: Source + * Day of Defeat: Source + * Left 4 Dead 2 + * Team Fortress 2 + """ + raise NotImplementedError( + 'No support for game "{0}"'.format(GAME_NAME)) + + def has_c4(self): + """Return whether or not the player is carrying C4. + + :rtype: bool + + :raise NotImplementedError: + If this method is not available for the current game. + + .. note:: + + This method is only available for the following games: + + * Counter-Strike: Global Offensive + * Couter-Strike: Source + """ + raise NotImplementedError( + 'No support for game "{0}"'.format(GAME_NAME)) + # ============================================================================= # >> HELPER FUNCTIONS From be58c5b93b0869f736ee409d9c53c7356320a8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Thu, 13 Jun 2019 21:55:08 -0400 Subject: [PATCH 15/20] Added missing documentation for PlayerMixin. --- .../source/developing/modules/players.entity.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.entity.rst b/addons/source-python/docs/source-python/source/developing/modules/players.entity.rst index bf3bf4342..3f215965c 100644 --- a/addons/source-python/docs/source-python/source/developing/modules/players.entity.rst +++ b/addons/source-python/docs/source-python/source/developing/modules/players.entity.rst @@ -5,3 +5,8 @@ players.entity module :members: :undoc-members: :show-inheritance: + +.. autoclass:: _players.PlayerMixin + :members: + :undoc-members: + :show-inheritance: From 179f62b8ddafbe917ebe234fc798828685ccb772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Thu, 13 Jun 2019 22:42:11 -0400 Subject: [PATCH 16/20] Fixed a TypeError when skippables is not a tuple. --- addons/source-python/packages/source-python/core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 847afd89a..5a705b95c 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -442,7 +442,7 @@ def some_method(self): if f.f_locals is not f.f_globals: raise ImportError( '"engine_import" must only be called from global scopes.') - skippables = ENGINE_IMPORT_SKIPPABLES + skippables + skippables = ENGINE_IMPORT_SKIPPABLES + tuple(skippables) caller = getmodule(f) directory, name = Path(caller.__file__).splitpath() for subfolder in (SOURCE_ENGINE, GAME_NAME): From 74765adc59cb29e58e00225cb8da3c3cbcf9a52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Thu, 13 Jun 2019 23:06:55 -0400 Subject: [PATCH 17/20] Fixed private class members not being skipped when skip_privates is enabled. --- addons/source-python/packages/source-python/core/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 5a705b95c..4e983b6b7 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -470,7 +470,8 @@ def some_method(self): if (obj.__name__ == base.__name__ and base.__module__ == caller.__name__): for k, v in obj.__dict__.items(): - if f'{attr}.{k}' in skippables: + if (f'{attr}.{k}' in skippables or + (skip_privates and k.startswith('_'))): continue if (k == '__doc__' and getattr(base, '__doc__', None) is not None): From d231c8a986ad652434306144a8dccd04c2fc0558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Thu, 13 Jun 2019 23:18:54 -0400 Subject: [PATCH 18/20] Never skip Python's magic methods/members. --- addons/source-python/packages/source-python/core/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index 4e983b6b7..c9e8357e0 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -471,7 +471,9 @@ def some_method(self): base.__module__ == caller.__name__): for k, v in obj.__dict__.items(): if (f'{attr}.{k}' in skippables or - (skip_privates and k.startswith('_'))): + (skip_privates and k.startswith('_') and + not (k.startswith('__') and + k.endswith('__')))): continue if (k == '__doc__' and getattr(base, '__doc__', None) is not None): From 1fdca4f775e1e55d63de7c01b8d8ef147ef5875a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Thu, 13 Jun 2019 23:49:51 -0400 Subject: [PATCH 19/20] Reverted 74765ad and d231c8a because if private class members are skipped they becomes unusable outside of the extension class scope meaning they can no longer be retrieved from an instance which can break extension methods that rely on them. --- addons/source-python/packages/source-python/core/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/addons/source-python/packages/source-python/core/__init__.py b/addons/source-python/packages/source-python/core/__init__.py index c9e8357e0..5a705b95c 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -470,10 +470,7 @@ def some_method(self): if (obj.__name__ == base.__name__ and base.__module__ == caller.__name__): for k, v in obj.__dict__.items(): - if (f'{attr}.{k}' in skippables or - (skip_privates and k.startswith('_') and - not (k.startswith('__') and - k.endswith('__')))): + if f'{attr}.{k}' in skippables: continue if (k == '__doc__' and getattr(base, '__doc__', None) is not None): From fdc2ab81d7482a2e2da9417dcab263f0aaa3374b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Bri=C3=A8re?= Date: Fri, 14 Jun 2019 02:08:56 -0400 Subject: [PATCH 20/20] Fixed wrap_entity_mem_func decorated methods from being documented as properties. --- .../source-python/entities/helpers.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/addons/source-python/packages/source-python/entities/helpers.py b/addons/source-python/packages/source-python/entities/helpers.py index ccc9e9b9e..69597be9c 100644 --- a/addons/source-python/packages/source-python/entities/helpers.py +++ b/addons/source-python/packages/source-python/entities/helpers.py @@ -85,23 +85,28 @@ # >> CLASSES # ============================================================================= class EntityMemFuncWrapper(MemberFunction): - 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 + def __init__(self, wrapper): + self.__func__ = wrapper + + def __get__(self, wrapped_self, objtype): + if wrapped_self is None: + return self.__func__ self.wrapped_self = wrapped_self + func = wrapped_self.__getattr__(self.__func__.__name__) + super().__init__(func._manager, func._type_name, func, func._this) + return self def __call__(self, *args, **kwargs): return super().__call__( - *self.wrapper(self.wrapped_self, *args, **kwargs)) + *self.__func__(self.wrapped_self, *args, **kwargs)) def call_trampoline(self, *args, **kwargs): return super().call_trampoline( - *self.wrapper(self.wrapped_self, *args, **kwargs)) + *self.__func__(self.wrapped_self, *args, **kwargs)) def skip_hooks(self, *args, **kwargs): return super().skip_hooks( - *self.wrapper(self.wrapped_self, *args, **kwargs)) + *self.__func__(self.wrapped_self, *args, **kwargs)) # ============================================================================= @@ -109,8 +114,4 @@ 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 EntityMemFuncWrapper(wrapper)