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/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: 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 0c9d5d375..5a705b95c 100644 --- a/addons/source-python/packages/source-python/core/__init__.py +++ b/addons/source-python/packages/source-python/core/__init__.py @@ -12,19 +12,28 @@ from collections import defaultdict # Contextlib from contextlib import contextmanager +from contextlib import suppress +# 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 isroutine # OS from os import sep # Path from path import Path # Platform from platform import system +# Runpy +from runpy import run_path # Sys import sys +# Types +from types import MethodType # Urllib from urllib.request import urlopen # Weakref @@ -63,6 +72,7 @@ 'WeakAutoUnload', 'GAME_NAME', 'OutputReturn', + 'ENGINE_IMPORT_SKIPPABLES', 'PLATFORM', 'SOURCE_ENGINE', 'SOURCE_ENGINE_BRANCH', @@ -70,9 +80,11 @@ 'console_message', 'create_checksum', 'echo_console', + 'engine_import', 'get_core_modules', 'get_interface', 'get_public_ip', + 'get_wrapped', 'ignore_unicode_errors', 'server_output', ) @@ -90,6 +102,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 @@ -334,4 +356,163 @@ 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(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. + + 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 + + class SomeClass(SomeClass): + def some_method(self): + return 'OrangeBox' + + ``../packages/orangebox/cstrike/some_module.py`` + + .. code:: python + + 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. + + 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: + raise ImportError( + '"engine_import" must only be called from global scopes.') + skippables = ENGINE_IMPORT_SKIPPABLES + tuple(skippables) + 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 attr in skippables: + continue + if (attr == '__doc__' and + getattr(caller, '__doc__', None) is not None): + continue + if attr == '__all__': + if hasattr(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): + 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 + 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): + o = getattr(caller, attr) + if o is obj: + continue + elif isroutine(o): + update_wrapper(getattr(obj, '__func__', obj), o) + setattr(caller, attr, obj) + +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: + The wrapped function or ``None`` if the given wrapper is not wrapping + any function. + """ + if not isroutine(func): + raise TypeError(f'"{func}" is not a function.') + 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/_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 88% 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..efc17dda4 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,9 @@ # >> IMPORTS # ============================================================================= # Source.Python -from entities import BaseEntityGenerator -from . import Entity as _Entity +# Core +from core import get_wrapped +# Weapons from weapons.manager import weapon_manager @@ -30,7 +31,7 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class Entity(_Entity): +class Entity(Entity): """Class used to interact directly with entities.""" @classmethod @@ -38,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, cls)(parent_class or classname) entity.item_definition_index = index else: - entity = super().create(classname) + entity = get_wrapped(Entity.create, cls)(classname) return entity @classmethod @@ -64,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, cls)(classname) 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/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) 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/_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/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 91% 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..9d6fdae42 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,12 +27,12 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class Player(_Player): +class Player(Player): """Class used to interact directly with players.""" 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.""" @@ -54,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.""" @@ -65,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.""" 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/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..dbaf0b6d2 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,990 @@ # ============================================================================= -# >> 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 + + 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. - # Import the game-specific 'Player' class - Player = import_module( - 'players.engines.{engine}.{game}'.format( - engine=SOURCE_ENGINE, - game=GAME_NAME, + .. 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. + + :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] + + 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 - # Import the base 'Player' class - from players._base import Player + :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 +# ============================================================================= +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/helpers.py b/addons/source-python/packages/source-python/players/helpers.py index 4700fea57..4ae088ce7 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,24 @@ '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. + """ + 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() 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/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)) 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() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a58d6d205..8664a31e6 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) +{ +}