diff --git a/addons/source-python/data/source-python/entities/csgo/CEconEntity.ini b/addons/source-python/data/source-python/entities/csgo/CEconEntity.ini new file mode 100644 index 000000000..5d6de7f2b --- /dev/null +++ b/addons/source-python/data/source-python/entities/csgo/CEconEntity.ini @@ -0,0 +1,3 @@ +[property] + + item_definition_index = m_AttributeManager.m_Item.m_iItemDefinitionIndex diff --git a/addons/source-python/data/source-python/weapons/csgo.ini b/addons/source-python/data/source-python/weapons/csgo.ini index 368fd0b0d..7c7369623 100644 --- a/addons/source-python/data/source-python/weapons/csgo.ini +++ b/addons/source-python/data/source-python/weapons/csgo.ini @@ -30,241 +30,310 @@ # Snipers [[awp]] slot = 1 - maxammo = "ammo_338mag_max" + maxammo = 30 ammoprop = 6 clip = 10 cost = 4750 + item_definition_index = 9 tags = "all,primary,rifle,sniper" [[g3sg1]] slot = 1 - maxammo = "ammo_762mm_max" + maxammo = 90 ammoprop = 2 clip = 20 cost = 5000 + item_definition_index = 11 tags = "all,primary,rifle,sniper" [[scar20]] slot = 1 - maxammo = "ammo_762mm_max" + maxammo = 90 ammoprop = 2 clip = 20 cost = 5000 + item_definition_index = 38 tags = "all,primary,rifle,sniper" [[ssg08]] slot = 1 - maxammo = "ammo_762mm_max" + maxammo = 90 ammoprop = 2 clip = 10 cost = 1700 + item_definition_index = 40 tags = "all,primary,rifle" # Assault Rifles [[ak47]] slot = 1 - maxammo = "ammo_762mm_max" + maxammo = 90 ammoprop = 2 clip = 30 cost = 2700 + item_definition_index = 7 tags = "all,primary,rifle" [[aug]] slot = 1 - maxammo = "ammo_762mm_max" + maxammo = 90 ammoprop = 2 clip = 30 cost = 3300 + item_definition_index = 8 tags = "all,primary,rifle" [[famas]] slot = 1 - maxammo = "ammo_556mm_max" + maxammo = 90 ammoprop = 3 clip = 25 cost = 2250 + item_definition_index = 10 tags = "all,primary,rifle" [[galilar]] slot = 1 - maxammo = "ammo_556mm_max" + maxammo = 90 ammoprop = 3 clip = 35 cost = 2000 + item_definition_index = 13 tags = "all,primary,rifle" [[m4a1]] slot = 1 - maxammo = "ammo_556mm_max" + maxammo = 90 ammoprop = 3 clip = 30 cost = 3100 + item_definition_index = 16 tags = "all,primary,rifle" + [[m4a1_silencer]] + slot = 1 + maxammo = 40 + ammoprop = 4 + clip = 20 + cost = 3100 + item_definition_index = 60 + parent_class = "weapon_m4a1" + tags = "all,secondary,rifle" + [[sg556]] slot = 1 - maxammo = "ammo_556mm_max" + maxammo = 90 ammoprop = 3 clip = 30 cost = 3000 + item_definition_index = 39 tags = "all,primary,rifle" # SMGs [[bizon]] slot = 1 - maxammo = "ammo_9mm_max" + maxammo = 120 ammoprop = 7 clip = 64 cost = 1400 + item_definition_index = 26 tags = "all,primary,smg" [[mac10]] slot = 1 - maxammo = "ammo_45acp_max" + maxammo = 100 ammoprop = 9 clip = 30 cost = 1050 + item_definition_index = 17 tags = "all,primary,smg" [[mp7]] slot = 1 - maxammo = "ammo_9mm_max" + maxammo = 120 ammoprop = 7 clip = 30 cost = 1700 + item_definition_index = 33 tags = "all,primary,smg" [[mp9]] slot = 1 - maxammo = "ammo_9mm_max" + maxammo = 120 ammoprop = 7 clip = 30 cost = 1250 + item_definition_index = 34 tags = "all,primary,smg" [[p90]] slot = 1 - maxammo = "ammo_57mm_max" + maxammo = 100 ammoprop = 13 clip = 50 cost = 2350 + item_definition_index = 19 tags = "all,primary,smg" [[ump45]] slot = 1 - maxammo = "ammo_45acp_max" + maxammo = 100 ammoprop = 9 clip = 25 cost = 1200 + item_definition_index = 24 tags = "all,primary,smg" # Shotguns [[mag7]] slot = 1 - maxammo = "ammo_buckshot_max" + maxammo = 32 ammoprop = 8 clip = 5 cost = 1800 + item_definition_index = 27 tags = "all,primary,shotgun" [[nova]] slot = 1 - maxammo = "ammo_buckshot_max" + maxammo = 32 ammoprop = 8 clip = 8 cost = 1200 + item_definition_index = 35 tags = "all,primary,shotgun" [[sawedoff]] slot = 1 - maxammo = "ammo_buckshot_max" + maxammo = 32 ammoprop = 8 clip = 7 cost = 1200 + item_definition_index = 29 tags = "all,primary,shotgun" [[xm1014]] slot = 1 - maxammo = "ammo_buckshot_max" + maxammo = 32 ammoprop = 8 clip = 7 cost = 2000 + item_definition_index = 25 tags = "all,primary,shotgun" # Heavy Machine Gun [[m249]] slot = 1 - maxammo = "ammo_556mm_box_max" + maxammo = 200 ammoprop = 5 clip = 100 cost = 5200 + item_definition_index = 14 tags = "all,primary,machinegun" [[negev]] slot = 1 - maxammo = "ammo_556mm_box_max" + maxammo = 200 ammoprop = 5 clip = 150 cost = 5700 + item_definition_index = 28 tags = "all,primary,machinegun" # ========================================================================= # SECONDARY WEAPONS # ========================================================================= + [[cz75a]] + slot = 2 + maxammo = 12 + ammoprop = 12 + clip = 12 + cost = 500 + item_definition_index = 63 + parent_class = "weapon_p250" + tags = "all,secondary,pistol" + [[deagle]] slot = 2 - maxammo = "ammo_50AE_max" + maxammo = 35 ammoprop = 1 clip = 7 cost = 700 + item_definition_index = 1 tags = "all,secondary,pistol" [[elite]] slot = 2 - maxammo = "ammo_9mm_max" + maxammo = 120 ammoprop = 7 clip = 30 cost = 500 + item_definition_index = 2 tags = "all,secondary,pistol" [[fiveseven]] slot = 2 - maxammo = "ammo_57mm_max" + maxammo = 100 ammoprop = 13 clip = 20 cost = 500 + item_definition_index = 3 tags = "all,secondary,pistol" [[glock]] slot = 2 - maxammo = "ammo_9mm_max" + maxammo = 120 ammoprop = 7 clip = 20 cost = 200 + item_definition_index = 4 tags = "all,secondary,pistol" [[hkp2000]] slot = 2 - maxammo = "ammo_357sig_small_max" + maxammo = 24 ammoprop = 10 clip = 12 cost = 200 + item_definition_index = 32 tags = "all,secondary,pistol" [[p250]] slot = 2 - maxammo = "ammo_357sig_p250_max" + maxammo = 26 ammoprop = 20 clip = 13 cost = 300 + item_definition_index = 36 + tags = "all,secondary,pistol" + + [[revolver]] + slot = 2 + maxammo = 8 + ammoprop = 1 + clip = 8 + cost = 850 + item_definition_index = 64 + parent_class = "weapon_deagle" tags = "all,secondary,pistol" [[tec9]] slot = 2 - maxammo = "ammo_9mm_max" + maxammo = 120 ammoprop = 7 clip = 32 cost = 500 + item_definition_index = 30 + tags = "all,secondary,pistol" + + [[usp_silencer]] + slot = 2 + maxammo = 24 + ammoprop = 11 + clip = 12 + cost = 200 + item_definition_index = 61 + parent_class = "weapon_hkp2000" tags = "all,secondary,pistol" # ========================================================================= @@ -272,8 +341,75 @@ # ========================================================================= [[knife]] slot = 3 + item_definition_index = 42 + tags = "all,knife,melee" + + [[knife_bayonet]] + slot = 3 + item_definition_index = 500 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_butterfly]] + slot = 3 + item_definition_index = 515 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_falchion]] + slot = 3 + item_definition_index = 512 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_flip]] + slot = 3 + item_definition_index = 505 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_gut]] + slot = 3 + item_definition_index = 506 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_karambit]] + slot = 3 + item_definition_index = 507 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_m9_bayonet]] + slot = 3 + item_definition_index = 508 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_push]] + slot = 3 + item_definition_index = 516 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_survival_bowie]] + slot = 3 + item_definition_index = 514 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + + [[knife_t]] + slot = 3 + item_definition_index = 59 + parent_class = "weapon_knife" tags = "all,knife,melee" + [[knife_tactical]] + slot = 3 + item_definition_index = 509 + parent_class = "weapon_knife" + tags = "all,knife,melee,earned" + [[knifegg]] slot = 3 tags = "all,knife,melee" @@ -284,6 +420,7 @@ ammoprop = 19 clip = 1 cost = 400 + item_definition_index = 31 tags = "all,melee" # ========================================================================= @@ -294,6 +431,7 @@ maxammo = "ammo_grenade_limit_default" ammoprop = 18 cost = 50 + item_definition_index = 47 tags = "all,grenade" [[flashbang]] @@ -301,6 +439,7 @@ maxammo = "ammo_grenade_limit_flashbang" ammoprop = 15 cost = 200 + item_definition_index = 43 tags = "all,grenade" [[hegrenade]] @@ -308,6 +447,7 @@ maxammo = "ammo_grenade_limit_default" ammoprop = 14 cost = 300 + item_definition_index = 44 tags = "all,grenade,explosive" [[incgrenade]] @@ -315,6 +455,7 @@ maxammo = "ammo_grenade_limit_default" ammoprop = 17 cost = 600 + item_definition_index = 48 tags = "all,grenade,incendiary" [[molotov]] @@ -322,6 +463,7 @@ maxammo = "ammo_grenade_limit_default" ammoprop = 17 cost = 400 + item_definition_index = 46 tags = "all,grenade,incendiary" [[smokegrenade]] @@ -329,6 +471,7 @@ maxammo = "ammo_grenade_limit_default" ammoprop = 16 cost = 300 + item_definition_index = 45 tags = "all,grenade" # ========================================================================= @@ -336,4 +479,5 @@ # ========================================================================= [[c4]] slot = 5 + item_definition_index = 49 tags = "all,objective" diff --git a/addons/source-python/packages/source-python/entities/_base.py b/addons/source-python/packages/source-python/entities/_base.py new file mode 100644 index 000000000..79ad8cede --- /dev/null +++ b/addons/source-python/packages/source-python/entities/_base.py @@ -0,0 +1,875 @@ +# ../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 object.""" + # 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 _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_color(self): + """Return the entity's color. + + :rtype: Color + """ + return self.render_color + + def set_color(self, color): + """Set the entity's color. + + :param Color color: + Color to set. + """ + # Set the entity's render mode + self.render_mode = RenderMode.TRANS_COLOR + + # Set the entity's color + self.render_color = color + + # Set the entity's alpha + self.render_amt = color.a + + # Set the "color" property for Entity + color = property( + get_color, set_color, + doc="""Property to get/set the entity's color values.""") + + def get_model(self): + """Return the entity's model. + + :rtype: Model + """ + 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.""") + + @property + def model_header(self): + """Return a ModelHeader instance of the current entity's model.""" + return model_cache.get_model_header(model_cache.find_model( + self.model_name)) + + def get_property_bool(self, name): + """Return the boolean property.""" + return self._get_property(name, 'bool') + + def get_property_color(self, name): + """Return the Color property.""" + return self._get_property(name, 'Color') + + def get_property_edict(self, name): + """Return the Edict property.""" + return self._get_property(name, 'Edict') + + def get_property_float(self, name): + """Return the float property.""" + return self._get_property(name, 'float') + + def get_property_int(self, name): + """Return the integer property.""" + return self._get_property(name, 'int') + + def get_property_interval(self, name): + """Return the Interval property.""" + return self._get_property(name, 'Interval') + + def get_property_pointer(self, name): + """Return the pointer property.""" + return self._get_property(name, 'pointer') + + def get_property_quaternion(self, name): + """Return the Quaternion property.""" + return self._get_property(name, 'Quaternion') + + def get_property_short(self, name): + """Return the short property.""" + return self._get_property(name, 'short') + + def get_property_ushort(self, name): + """Return the ushort property.""" + return self._get_property(name, 'ushort') + + def get_property_string(self, name): + """Return the string property.""" + return self._get_property(name, 'string_array') + + def get_property_string_pointer(self, name): + """Return the string property.""" + return self._get_property(name, 'string_pointer') + + def get_property_char(self, name): + """Return the char property.""" + return self._get_property(name, 'char') + + def get_property_uchar(self, name): + """Return the uchar property.""" + return self._get_property(name, 'uchar') + + def get_property_uint(self, name): + """Return the uint property.""" + return self._get_property(name, 'uint') + + def get_property_vector(self, name): + """Return the Vector property.""" + 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.""" + self._set_property(name, 'bool', value) + + def set_property_color(self, name, value): + """Set the Color property.""" + self._set_property(name, 'Color', value) + + def set_property_edict(self, name, value): + """Set the Edict property.""" + self._set_property(name, 'Edict', value) + + def set_property_float(self, name, value): + """Set the float property.""" + self._set_property(name, 'float', value) + + def set_property_int(self, name, value): + """Set the integer property.""" + self._set_property(name, 'int', value) + + def set_property_interval(self, name, value): + """Set the Interval property.""" + self._set_property(name, 'Interval', value) + + def set_property_pointer(self, name, value): + """Set the pointer property.""" + self._set_property(name, 'pointer', value) + + def set_property_quaternion(self, name, value): + """Set the Quaternion property.""" + self._set_property(name, 'Quaternion', value) + + def set_property_short(self, name, value): + """Set the short property.""" + self._set_property(name, 'short', value) + + def set_property_ushort(self, name, value): + """Set the ushort property.""" + self._set_property(name, 'ushort', value) + + def set_property_string(self, name, value): + """Set the string property.""" + self._set_property(name, 'string_array', value) + + def set_property_string_pointer(self, name, value): + """Set the string property.""" + self._set_property(name, 'string_pointer', value) + + def set_property_char(self, name, value): + """Set the char property.""" + self._set_property(name, 'char', value) + + def set_property_uchar(self, name, value): + """Set the uchar property.""" + self._set_property(name, 'uchar', value) + + def set_property_uint(self, name, value): + """Set the uint property.""" + self._set_property(name, 'uint', value) + + def set_property_vector(self, name, value): + """Set the Vector property.""" + 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. + :raises 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 lookup_attachment(self, name): + """Return the attachment index matching the given name. + + :param str name: + The name of the attachment. + :rtype: int + """ + # Get the ModelHeader instance of the entity + model_header = self.model_header + + # Loop through all attachments + for index in range(model_header.attachments_count): + + # Are the names matching? + if name == model_header.get_attachment(index).name: + + # Return the current index + return index + + # No attachment found + return INVALID_ATTACHMENT_INDEX + + 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 stop_sound(self, sample, channel=Channel.AUTO): + """Stop the given sound from being emitted by this entity. + + :param str sample: + Sound file relative to the ``sounds`` directory. + :param Channel channel: + The channel of the sound. + """ + engine_sound.stop_sound(self.index, channel, sample) + + 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/__init__.py b/addons/source-python/packages/source-python/entities/engines/__init__.py new file mode 100644 index 000000000..51c945f50 --- /dev/null +++ b/addons/source-python/packages/source-python/entities/engines/__init__.py @@ -0,0 +1,3 @@ +# ../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 new file mode 100644 index 000000000..2d0c2d4e5 --- /dev/null +++ b/addons/source-python/packages/source-python/entities/engines/csgo/__init__.py @@ -0,0 +1,66 @@ +# ../engines/engines/csgo/__init__.py + +"""Provides CS:GO specific Entity based functionality.""" + +# ============================================================================= +# >> IMPORTS +# ============================================================================= +# Source.Python +from entities import BaseEntityGenerator +from entities._base import Entity as _Entity +from weapons.manager import weapon_manager + + +# ============================================================================= +# >> GLOBAL VARIABLES +# ============================================================================= +_weapon_names_for_definition = { + weapon_manager[weapon].name: values.get('item_definition_index') + for weapon, values in weapon_manager.ini['weapons'].items() + if values.get('item_definition_index') +} +_weapon_parents = { + weapon_manager[weapon].name: values.get('parent_class') + for weapon, values in weapon_manager.ini['weapons'].items() + if values.get('parent_class') +} +_parent_weapons = set(_weapon_parents.values()) + + +# ============================================================================= +# >> CLASSES +# ============================================================================= +class Entity(_Entity): + """Class used to interact directly with entities.""" + + @classmethod + def create(cls, classname): + index = _weapon_names_for_definition.get(classname) + if classname in _weapon_parents and index is not None: + entity = super().create(_weapon_parents[classname]) + entity.item_definition_index = index + else: + entity = super().create(classname) + return entity + + @classmethod + def find(cls, classname): + index = _weapon_names_for_definition.get(classname) + if classname in _weapon_parents and index is not None: + parent_classname = _weapon_parents[classname] + for entity in BaseEntityGenerator(parent_classname, True): + if not entity.is_networked(): + continue + if entity.get_network_property_int( + 'm_AttributeManager.m_Item.m_iItemDefinitionIndex' + ) == index: + return cls(entity.index) + elif classname in _parent_weapons: + for entity in BaseEntityGenerator(classname, True): + if not entity.is_networked(): + continue + if entity.get_network_property_int( + 'm_AttributeManager.m_Item.m_iItemDefinitionIndex' + ) in (index, 0): + return cls(entity.index) + return super().find(classname) diff --git a/addons/source-python/packages/source-python/entities/entity.py b/addons/source-python/packages/source-python/entities/entity.py index 96dd3b890..a1d2413fc 100644 --- a/addons/source-python/packages/source-python/entities/entity.py +++ b/addons/source-python/packages/source-python/entities/entity.py @@ -1,61 +1,14 @@ # ../entities/entity.py -"""Provides a base class to interact with a specific entity.""" +"""Provides a class used to interact with a specific entity.""" # ============================================================================= # >> IMPORTS # ============================================================================= -# Python Imports -# Collections -from collections import defaultdict -# Contextlib -from contextlib import suppress - -# Source.Python Imports -# Core +from importlib import import_module 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 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 edict_from_index -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 get_object_pointer -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 +from core import SOURCE_ENGINE +from paths import SP_PACKAGES_PATH # ============================================================================= @@ -69,8 +22,7 @@ # ============================================================================= # >> ALL DECLARATION # ============================================================================= -# Add all the global variables to __all__ -__all__ = ('BaseEntity', +__all__ = ('BaseEntity' 'Entity', ) @@ -78,816 +30,30 @@ # ============================================================================= # >> 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 object.""" - # 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 _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_color(self): - """Return the entity's color. - - :rtype: Color - """ - return self.render_color - - def set_color(self, color): - """Set the entity's color. - - :param Color color: - Color to set. - """ - # Set the entity's render mode - self.render_mode = RenderMode.TRANS_COLOR - - # Set the entity's color - self.render_color = color - - # Set the entity's alpha - self.render_amt = color.a - - # Set the "color" property for Entity - color = property( - get_color, set_color, - doc="""Property to get/set the entity's color values.""") - - def get_model(self): - """Return the entity's model. - - :rtype: Model - """ - 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.""") - - @property - def model_header(self): - """Return a ModelHeader instance of the current entity's model.""" - return model_cache.get_model_header(model_cache.find_model( - self.model_name)) - - def get_property_bool(self, name): - """Return the boolean property.""" - return self._get_property(name, 'bool') - - def get_property_color(self, name): - """Return the Color property.""" - return self._get_property(name, 'Color') - - def get_property_edict(self, name): - """Return the Edict property.""" - return self._get_property(name, 'Edict') - - def get_property_float(self, name): - """Return the float property.""" - return self._get_property(name, 'float') - - def get_property_int(self, name): - """Return the integer property.""" - return self._get_property(name, 'int') - - def get_property_interval(self, name): - """Return the Interval property.""" - return self._get_property(name, 'Interval') - - def get_property_pointer(self, name): - """Return the pointer property.""" - return self._get_property(name, 'pointer') - - def get_property_quaternion(self, name): - """Return the Quaternion property.""" - return self._get_property(name, 'Quaternion') - - def get_property_short(self, name): - """Return the short property.""" - return self._get_property(name, 'short') - - def get_property_ushort(self, name): - """Return the ushort property.""" - return self._get_property(name, 'ushort') - - def get_property_string(self, name): - """Return the string property.""" - return self._get_property(name, 'string_array') - - def get_property_string_pointer(self, name): - """Return the string property.""" - return self._get_property(name, 'string_pointer') - - def get_property_char(self, name): - """Return the char property.""" - return self._get_property(name, 'char') - - def get_property_uchar(self, name): - """Return the uchar property.""" - return self._get_property(name, 'uchar') - - def get_property_uint(self, name): - """Return the uint property.""" - return self._get_property(name, 'uint') - - def get_property_vector(self, name): - """Return the Vector property.""" - 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.""" - self._set_property(name, 'bool', value) - - def set_property_color(self, name, value): - """Set the Color property.""" - self._set_property(name, 'Color', value) - - def set_property_edict(self, name, value): - """Set the Edict property.""" - self._set_property(name, 'Edict', value) - - def set_property_float(self, name, value): - """Set the float property.""" - self._set_property(name, 'float', value) - - def set_property_int(self, name, value): - """Set the integer property.""" - self._set_property(name, 'int', value) - - def set_property_interval(self, name, value): - """Set the Interval property.""" - self._set_property(name, 'Interval', value) - - def set_property_pointer(self, name, value): - """Set the pointer property.""" - self._set_property(name, 'pointer', value) - - def set_property_quaternion(self, name, value): - """Set the Quaternion property.""" - self._set_property(name, 'Quaternion', value) - - def set_property_short(self, name, value): - """Set the short property.""" - self._set_property(name, 'short', value) - - def set_property_ushort(self, name, value): - """Set the ushort property.""" - self._set_property(name, 'ushort', value) - - def set_property_string(self, name, value): - """Set the string property.""" - self._set_property(name, 'string_array', value) - - def set_property_string_pointer(self, name, value): - """Set the string property.""" - self._set_property(name, 'string_pointer', value) - - def set_property_char(self, name, value): - """Set the char property.""" - self._set_property(name, 'char', value) - - def set_property_uchar(self, name, value): - """Set the uchar property.""" - self._set_property(name, 'uchar', value) - - def set_property_uint(self, name, value): - """Set the uint property.""" - self._set_property(name, 'uint', value) - - def set_property_vector(self, name, value): - """Set the Vector property.""" - 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. - :raises 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 lookup_attachment(self, name): - """Return the attachment index matching the given name. - - :param str name: - The name of the attachment. - :rtype: int - """ - # Get the ModelHeader instance of the entity - model_header = self.model_header - - # Loop through all attachments - for index in range(model_header.attachments_count): - - # Are the names matching? - if name == model_header.get_attachment(index).name: - - # Return the current index - return index - - # No attachment found - return INVALID_ATTACHMENT_INDEX - - 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 stop_sound(self, sample, channel=Channel.AUTO): - """Stop the given sound from being emitted by this entity. - - :param str sample: - Sound file relative to the ``sounds`` directory. - :param Channel channel: - The channel of the sound. - """ - engine_sound.stop_sound(self.index, channel, sample) - - 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] +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 diff --git a/addons/source-python/packages/source-python/weapons/_base.py b/addons/source-python/packages/source-python/weapons/_base.py index 1b08dce6a..4af903f1b 100644 --- a/addons/source-python/packages/source-python/weapons/_base.py +++ b/addons/source-python/packages/source-python/weapons/_base.py @@ -151,3 +151,8 @@ def set_secondary_fire_ammo(self, value): 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 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 index 0c89df66c..0ef935ca8 100644 --- a/addons/source-python/packages/source-python/weapons/engines/csgo/__init__.py +++ b/addons/source-python/packages/source-python/weapons/engines/csgo/__init__.py @@ -10,13 +10,26 @@ from weapons.manager import weapon_manager +# ============================================================================= +# >> GLOBAL VARIABLES +# ============================================================================= +_item_definition_indexes = { + values.get('item_definition_index'): weapon_manager[weapon].name + for weapon, values in weapon_manager.ini['weapons'].items() + if values.get('item_definition_index') +} + + # ============================================================================= # >> CLASSES # ============================================================================= class Weapon(_Weapon): """Allows easy usage of the weapon's attributes.""" def get_ammo(self): - """Return the amount of ammo the player has for the weapon.""" + """Return the amount of ammo the player has for the weapon. + + :rtype: int + """ # Is the weapon not a grenade? if 'grenade' not in weapon_manager[self.classname].tags: return self.primary_ammo_count @@ -29,7 +42,6 @@ def get_ammo(self): ) ) - def set_ammo(self, value): """Set the player's ammo property for the weapon.""" # Is the weapon not a grenade? @@ -46,8 +58,18 @@ def set_ammo(self, value): value, ) - # Set the "ammo" property methods ammo = property( get_ammo, set_ammo, doc="""Property to get/set the weapon's ammo.""") + + @property + def weapon_name(self): + """Return the full class name of the weapon. + + :rtype: str + """ + return _item_definition_indexes.get( + self.item_definition_index, + self.classname, + ) diff --git a/addons/source-python/packages/source-python/weapons/manager.py b/addons/source-python/packages/source-python/weapons/manager.py index b96749480..3eff68e6a 100644 --- a/addons/source-python/packages/source-python/weapons/manager.py +++ b/addons/source-python/packages/source-python/weapons/manager.py @@ -46,10 +46,10 @@ def __init__(self): super().__init__() # Get the ConfigObj instance of the file - ini = ConfigObj(_gamepath, unrepr=True) + self.ini = ConfigObj(_gamepath, unrepr=True) # Get the "properties" - properties = ini['properties'] + properties = self.ini['properties'] # Get the game's weapon prefix self._prefix = properties['prefix'] @@ -61,22 +61,24 @@ def __init__(self): self._myweapons = properties['myweapons'] # Store any special names - self._special_names = ini.get('special names', {}) + self._special_names = self.ini.get('special names', {}) # Store projectile names - self._projectiles = ini.get('projectiles', {}) + self._projectiles = self.ini.get('projectiles', {}) # Store tags as a set self._tags = set() # Loop through all weapons - for basename in ini['weapons']: + for basename in self.ini['weapons']: # Get the weapon's full name name = self._format_name(basename) # Add the weapon to the dictionary - self[name] = WeaponClass(name, basename, ini['weapons'][basename]) + self[name] = WeaponClass( + name, basename, self.ini['weapons'][basename] + ) # Add the weapon's tags to the set of tags self._tags.update(self[name].tags)