



Syntax: Select all
# ../temp_scanner/temp_scanner.py
# Python
from random import choice
# Source.Python
from core import PLATFORM
from core.cache import cached_property
from cvars import ConVar
from engines.trace import (ContentMasks, engine_trace, GameTrace, Ray,
MAX_TRACE_LENGTH)
from entities.constants import WORLD_ENTITY_INDEX, RenderEffects
from entities.entity import Entity
from entities.helpers import index_from_pointer
from entities.hooks import EntityPreHook, EntityCondition
from events import Event
from filters.entities import BaseEntityIter
from filters.players import PlayerIter
from listeners import OnLevelInit
from listeners.tick import Delay
from mathlib import Vector
from memory import Convention, DataType
from memory.helpers import MemberFunction
from memory.manager import TypeManager
# How many scanners should there be?
MAX_SCANNERS = 1
# How often should the scanner look for a new target? (in seconds)
TARGET_INTERVAL = 30
UP = Vector(0, 0, 1)
manager = TypeManager()
ACTIVATE_OFFSET = 33 if PLATFORM == 'windows' else 34
is_cscanner = EntityCondition.equals_entity_classname('npc_cscanner')
# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
def load():
"""Called when the plugin gets loaded."""
Scanner.spawn_health.set_int(100)
try:
# Try to spawn the scanners in case the plugin was loaded on the fly.
Scanner.spawn_scanners(MAX_SCANNERS)
except IndexError:
pass
def unload():
"""Called when the plugin gets unloaded."""
for scanner in Scanner.cache.copy().values():
scanner.remove()
@OnLevelInit
def on_level_init(map_name):
"""Called when a map starts."""
Delay(5, Scanner.spawn_scanners, (MAX_SCANNERS,), cancel_on_level_end=True)
@Event('round_start')
def round_start(event):
"""Called when a new round starts."""
Scanner.spawn_scanners(MAX_SCANNERS)
@Event('player_death')
@Event('player_disconnect')
def player_state_changed(event):
"""Called when a player dies or leaves the server."""
# Notify all the scanners that a player has just died.
for scanner in Scanner.cache.values():
scanner.on_player_state_changed(event['userid'])
# =============================================================================
# >> ON TAKE DAMAGE ALIVE
# =============================================================================
@EntityPreHook(is_cscanner, 'on_take_damage_alive')
def on_take_damage_alive_pre(stack_data):
"""Called when an 'npc_cscanner' is about to take damage."""
# Is this one of our scanners?
if index_from_pointer(stack_data[0]) in Scanner.cache:
# Block all damage.
return False
# =============================================================================
# >> SCANNER
# =============================================================================
class Scanner(Entity):
"""Extended Entity class.
Args:
index (int): A valid entity index.
caching (bool): Check for a cached instance?
Attributes:
target_userid (int): Userid of the player we're currently following.
think (Repeat): Instance of Repeat() used for looping the `_think()`
function.
"""
spawn_health = ConVar('sk_scanner_health')
def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.target_userid = None
self.think = self.repeat(self._think)
def _think(self):
player = self.get_random_player()
try:
# Try to store the player's userid for later use.
self.target_userid = player.userid
except AttributeError:
# Didn't find a player, try again in 5 seconds.
self.delay(5, self.restart_thinking)
return
# Is this player missing a 'target_name'?
if not player.target_name:
# Give them one.
player.target_name = f'player_{player.userid}'
self.call_input('SetFollowTarget', player.target_name)
@staticmethod
def spawn_scanners(number):
for i in range(number):
Scanner.create(origin=get_random_origin())
@classmethod
def create(cls, origin):
"""Creates the 'npc_cscanner' at the specified origin."""
scanner = super().create('npc_cscanner')
scanner.origin = origin
# Make the NPC fully transparent.
scanner.render_amt = 1
# Slowly fade it into existence.
scanner.render_fx = RenderEffects.SOLID_SLOW
scanner.spawn()
# If we don't activate the Scanner, the server will crash if it tries
# to inspect/photograph a player.
scanner.activate()
scanner.become_hostile()
# It's time for the Scanner to start thinking..
scanner.delay(1, scanner.restart_thinking)
return scanner
@cached_property
def activate(self):
"""Activates the entity."""
return MemberFunction(
manager,
DataType.VOID,
self.make_virtual_function(
ACTIVATE_OFFSET,
Convention.THISCALL,
(DataType.POINTER,),
DataType.VOID
),
self
)
def become_hostile(self):
"""Makes the NPC hate the players."""
self.call_input('SetRelationship', 'player D_HT 99')
def get_random_player(self):
"""Returns a randomly chosen player that is currently alive."""
players = []
for player in PlayerIter('alive'):
players.append(player)
# Did we not find any players?
if not players:
return None
return choice(players)
def restart_thinking(self):
"""Restarts the Scanner's thinking loop."""
self.think.stop()
self.think.start(interval=TARGET_INTERVAL, execute_on_start=True)
def on_player_state_changed(self, userid):
"""Called when a player dies or disconnects."""
# Was the Scanner following this player?
if userid == self.target_userid:
# Pick a new target.
self.delay(1, self.restart_thinking)
# =============================================================================
# >> HELPERS
# =============================================================================
def get_random_origin():
"""Returns a randomly chosen origin based on player spawnpoints."""
origins = []
# Go through all the 'info_player_*' entities.
# (combine, deathmatch, rebel, start).
for base_entity in BaseEntityIter(('info_player_',), False):
origins.append(base_entity.origin)
# Pick a random origin.
origin = choice(origins)
# Calculate the highest possible position.
ceiling = origin + UP * MAX_TRACE_LENGTH
# Let's fire a GameTrace() and see what we hit.
trace = GameTrace()
engine_trace.clip_ray_to_entity(
Ray(origin, ceiling),
ContentMasks.ALL,
Entity(WORLD_ENTITY_INDEX),
trace
)
# If the GameTrace() hit something, return that position, otherwise return
# the regular origin.
return trace.end_position if trace.did_hit() else origin
Code: Select all
2020-10-22 17:19:19 - sp - MESSAGE [SP] Loading plugin 'temp_scanner'...
2020-10-22 17:19:19 - sp - EXCEPTION
[SP] Caught an Exception:
Traceback (most recent call last):
File "..\addons\source-python\packages\source-python\plugins\command.py", line 164, in load_plugin
plugin = self.manager.load(plugin_name)
File "..\addons\source-python\packages\source-python\plugins\manager.py", line 194, in load
plugin._load()
File "..\addons\source-python\packages\source-python\plugins\instance.py", line 76, in _load
self.module.load()
File "..\addons\source-python\plugins\temp_scanner\temp_scanner.py", line 41, in load
Scanner.spawn_scanners(MAX_SCANNERS)
File "..\addons\source-python\plugins\temp_scanner\temp_scanner.py", line 126, in spawn_scanners
Scanner.create(origin=get_random_origin())
File "..\addons\source-python\plugins\temp_scanner\temp_scanner.py", line 200, in get_random_origin
origin = choice(origins)
File "..\addons\source-python\Python3\random.py", line 257, in choice
raise IndexError('Cannot choose from an empty sequence') from None
IndexError: Cannot choose from an empty sequence
VinciT wrote:...
Is it guaranteed to only be called for networked entities? Not fully sure, but I don't think damage event are limited to them, which would raise here in such case. A try/except may be safer.VinciT wrote:Syntax: Select all
if index_from_pointer(stack_data[0]) in Scanner.cache:
Would probably be better to use RenderEffects instead of hard-coded value.VinciT wrote:Syntax: Select all
scanner.set_key_value_int('renderfx', 7)
Not that it really matter here since you call it only once anyways, but I think it is generally better to cache dynamic functions where possible because making them on-the-fly can prove to be extremely costly in comparison. For example:VinciT wrote:Syntax: Select all
def activate(self):
"""Activates the entity."""
Activate = self.make_virtual_function(
ACTIVATE_OFFSET,
Convention.THISCALL,
(DataType.POINTER,),
DataType.VOID
)
Activate(self)
Syntax: Select all
@cached_property
def activate(self):
"""Activates the entity."""
return MemberFunction(
manager,
DataType.VOID,
self.make_virtual_function(
ACTIVATE_OFFSET,
Convention.THISCALL,
(DataType.POINTER,),
DataType.VOID
),
self
)
Unless I missed something obvious, a new target will not be selected if the current one is leaving the server while being alive because on_player_death will never be called.VinciT wrote:Syntax: Select all
self.restart_thinking()
Since CNPC_CScanner has its own OnTakeDamage_Alive() function, I believe the hook will only be limited to npc_cscanner entities. Would there still be a reason to use a try/except here?L'In20Cible wrote:Is it guaranteed to only be called for networked entities? Not fully sure, but I don't think damage event are limited to them, which would raise here in such case. A try/except may be safer.
Agreed and done!L'In20Cible wrote:Would probably be better to use RenderEffects instead of hard-coded value.
Ooooh that's awesome! Thank you for this!L'In20Cible wrote:Not that it really matter here since you call it only once anyways, but I think it is generally better to cache dynamic functions where possible because making them on-the-fly can prove to be extremely costly in comparison. For example:Syntax: Select all
@cached_property
def activate(self):
"""Activates the entity."""
return MemberFunction(
manager,
DataType.VOID,
self.make_virtual_function(
ACTIVATE_OFFSET,
Convention.THISCALL,
(DataType.POINTER,),
DataType.VOID
),
self
)
Thank you for the kind words, PEACE. Can't wait to get the boss scanner done so you guys can mess around with it.PEACE wrote:Wow wonder if he saw this i have him on Steam so ill let him know , you do some amazing work VinciT good going
Should be fixed with this update.daren adler wrote:Gave me this errorCode: Select all
2020-10-22 17:19:19 - sp - MESSAGE [SP] Loading plugin 'temp_scanner'...
2020-10-22 17:19:19 - sp - EXCEPTION
[SP] Caught an Exception:
Traceback (most recent call last):
File "..\addons\source-python\packages\source-python\plugins\command.py", line 164, in load_plugin
plugin = self.manager.load(plugin_name)
File "..\addons\source-python\packages\source-python\plugins\manager.py", line 194, in load
plugin._load()
File "..\addons\source-python\packages\source-python\plugins\instance.py", line 76, in _load
self.module.load()
File "..\addons\source-python\plugins\temp_scanner\temp_scanner.py", line 41, in load
Scanner.spawn_scanners(MAX_SCANNERS)
File "..\addons\source-python\plugins\temp_scanner\temp_scanner.py", line 126, in spawn_scanners
Scanner.create(origin=get_random_origin())
File "..\addons\source-python\plugins\temp_scanner\temp_scanner.py", line 200, in get_random_origin
origin = choice(origins)
File "..\addons\source-python\Python3\random.py", line 257, in choice
raise IndexError('Cannot choose from an empty sequence') from None
IndexError: Cannot choose from an empty sequence
Then that should do. If they have their own override in their dispatch table they shouldn't be called by any other entity types.VinciT wrote:Since CNPC_CScanner has its own OnTakeDamage_Alive() function, I believe the hook will only be limited to npc_cscanner entities. Would there still be a reason to use a try/except here?
PEACE wrote:Yes it does work well i have tested it on my server as well , but wonders if VinciT will make it work like in the vid clips so it attacks , that looks pretty mean . VinciT does good work so do many others in here and the fact they are willing to support HL2DM is so awesome of them . thanks guys ^5
Painkiller is correct, the gifs I posted are just a preview of what I'm currently working on. The plan was to post the special scanner as a single file plugin in this thread, but as I kept adding more and more features, I decided to turn it into a full blown plugin.Painkiller wrote:I think the things on the pictures, he is still working on it, it is only a preview.
Users browsing this forum: No registered users and 3 guests