[HL2:DM] Crossbow

A place for requesting new Source.Python plugins to be made for your server.

Please request only one plugin per thread.
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Crossbow

Postby L'In20Cible » Thu Apr 23, 2020 5:18 am

VinciT wrote:Is there no way this can be adjusted within DynamicHooks? Maybe with a recursion limit?

I haven't investigated so not sure of the feasibility with the current implementation, nor if this would cause other issues, but in theory, it could be easier to workaround than a recursion limit. An idea could be to use a vector that push back in pre and pop in post instead of only storing a single address. Which would ensure the post handler always retrieve the address for the current scope, etc. regardless if the register was re-used. For example, here is a simple Python code illustrating it:

Syntax: Select all

addresses = list()

def pre(depth=0):
if depth == 10:
return
addresses.append(depth)
print(addresses)
pre(depth + 1)
post()

def post():
current = addresses.pop()
print(addresses, '# Current:', current)

pre()


Syntax: Select all

[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 5]
[0, 1, 2, 3, 4, 5, 6]
[0, 1, 2, 3, 4, 5, 6, 7]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8] # Current: 9
[0, 1, 2, 3, 4, 5, 6, 7] # Current: 8
[0, 1, 2, 3, 4, 5, 6] # Current: 7
[0, 1, 2, 3, 4, 5] # Current: 6
[0, 1, 2, 3, 4] # Current: 5
[0, 1, 2, 3] # Current: 4
[0, 1, 2] # Current: 3
[0, 1] # Current: 2
[0] # Current: 1
[] # Current: 0
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Crossbow

Postby Painkiller » Thu Apr 23, 2020 7:45 am

Hello,

I'll try the color codes.

It appears that it only lights up when touching another entity (and not the world). I'd like to avoid using VPhysicsCollision() if at all possible, as it seems like it gets called quite often. What if the grenade pulsed the effect each second, and every time someone grabs or throws it, as well as when it explodes? Would that be okay?


That would be great. (Maybe an option to turn on/off please.
I would also be very happy if there could be a random option for the colors (only npc_grenade_frag)

Could you put that in for the guns, please?
In the picture you can see two circles running in opposite directions. (These rings only exist on this map)
Image


Otherwise, the new code will work properly.
Thank you very much for that and praise for your work.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Crossbow

Postby VinciT » Fri Apr 24, 2020 2:29 am

L'In20Cible wrote:I haven't investigated so not sure of the feasibility with the current implementation, nor if this would cause other issues, but in theory, it could be easier to workaround than a recursion limit. An idea could be to use a vector that push back in pre and pop in post instead of only storing a single address. Which would ensure the post handler always retrieve the address for the current scope, etc. regardless if the register was re-used.
Seems like a challenging issue. Here's hoping to having a solution for it one day.

Painkiller wrote:That would be great. (Maybe an option to turn on/off please.
I would also be very happy if there could be a random option for the colors (only npc_grenade_frag)
I wasn't able to get the grenade to pulse when a player grabs or throws it, but I think the pulsing looks rather nice (with random colors enabled).


Painkiller wrote:Could you put that in for the guns, please?
In the picture you can see two circles running in opposite directions. (These rings only exist on this map)
I'm not exactly sure how those are made, so for the time being I've added spotlights for weapons instead. I hope that's okay.

Syntax: Select all

# ../lights/lights.py

# Python
import random
from colorsys import hsv_to_rgb

# Source.Python
from colors import Color
from effects.base import TempEntity
from effects.hooks import TempEntityPreHook
from entities.constants import EntityEffects
from entities.dictionary import EntityDictionary
from entities.entity import Entity, BaseEntity
from entities.helpers import index_from_pointer
from entities.hooks import EntityPreHook, EntityCondition
from filters.entities import BaseEntityIter
from filters.recipients import RecipientFilter
from filters.weapons import WeaponClassIter
from listeners import OnEntityOutput, OnEntitySpawned, OnEntityDeleted
from mathlib import QAngle
from stringtables import string_tables


# Colors when the 'prop_combine_ball' bounces and explodes.
COLOR_BALL_BOUNCE = Color(0, 191, 255)
COLOR_BALL_EXPLODE = Color(170, 0, 255)
# Color when the 'crossbow_bolt' bounces.
COLOR_BOLT_BOUNCE = Color(255, 88, 0)
# Colors when the 'npc_grenade_frag' pulses and explodes.
COLOR_FRAG_PULSE = Color(0, 255, 88)
COLOR_FRAG_EXPLODE = Color(0, 255, 88)
# Should the grenade color be random? (0 - no, 1 - yes)
COLOR_FRAG_RANDOM = 1
# Default color for weapons.
COLOR_WEAPON_DEFAULT = Color(255, 25, 0)


# How wide should the 'point_spotlight' beam be?
SPOTLIGHT_DEFAULT_WIDTH = 20
# How tall should the 'point_spotlight' beam be?
SPOTLIGHT_DEFAULT_LENGTH = 300


item_colors = {
'item_battery': Color(255, 255, 25),
'item_healthkit': Color(25, 255, 55),
'item_healthvial': Color(25, 255, 55),
'weapon_default': COLOR_WEAPON_DEFAULT
}


spotlight_pairs = {}
# Angle to make the 'point_spotlight' point upwards.
SPOTLIGHT_ANGLE = QAngle(-90, 0, 0)


class Grenade(Entity):
"""Extended Entity class."""
caching = True

def start_pulsing(self):
"""Starts looping the `self.pulse()` function."""
self.repeat(self.pulse).start(interval=0.75)

def pulse(self, color=COLOR_FRAG_PULSE, radius=200, decay=200):
"""Creates a dynamic light effect with the given arguments at its
current origin."""
create_dynamic_light(
self.origin,
random.choice(grenade_colors) if COLOR_FRAG_RANDOM else color,
radius,
0.6,
8,
decay
)


def get_pretty_colors(amount):
"""Returns a list of vibrant colors.

Args:
amount (int): How many colors should be generated?

Returns:
list of Color: A list containing Color instances.
"""
colors = []
step = 1 / amount

for hue in range(0, amount):
colors.append(
Color(*(int(255 * x) for x in hsv_to_rgb(step * hue, 1, 1))))

return colors


# List of vibrant colors.
grenade_colors = get_pretty_colors(amount=25)
# Dictionary that stores 'npc_grenade_frag' instances.
grenade_instances = EntityDictionary(Grenade)


def load():
"""Called when the plugin is loaded."""
weapons = [weapon.name for weapon in list(WeaponClassIter())]

# In case of a late plugin load, find the entities that need spotlights.
for base_entity in BaseEntityIter(set(list(item_colors.keys()) + weapons)):
# Avoid weapons equipped by players.
if base_entity.get_datamap_property_int('m_hOwnerEntity') != -1:
continue

# Avoid disabled weapons.
if base_entity.get_datamap_property_int(
'm_fEffects') & EntityEffects.NODRAW:
continue

create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)


def unload():
"""Called when the plugin is unloaded."""
# Go through all the 'point_spotlight' entities and remove them.
for spotlight in spotlight_pairs.values():
spotlight.remove()


# =============================================================================
# >> DYNAMIC_LIGHT: crossbow_bolt, prop_combine_ball, npc_grenade_frag
# =============================================================================
@EntityPreHook(
EntityCondition.equals_entity_classname('npc_grenade_frag'), 'detonate')
def detonate_pre(stack_data):
"""Called when an 'npc_grenade_frag' is about to explode."""
index = index_from_pointer(stack_data[0])

if index in grenade_instances:
grenade_instances[index].pulse(
color=COLOR_FRAG_EXPLODE, radius=400, decay=400)


@TempEntityPreHook('EffectDispatch')
def effect_dispatch_pre(temp_entity, recipient_filter):
# Get the name of the effect.
effect_name = string_tables.EffectDispatch[temp_entity.effect_name_index]

# Did the 'prop_combine_ball' bounce?
if 'cball_bounce' in effect_name:
# Delay the effect by a single frame, otherwise the server will crash!
temp_entity.entity.delay(0, create_dynamic_light,
(temp_entity.origin, COLOR_BALL_BOUNCE, 200, 3, 8, 150))

# Or did it explode?
if 'cball_explode' in effect_name:
temp_entity.entity.delay(0, create_dynamic_light,
(temp_entity.origin, COLOR_BALL_EXPLODE, 600, 1, 8, 600))


@EntityPreHook(
EntityCondition.equals_entity_classname('crossbow_bolt'), 'start_touch')
def bolt_touch_pre(stack_data):
entity = Entity._obj(stack_data[0])

# Is this a bolt?
if 'crossbow_bolt' in entity.classname:
create_dynamic_light(entity.origin, COLOR_BOLT_BOUNCE, 200, 3, 8, 150)

# Try not to freeze the server. (thank you L'In20Cible)
parent = entity.parent
if parent is not None:
parent.start_touch.skip_hooks(stack_data[1])
return False


def create_dynamic_light(
origin, color, radius, life_time, exponent, decay, *recipients):
"""Creates a dynamic light effect at the specified origin.

Args:
origin (Vector): Spawn position of the effect.
color (Color): Color of the effect.
radius (float): How wide the glow is.
life_time (float): How long should the effect last? (in seconds)
exponent (int): Strength of the glow.
decay (float): By how much the radius is lowered each second.
recipients (RecipientFilter): Players that should see the effect.
"""
light = TempEntity('Dynamic Light')
light.origin = origin
light.color = color
light.radius = radius
light.life_time = life_time
light.exponent = exponent
light.decay = decay
light.create(RecipientFilter())


# =============================================================================
# >> POINT_SPOTLIGHT: items and weapons
# =============================================================================
class Spotlight(Entity):
"""Modified Entity class for properly removing a 'point_spotlight'."""
caching = True

def remove(self):
"""Turns off the 'point_spotlight' before removing it."""
self.call_input('LightOff')
super().remove()


def create_spotlight(origin, color, **kwargs):
"""Creates a 'point_spotlight' at the specified origin.

Args:
origin (Vector): Spawn position of the 'point_spotlight'.
color (Color): Color of the light.
**kwargs: Additional keywords arguments.
"""
spotlight = Spotlight.create('point_spotlight')
spotlight.origin = origin
spotlight.angles = kwargs.get('angle', SPOTLIGHT_ANGLE)
spotlight.color = color

spotlight.set_key_value_bool('disablereceiveshadows', False)
spotlight.set_key_value_float('HDRColorScale', 1)
spotlight.set_key_value_int('rendermode', 0)
spotlight.set_key_value_int('renderamt', 0)
spotlight.set_key_value_int('renderfx', 0)
spotlight.set_key_value_int(
'spotlightwidth', kwargs.get('width', SPOTLIGHT_DEFAULT_WIDTH))
spotlight.set_key_value_int(
'spotlightlength', kwargs.get('length', SPOTLIGHT_DEFAULT_LENGTH))

# Make sure the 'point_spotlight' spawns turned on.
spotlight.spawn_flags = 1
spotlight.spawn()
return spotlight


def create_item_spotlight(classname, index, origin):
# Is this a weapon without a set color?
if classname not in item_colors and 'weapon' in classname:
classname = 'weapon_default'

# Is there a color set for this item/weapon?
if classname in item_colors:
spotlight = create_spotlight(origin, item_colors[classname])
# Store the 'point_spotlight' instance in a dictionary, but tie it to
# the index of the specified item/weapon.
spotlight_pairs[index] = spotlight


@OnEntityOutput
def on_entity_output(output, activator, caller, value, delay):
# Items (item_battery, item_healthkit) send out 'OnPlayerTouch' whenever
# they are applied to a player, while weapons send out 'OnPlayerPickup'.
if output in ('OnPlayerTouch', 'OnPlayerPickup'):
index = caller.index

# Is there a 'point_spotlight' for this item?
if index in spotlight_pairs:
spotlight = spotlight_pairs[index]
# Delay the removal by a single frame to avoid crashing the server.
spotlight.delay(0, spotlight.remove)
# Remove the Spotlight instance from the dictionary.
del spotlight_pairs[index]


@EntityPreHook(
EntityCondition.equals_entity_classname('item_battery'), 'materialize')
def item_materialize_pre(stack_data):
"""Called when an item (item_battery, item_healthkit) becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)


@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
def weapon_materialize_pre(stack_data):
"""Called when a weapon (weapon_rpg, weapon_pistol) becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)


@OnEntitySpawned
def on_entity_spawned(base_entity):
"""Called when an entity is spawned."""
try:
index = base_entity.index
except ValueError:
return

classname = base_entity.classname

# Is this a grenade?
if 'npc_grenade_frag' in classname:
grenade_instances[index].start_pulsing()
return

# Is this a weapon?
if 'weapon' in classname:
# Does it have an owner? (player equipped it)
if base_entity.get_datamap_property_int('m_hOwnerEntity') != -1:
return

# When a player picks up a map placed weapon, another one gets spawned
# right away, but it's disabled. This line helps avoid spawning
# another 'point_spotlight' for the disabled weapon.
# More information: https://git.io/JfIFH
if base_entity.get_datamap_property_int('m_fEffects') == 128:
return

create_item_spotlight(classname, base_entity.index, base_entity.origin)


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity is removed."""
try:
index = base_entity.index
except ValueError:
return

# Does this entity have a 'point_spotlight' tied to it?
if index in spotlight_pairs:
spotlight_pairs[index].remove()
del spotlight_pairs[index]

By the way, you can quite easily add spotlights to other items, or even have a specific weapon have a different color by modifying the items_color dictionary:

Syntax: Select all

item_colors = {
'item_battery': Color(255, 255, 25),
'item_healthkit': Color(25, 255, 55),
'item_healthvial': Color(25, 255, 55),
'weapon_default': COLOR_WEAPON_DEFAULT,
# Make the ammo for shotguns have a white spotlight.
'item_box_buckshot': Color(255, 255, 255),
# Maybe a pink crossbow?
'weapon_crossbow': Color(245, 66, 224)
}
ImageImageImageImageImage
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Crossbow

Postby L'In20Cible » Fri Apr 24, 2020 6:26 am

VinciT wrote:I'm not exactly sure how those are made

They are probably parented particles: Particle effects!

VinciT wrote:

Syntax: Select all

...

Some suggestions:


VinciT wrote:

Syntax: Select all

weapons = [weapon.name for weapon in list(WeaponClassIter())]

# In case of a late plugin load, find the entities that need spotlights.
for base_entity in BaseEntityIter(set(list(item_colors.keys()) + weapons)):

There is a lot of redundant casts going on here. A straight-forward way would be:

Syntax: Select all

for base_entity in BaseEntityIter({weapon.name for weapon in WeaponClassIter()}.union(item_colors.keys())):



VinciT wrote:

Syntax: Select all

if base_entity.get_datamap_property_int('m_hOwnerEntity') != -1:



Syntax: Select all

if base_entity.owner_handle != -1:



VinciT wrote:

Syntax: Select all

if base_entity.get_datamap_property_int('m_fEffects') == 128:



Syntax: Select all

if base_entity.effects & EntityEffects.NODRAW:



VinciT wrote:

Syntax: Select all

light = TempEntity('Dynamic Light')

Every call could be optimized by globalizing that instance, as mentioned here and here.


VinciT wrote:

Syntax: Select all

light.create(RecipientFilter())



Syntax: Select all

light.create(*recipients)



VinciT wrote:

Syntax: Select all

spotlight.set_key_value_int('rendermode', 0)
spotlight.set_key_value_int('renderamt', 0)
spotlight.set_key_value_int('renderfx', 0)



Syntax: Select all

spotlight.render_mode = RenderMode.NORMAL
spotlight.render_amt = 0
spotlight.render_fx = RenderEffects.NONE



VinciT wrote:

Syntax: Select all

spotlight = spotlight_pairs[index]
# Delay the removal by a single frame to avoid crashing the server.
spotlight.delay(0, spotlight.remove)
# Remove the Spotlight instance from the dictionary.
del spotlight_pairs[index]

I would rather recommend the following:

Syntax: Select all

spotlight = spotlight_pairs.pop(index)
# Delay the removal by a single frame to avoid crashing the server.
spotlight.delay(0, spotlight.remove)[index]

So that the interpreter only have to locate index once to retrieve AND delete it rather than twice.



VinciT wrote:

Syntax: Select all

@EntityPreHook(
EntityCondition.equals_entity_classname('item_battery'), 'materialize')
def item_materialize_pre(stack_data):
"""Called when an item (item_battery, item_healthkit) becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)


@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
def weapon_materialize_pre(stack_data):
"""Called when a weapon (weapon_rpg, weapon_pistol) becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)



Syntax: Select all

@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
@EntityPreHook(
EntityCondition.equals_entity_classname('item_battery'), 'materialize')
def item_materialize_pre(stack_data):
"""Called when an item (item_battery, item_healthkit) becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)



VinciT wrote:

Syntax: Select all

if index in spotlight_pairs:
spotlight_pairs[index].remove()
del spotlight_pairs[index]

A try/except or a suppress context would be more efficient for the reasons mentioned here:

Another thing that have major impact on the performance is probably the code being "over-sanitized". Not sure if that is an existing expression, but basically what I mean is the data being internally validated multiple times. The following commit is probably a good example of it: ec3f424. Another good example would be hasattr. Take the following example:

Syntax: Select all

class Class:
something = 'something'
something_else = 'something else'
cls = Class()

t = time()
for i in range(100000000):
if hasattr(cls, 'something'):
something = getattr(cls, 'something')

print(time() - t)

Result:

Code: Select all

28.30333185195923


If you are looking for performance, then this is not a good implementation. The code above is pretty much the same logic as writting:

Syntax: Select all

for i in range(100000000):
try:
something = getattr(cls, 'something')
result = True
except AttributeError:
result = False
if result:
something = getattr(cls, 'something')


You basically tell Python:

Hey, go look into data if there is something, if there is, come back to let me know then go look for it again and bring it back.


Of course, it works. But if you are looking for performance then it is bad practice as you are technically asking Python to retrieve the value twice. What you should be telling Python instead is:

Hey, go look into data if there is something, if there is, bring it back or let me know if there was a problem.


Syntax: Select all

t = time()
for i in range(100000000):
try:
something = getattr(cls, 'something')
except AttributeError:
pass

print(time() - t)


Result:

15.540138721466064


Two birds one stone; you got your value at the same time of validating it so you greatly improved the performance. This just shows that everything has cost, and you should always try to find the fastest route if performance is an issue.

The same applies for dictionary, etc. If what you want to achieve can be done with a single interaction to an object then in most case will be more efficient.


VinciT wrote:Seems like a challenging issue. Here's hoping to having a solution for it one day.

This fixes it for me: DynamicHooks#4
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Crossbow

Postby Painkiller » Fri Apr 24, 2020 12:17 pm

I thank you both for your great work.

I picked out a code from the grenade that makes the trail randomly colored.
Maybe you could use it to tell the grenade what color it will flash to match the trail.

Syntax: Select all

# ../addons/source-python/plugins/supermod/modules/fragtrail.py

# ============================================================================
# >> IMPORTS
# ============================================================================
from colors import Color
from entities.entity import Entity
from listeners import OnEntityCreated
from listeners import OnEntityDeleted
from listeners import OnLevelInit
from entities.helpers import baseentity_from_pointer
import core
from effects.base import TempEntity
from players.entity import Player
import random
from engines.precache import Model
from engines.server import engine_server
from listeners.tick import Delay
from messages import SayText2
from entities.helpers import index_from_inthandle
core.console_message("\n[Supermod] Fragtrail loaded!\n\n")

global map_start
map_start = 1

@OnLevelInit
def level_init(map_name):
global map_start
map_start = 1
Delay(2.0, set_start, (0,))

def set_start(value):
global map_start
map_start = value

Delay(2.0, set_start, (0,))

@OnEntityCreated
def on_entity_created(base_entity):
global map_start
if map_start == 0:
try:
entity = Entity(base_entity.index)
except ValueError:
return
if entity.classname == "npc_grenade_frag":
Delay(0.1,changeTrail, (entity,))

def changeTrail(entity):
r = random.randint(0,255)
g = random.randint(0,255)
b = random.randint(0,255)
a = 255
color = r + (g << 8) + (b << 16) + (a << 24)
if color >= 2**31: color -= 2**32
player = Player(index_from_inthandle(entity.owner_handle))
try:
ent = entity.main_glow
except:
return
glow = Entity(index_from_inthandle(ent))
glow.model = Model("sprites/glow01.spr")
glow.spawn()
glow.set_property_int("m_clrRender", color)
ent = entity.glow_trail
glow_trail = Entity(index_from_inthandle(ent))
glow_trail.set_property_int("m_clrRender", color)


I hope it will help.

Thanks in advance.

Edit: I noticed an error but it is not in the logs.
Many plugins no longer load, weapons do not shoot.
It happens after using the mapchanged
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Crossbow

Postby VinciT » Sat Apr 25, 2020 12:24 am

L'In20Cible wrote:They are probably parented particles: Particle effects!
If they're unique particles, there's not much we can do to spawn them. From what I've noticed - only CSGO has the capability to precache custom particles without using a custom map.

L'In20Cible wrote:There is a lot of redundant casts going on here. A straight-forward way would be:

Syntax: Select all

for base_entity in BaseEntityIter({weapon.name for weapon in WeaponClassIter()}.union(item_colors.keys())):
Oh wow, this is so clean.

L'In20Cible wrote:

Syntax: Select all

if base_entity.owner_handle != -1:
I had no idea this property was moved to BaseEntity. Good to know!

L'In20Cible wrote:

Syntax: Select all

if base_entity.effects & EntityEffects.NODRAW:
This is what I had when I originally wrote that part, but for some reason it wouldn't work properly. I've added a single frame delay to the OnEntitySpawned listener, and everything's working fine now. I'm guessing not all properties are set when the listener fires.

L'In20Cible wrote:Every call could be optimized by globalizing that instance, as mentioned here and here.
Does this mean I should create only one instance or one per unique effect? As in, one for the bolt, another for the grenade?

Syntax: Select all

dynamic_light_ball = TempEntity('Dynamic Light')
dynamic_light_bolt = TempEntity('Dynamic Light')
dynamic_light_frag = TempEntity('Dynamic Light')
# Or..
dynamic_light = TempEntity('Dynamic Light')
With arguments of course.
Edit: Now that I think about it, it makes sense to make one for each of the unique effects, that way there's almost no need to change any arguments apart from the origin. I'll add that in tomorrow.

L'In20Cible wrote:

Syntax: Select all

@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
@EntityPreHook(
EntityCondition.equals_entity_classname('item_battery'), 'materialize')
def item_materialize_pre(stack_data):
"""Called when an item (item_battery, item_healthkit) becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)
I've tried this before and I kept getting errors. But now all of a sudden it works? I must've been doing something wrong.

L'In20Cible wrote:A try/except or a suppress context would be more efficient for the reasons mentioned here
So it's better to ask for forgiveness rather than permission in Python? :tongue: The performance difference in your examples is actually nuts.

L'In20Cible wrote:This fixes it for me: DynamicHooks#4
You've actually done it, nice! Can't wait for the changes to come to SP.

Painkiller wrote:I picked out a code from the grenade that makes the trail randomly colored.
Maybe you could use it to tell the grenade what color it will flash to match the trail.
Sure thing, give this a shot:

Syntax: Select all

# ../lights/lights.py

# Python
import random
from colorsys import hsv_to_rgb

# Source.Python
from colors import Color
from effects.base import TempEntity
from effects.hooks import TempEntityPreHook
from entities.constants import EntityEffects, RenderMode, RenderEffects
from entities.dictionary import EntityDictionary
from entities.entity import Entity, BaseEntity
from entities.helpers import index_from_pointer, index_from_inthandle
from entities.hooks import EntityPreHook, EntityCondition
from filters.entities import BaseEntityIter
from filters.weapons import WeaponClassIter
from listeners import OnEntityOutput, OnEntitySpawned, OnEntityDeleted
from listeners.tick import Delay
from mathlib import QAngle
from stringtables import string_tables


class ColorEx(Color):
"""Extended Color class."""

def __init__(self, r, g, b, a=255):
super().__init__(r, g, b, a)

self.raw = r + (g << 8) + (b << 16) + (a << 24)
if self.raw >= 2**31: self.raw -= 2**32


# Colors when the 'prop_combine_ball' bounces and explodes.
COLOR_BALL_BOUNCE = Color(0, 191, 255)
COLOR_BALL_EXPLODE = Color(170, 0, 255)
# Color when the 'crossbow_bolt' bounces.
COLOR_BOLT_BOUNCE = Color(255, 88, 0)
# Colors when the 'npc_grenade_frag' pulses and explodes.
COLOR_FRAG_PULSE = ColorEx(0, 255, 88)
COLOR_FRAG_EXPLODE = ColorEx(0, 255, 88)
# Should the grenade color be random? (0 - no, 1 - yes)
COLOR_FRAG_RANDOM = 1
# Default color for weapons.
COLOR_WEAPON_DEFAULT = Color(255, 25, 0)


# How wide should the 'point_spotlight' beam be?
SPOTLIGHT_DEFAULT_WIDTH = 20
# How tall should the 'point_spotlight' beam be?
SPOTLIGHT_DEFAULT_LENGTH = 300


item_colors = {
'item_battery': Color(255, 255, 25),
'item_healthkit': Color(25, 255, 55),
'item_healthvial': Color(25, 255, 55),
'weapon_default': COLOR_WEAPON_DEFAULT
}


spotlight_pairs = {}
# Angle to make the 'point_spotlight' point upwards.
SPOTLIGHT_ANGLE = QAngle(-90, 0, 0)


# Globalized instances of temporary entities.
light_bolt_bounce = TempEntity('Dynamic Light', radius=200, decay=175,
life_time=3, exponent=8, color=COLOR_BOLT_BOUNCE)

light_ball_bounce = TempEntity('Dynamic Light', radius=200, decay=150,
life_time=3, exponent=8, color=COLOR_BALL_BOUNCE)
light_ball_explode = TempEntity('Dynamic Light', radius=600, decay=600,
life_time=1, exponent=8, color=COLOR_BALL_EXPLODE)

light_frag_pulse = TempEntity('Dynamic Light', radius=200, decay=280,
life_time=0.6, exponent=8, color=COLOR_FRAG_PULSE)
light_frag_explode = TempEntity('Dynamic Light', radius=400, decay=560,
life_time=0.6, exponent=8, color=COLOR_FRAG_EXPLODE)



class Grenade(Entity):
"""Extended Entity class."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)

self.glow = BaseEntity(index_from_inthandle(self.main_glow))
self.trail = BaseEntity(index_from_inthandle(self.glow_trail))
self.change_trail_color(COLOR_FRAG_PULSE)

def start_pulsing(self):
"""Starts looping the `self.pulse()` function."""
self.repeat(self.pulse).start(interval=0.75)

def pulse(self):
"""Creates a dynamic light effect at the current origin."""
# Should the color be random?
if COLOR_FRAG_RANDOM:
color = random.choice(grenade_colors)
# Change the color of the glow and trail.
self.change_trail_color(color)
# Change the color of the dynamic light effect.
light_frag_pulse.color = color

# Adjust the origin of the dynamic light effect and create it.
light_frag_pulse.origin = self.origin
light_frag_pulse.create()

def boom(self):
"""Creates a dynamic light effect upon detonation at the current
origin."""
if COLOR_FRAG_RANDOM:
light_frag_explode.color = random.choice(grenade_colors)

light_frag_explode.origin = self.origin
light_frag_explode.create()

def change_trail_color(self, color):
"""Changes the color of the parented sprite and spritetrail."""
self.glow.set_network_property_int('m_clrRender', color.raw)
self.trail.set_network_property_int('m_clrRender', color.raw)


def get_pretty_colors(amount):
"""Returns a list of vibrant colors.

Args:
amount (int): How many colors should be generated?

Returns:
list of ColorEx: A list containing ColorEx instances.
"""
colors = []
step = 1 / amount

for hue in range(0, amount):
colors.append(
ColorEx(*(int(255 * x) for x in hsv_to_rgb(step * hue, 1, 1))))

return colors


# List of vibrant colors.
grenade_colors = get_pretty_colors(amount=25)
# Dictionary that stores 'npc_grenade_frag' instances.
grenade_instances = EntityDictionary(Grenade)


def load():
"""Called when the plugin is loaded."""
# In case of a late plugin load, find the entities that need spotlights.
for base_entity in BaseEntityIter(
{weapon.name for weapon in WeaponClassIter()}.union(
item_colors.keys())):

# Avoid weapons equipped by players.
if base_entity.owner_handle != -1:
continue

# Avoid disabled weapons.
if base_entity.effects & EntityEffects.NODRAW:
continue

create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)


def unload():
"""Called when the plugin is unloaded."""
# Go through all the 'point_spotlight' entities and remove them.
for spotlight in spotlight_pairs.values():
spotlight.remove()


# =============================================================================
# >> DYNAMIC_LIGHT: crossbow_bolt, prop_combine_ball, npc_grenade_frag
# =============================================================================
@EntityPreHook(
EntityCondition.equals_entity_classname('npc_grenade_frag'), 'detonate')
def detonate_pre(stack_data):
"""Called when an 'npc_grenade_frag' is about to explode."""
index = index_from_pointer(stack_data[0])

if index in grenade_instances:
grenade_instances[index].boom()


@TempEntityPreHook('EffectDispatch')
def effect_dispatch_pre(temp_entity, recipient_filter):
# Get the name of the effect.
effect_name = string_tables.EffectDispatch[temp_entity.effect_name_index]

# Did the 'prop_combine_ball' bounce?
if 'cball_bounce' in effect_name:
light_ball_bounce.origin = temp_entity.origin
# Delay the effect by a single frame, otherwise the server will crash!
temp_entity.entity.delay(0, light_ball_bounce.create)

# Or did it explode?
if 'cball_explode' in effect_name:
light_ball_explode.origin = temp_entity.origin
temp_entity.entity.delay(0, light_ball_explode.create)


@EntityPreHook(
EntityCondition.equals_entity_classname('crossbow_bolt'), 'start_touch')
def bolt_touch_pre(stack_data):
entity = Entity._obj(stack_data[0])

# Is this a bolt?
if 'crossbow_bolt' in entity.classname:
light_bolt_bounce.origin = entity.origin
light_bolt_bounce.create()

# Try not to freeze the server. (thank you L'In20Cible)
parent = entity.parent
if parent is not None:
parent.start_touch.skip_hooks(stack_data[1])
return False


# =============================================================================
# >> POINT_SPOTLIGHT: items and weapons
# =============================================================================
class Spotlight(Entity):
"""Modified Entity class for properly removing a 'point_spotlight'."""
caching = True

def remove(self):
"""Turns off the 'point_spotlight' before removing it."""
self.call_input('LightOff')
super().remove()


def create_spotlight(origin, color, **kwargs):
"""Creates a 'point_spotlight' at the specified origin.

Args:
origin (Vector): Spawn position of the 'point_spotlight'.
color (Color): Color of the light.
**kwargs: Additional keywords arguments.
"""
spotlight = Spotlight.create('point_spotlight')
spotlight.origin = origin
spotlight.angles = kwargs.get('angle', SPOTLIGHT_ANGLE)
spotlight.color = color

spotlight.render_mode = RenderMode.NORMAL
spotlight.render_amt = 0
spotlight.render_fx = RenderEffects.NONE
spotlight.set_key_value_bool('disablereceiveshadows', False)
spotlight.set_key_value_float('HDRColorScale', 1)
spotlight.set_key_value_int(
'spotlightwidth', kwargs.get('width', SPOTLIGHT_DEFAULT_WIDTH))
spotlight.set_key_value_int(
'spotlightlength', kwargs.get('length', SPOTLIGHT_DEFAULT_LENGTH))

# Make sure the 'point_spotlight' spawns turned on.
spotlight.spawn_flags = 1
spotlight.spawn()
return spotlight


def create_item_spotlight(classname, index, origin):
# Is this a weapon without a set color?
if classname not in item_colors and 'weapon' in classname:
classname = 'weapon_default'

# Is there a color set for this item/weapon?
if classname in item_colors:
# Check if there's a 'point_spotlight' tied to this index already.
try:
# If there is, remove it.
spotlight_pairs.pop(index).remove()
except KeyError:
pass
# Store the 'point_spotlight' instance in a dictionary, but tie it to
# the index of the specified item/weapon.
spotlight_pairs[index] = create_spotlight(
origin, item_colors[classname])


@OnEntityOutput
def on_entity_output(output, activator, caller, value, delay):
# Items (item_battery, item_healthkit) send out 'OnPlayerTouch' whenever
# they are applied to a player, while weapons send out 'OnPlayerPickup'.
if output in ('OnPlayerTouch', 'OnPlayerPickup'):
# Is there a 'point_spotlight' for this item/weapon?
try:
spotlight = spotlight_pairs.pop(caller.index)
except KeyError:
return

# Delay the removal by a single frame to avoid crashing the server.
spotlight.delay(0, spotlight.remove)


@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
@EntityPreHook(
EntityCondition.equals_entity_classname('item_battery'), 'materialize')
def materialize_pre(stack_data):
"""Called when an item or weapon becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)


@OnEntitySpawned
def on_entity_spawned(base_entity):
"""Called when an entity is spawned."""
try:
index = base_entity.index
except ValueError:
return

# Delay our checks for a single frame. Without this, checking for
# EntityEffects.NODRAW doesn't seem to work.
Delay(0, on_actual_entity_spawned, (index,))


def on_actual_entity_spawned(index):
try:
base_entity = BaseEntity(index)
except ValueError:
return

# Is this a grenade?
if 'npc_grenade_frag' in base_entity.classname:
grenade_instances[index].start_pulsing()
return

# Is this a weapon?
if 'weapon' in base_entity.classname:
# Does it have an owner? (player equipped it)
if base_entity.owner_handle != -1:
return

# When a player picks up a map placed weapon, another one gets spawned
# right away, but it's disabled. This line helps avoid spawning
# another 'point_spotlight' for the disabled weapon.
# More information: https://git.io/JfIFH
if base_entity.effects & EntityEffects.NODRAW:
return

create_item_spotlight(
base_entity.classname, index, base_entity.origin)


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity is removed."""
try:
index = base_entity.index
except ValueError:
return

# Does this entity have a 'point_spotlight' tied to it?
try:
spotlight_pairs.pop(index).remove()
except KeyError:
pass


Painkiller wrote:Edit: I noticed an error but it is not in the logs.
Many plugins no longer load, weapons do not shoot.
It happens after using the mapchanged
I tried to replicate the issue, but I'm unable to. This error does pop up from time to time:

Code: Select all

[SP] Caught an Exception:
Traceback (most recent call last):
  File "..\addons\source-python\packages\source-python\entities\hooks.py", line 227, in on_entity_created
    _waiting_entity_hooks.initialize(base_entity.index)
  File "..\addons\source-python\packages\source-python\entities\hooks.py", line 212, in initialize
    if hook.initialize(entity):
  File "..\addons\source-python\packages\source-python\entities\hooks.py", line 156, in initialize
    if not self.test_function(entity):
  File "..\addons\source-python\packages\source-python\entities\hooks.py", line 79, in <lambda>
    return lambda entity: entity.classname in classnames

RuntimeError: Access violation - no RTTI data!
But nothing breaks when it does, so I'm not sure what the issue could be.
Last edited by VinciT on Sat Apr 25, 2020 7:43 pm, edited 1 time in total.
ImageImageImageImageImage
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Crossbow

Postby Painkiller » Sat Apr 25, 2020 7:19 am

Server does not start any more.
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Crossbow

Postby L'In20Cible » Sat Apr 25, 2020 8:58 am

VinciT wrote:If they're unique particles, there's not much we can do to spawn them. From what I've noticed - only CSGO has the capability to precache custom particles without using a custom map.

I think it stopped working with the SteamPipes update to prevent exploits. The same kind of exploits that were possible with weapon scripts (a server could send .txt version of the .ctx modifying anything and the clients would prioritize them on any servers, etc.).


VinciT wrote:I had no idea this property was moved to BaseEntity. Good to know!

m_fEffects is also wrapped by BaseEntity as effects.


VinciT wrote:Edit: Now that I think about it, it makes sense to make one for each of the unique effects, that way there's almost no need to change any arguments apart from the origin. I'll add that in tomorrow.
Yes, that's the idea. TempEntity has been implemented with isolation in mind, so globalizing them and defining the values that never change at run-time only once on instantiation is definitely recommended. :smile:


VinciT wrote:I've tried this before and I kept getting errors. But now all of a sudden it works? I must've been doing something wrong.
More than likely, the entity hook decorators always been stackable because they return the original function.


VinciT wrote:So it's better to ask for forgiveness rather than permission in Python? :tongue: The performance difference in your examples is actually nuts.
Better I don't know... but faster! :grin:


VinciT wrote:But nothing breaks when it does, so I'm not sure what the issue could be.

It happens because of: #312


Painkiller wrote:Server does not start any more.

The problem is here:

Syntax: Select all

Delay(0, on_actual_entity_spawned, (base_entity,))

When a new map starts, some entities such as infodecal, info_null, etc. are freed as soon as they spawn. So, on the next frame, that base_entity is freed and interacting with it is undefined. It should rather pass the index and re-cast the entity in the callback:

Syntax: Select all

Delay(0, on_actual_entity_spawned, (index,))


def on_actual_entity_spawned(index):
try:
base_entity = BaseEntity(index)
except ValueError:
return

Or, use Entity.delay that will make sure to cancel the delay when the entity is removed.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Crossbow

Postby VinciT » Sat Apr 25, 2020 7:44 pm

L'In20Cible wrote:I think it stopped working with the SteamPipes update to prevent exploits. The same kind of exploits that were possible with weapon scripts (a server could send .txt version of the .ctx modifying anything and the clients would prioritize them on any servers, etc.).
That's a real shame. They're rather easy to make and even the most basic ones can look so much better than the effects we're stuck with.

L'In20Cible wrote:The problem is here:

Syntax: Select all

Delay(0, on_actual_entity_spawned, (base_entity,))

When a new map starts, some entities such as infodecal, info_null, etc. are freed as soon as they spawn. So, on the next frame, that base_entity is freed and interacting with it is undefined. It should rather pass the index and re-cast the entity in the callback:

Syntax: Select all

Delay(0, on_actual_entity_spawned, (index,))


def on_actual_entity_spawned(index):
try:
base_entity = BaseEntity(index)
except ValueError:
return

Or, use Entity.delay that will make sure to cancel the delay when the entity is removed.
I try to avoid creating Entity instances in listeners that are called often, so I went with the first option. This will probably change once your pull request gets merged. :grin:
I've updated the code in the post above.
ImageImageImageImageImage
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Crossbow

Postby L'In20Cible » Sun Apr 26, 2020 12:17 am

VinciT wrote:I try to avoid creating Entity instances in listeners that are called often, so I went with the first option. This will probably change once your pull request gets merged. :grin:
There is nothing wrong to use Entity over BaseEntity. Actually, you could be surprised:

Syntax: Select all

from time import time

from entities.constants import WORLD_ENTITY_INDEX
from entities.entity import BaseEntity
from entities.entity import Entity

t = time()
for i in range(10000000):
BaseEntity(WORLD_ENTITY_INDEX)
print(time() - t)

t = time()
for i in range(10000000):
Entity(WORLD_ENTITY_INDEX)
print(time() - t)


Code: Select all

8.113307476043701
2.934182643890381


Moreover, using an Entity.delay would generally be safer than delaying with an index now that I think about it. Here it is fine, because you have the necessary checks in your callback, but take the following example:

Syntax: Select all

from entities.entity import BaseEntity
from listeners.tick import Delay

def print_classname(index):
print(BaseEntity(index).classname)

awp = BaseEntity.create('weapon_awp')
Delay(5, print_classname, (awp.index,))
awp.remove()
Delay(2, BaseEntity.create, ('weapon_glock',))


This should prints weapon_awp, but prints weapon_glock because the awp was removed and its index re-used by a glock by the time the delay processed. This can cause unexpected issues if the callback is not prepared to validate it indeed received an awp index, etc. In fact, it would result into the light being created twice if the index is re-used by an entity that matches your classname filters because it would also spawn and trigger its own delay. While using Entity.delay the callback would never be called because the entity that initiated it was deleted therefore the delay was cancelled. And the great thing about it, is that you can safely pass the entity instance around:

Syntax: Select all

from entities.entity import Entity
from listeners.tick import Delay

def print_classname(entity):
print(entity.classname)

awp = Entity.create('weapon_awp')
awp.delay(5, print_classname, (awp,))
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Crossbow

Postby VinciT » Sun Apr 26, 2020 12:40 am

L'In20Cible wrote:There is nothing wrong to use Entity over BaseEntity. Actually, you could be surprised:
Whaaat?! I was under the impression that BaseEntity is much quicker to instantiate than Entity. How is this possible?

L'In20Cible wrote:This should prints weapon_awp, but prints weapon_glock because the awp was removed and its index re-used by a glock by the time the delay processed. This can cause unexpected issues if the callback is not prepared to validate it indeed received an awp index, etc. In fact, it would result into the light being created twice if the index is re-used by an entity that matches your classname filters because it would also spawn and trigger its own delay. While using Entity.delay the callback would never be called because the entity that initiated it was deleted therefore the delay was cancelled.
I've had my fair share of delay related bugs in the past. Which is why I usually try to use entity.delay() whenever I can. Guess I was wrong to avoid creating Entity instances within these listeners.
ImageImageImageImageImage
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Crossbow

Postby L'In20Cible » Sun Apr 26, 2020 12:43 am

VinciT wrote:Whaaat?! I was under the impression that BaseEntity is much quicker to instantiate than Entity. How is this possible?
It was most likely the case before, but now that Entity instances are internally cached, while BaseEntity aren't, the former is more efficient as it just grab an existing instance over allocating a new one after validating the index, etc. :tongue:
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Crossbow

Postby Painkiller » Sun Apr 26, 2020 7:02 am

Anything new yet on the effects of the weapons?
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Crossbow

Postby VinciT » Sun Apr 26, 2020 4:08 pm

Painkiller wrote:Anything new yet on the effects of the weapons?
Haven't messed around with it yet, but I think it could be recreated using two func_rotating entities and two env_spritetrails. I'll do some testing in a few hours and report back.
ImageImageImageImageImage
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Crossbow

Postby VinciT » Mon Apr 27, 2020 5:43 pm

I managed to recreate the effect.Image

Syntax: Select all

# ../weapon_effects/weapon_effects.py

# Source.Python
from colors import Color
from engines.precache import Model
from entities.constants import RenderMode, RenderEffects
from entities.entity import BaseEntity, Entity
from entities.hooks import EntityPreHook, EntityCondition
from listeners import OnEntityDeleted, OnEntityOutput
from mathlib import Vector


class ColorEx(Color):
"""Extended Color class."""

def __init__(self, r, g, b, a=255):
super().__init__(r, g, b, a)

self.raw = r + (g << 8) + (b << 16) + (a << 24)
if self.raw >= 2**31: self.raw -= 2**32

self.string = f'{r} {g} {b}'


# Sprite used for the spinning trails.
WEAPON_TRAIL_MODEL = Model('sprites/laser.vmt')
weapon_trails = {
'weapon_default': (ColorEx(255, 255, 255), ColorEx(0, 195, 255)),
'weapon_rpg': (ColorEx(255, 50, 20), ColorEx(255, 150, 50))
}


trail_pairs = {}


@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
def weapon_materialize_pre(stack_data):
"""Called when a weapon becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_weapon_trail(
base_entity.classname, base_entity.index, base_entity.origin)


@OnEntityOutput
def on_entity_output(output, activator, caller, value, delay):
if output == 'OnPlayerPickup':
try:
trail_pairs.pop(caller.index).remove()
except KeyError:
return


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity is being removed."""
try:
index = base_entity.index
except ValueError:
return

try:
trail_pairs.pop(index).remove()
except KeyError:
pass


def create_weapon_trail(classname, index, origin):
# Is this a weapon without a set color?
if classname not in weapon_trails and 'weapon' in classname:
classname = 'weapon_default'

# Is there a color set for this weapon?
if classname in weapon_trails:
# Check if there's a 'point_spotlight' tied to this index already.
try:
# If there is, remove it.
trail_pairs.pop(index).remove()
except KeyError:
pass

trail_pairs[index] = SpinningTrails(
origin, WEAPON_TRAIL_MODEL, colors=weapon_trails[classname])


class SpinningTrails:
"""Class used to create a spinning effect using 'func_rotating' and
'env_spritetrail' entities.

Args:
origin (Vector): Spawn position of the effect.
sprite (Model): Sprite for the 'env_spritetrail' entity.
**kwargs: Additional keyword arguments.

"""
def __init__(self, origin, sprite, **kwargs):
self.rotators = []
self.trails = []

self.create(origin, sprite, **kwargs)

def create(self, origin, sprite, **kwargs):
# Lift the effect a bit from the ground.
origin.z += 0.5
# How many trails should there be?
number_of_trails = kwargs.get('number_of_trails', 2)
# Rotation speed.
max_speed = kwargs.get('max_speed', 250)
# The radius of the first trail.
min_radius = kwargs.get('min_radius', 16)
# How far apart should the trails be?
spacing = kwargs.get('spacing', 4)
# Colors of the trails. If more than one color is specified, the colors
# will alternate. These should be instances of ColorEx.
colors = kwargs.get('colors', (ColorEx(255, 255, 255),))
colors_num = len(colors)
# Speed at which the trail fades (in seconds).
life_time = kwargs.get('life_time', 0.8)

for i in range(0, number_of_trails):
# No need to create more than two of these. One for each direction.
if i < 2:
rotator = Entity.create('func_rotating')
rotator.origin = origin
rotator.max_speed = max_speed
# How quickly does the 'func_rotating' speed up or slow down?
# (0 - very slowly, 100 - very quickly)
rotator.fan_friction = 100
# Change the direction of every other trail.
rotator.reversed = bool(i % 2)
# Set certain spawn flags for the entity.
# 1: Starts rotating as soon as it spawns.
# 64: Not solid.
rotator.spawn_flags = 1 + 64
rotator.spawn()
# Add the rotator to a list.
self.rotators.append(rotator)

# Was more than one color specified?
if colors_num > 1:
# Change the colors based on trail order.
color = colors[i % colors_num]
else:
color = colors[0]

trail = create_sprite_trail(
origin, sprite.path, 4, 1, color.string, life_time)
# Alternate the parent every other trail.
trail.set_parent(self.rotators[i % 2], -1)
# Offset the position of the 'env_spritetrail' on the
# 'func_rotating' entity. The amount of offset is based on the
# given 'min_radius', 'spacing', and order of the trail.
trail.set_property_vector(
'm_vecOrigin', Vector(min_radius + spacing * i, 0, 0))

# Add the 'env_spritetrail' to the list.
self.trails.append(trail)


def remove(self):
"""Removes both 'func_rotating' entities after a single frame delay."""
for rotator in self.rotators:
rotator.delay(0, rotator.remove)


def create_sprite_trail(
origin, sprite_path, start_width, end_width, color_str, life_time):
"""Creates an 'env_spritetrail' entity.

Args:
origin (Vector): Spawn position.
sprite_path (string): Path to the sprite material.
start_width (float): Starting width of the trail.
end_width (float): Ending width of the trail.
color_str (str): String containing the RGB values of the color.
(e.g. '255 255 255' for white)
life_time (float): How long does the trail last before it starts to
fade (in seconds)?

Returns:
Entity: The entity instance of the created 'env_spritetrail'.

"""
trail = Entity.create('env_spritetrail')
trail.sprite_name = sprite_path
trail.origin = origin

trail.life_time = life_time
trail.start_width = start_width
trail.end_width = end_width

trail.render_mode = RenderMode.TRANS_ADD
trail.render_amt = 255
trail.render_fx = RenderEffects.NONE
trail.set_key_value_string('rendercolor', color_str)
trail.spawn()

# Texture resolution of the trail.
trail.texture_res = 0.05
return trail

But it crashes after a while. I'm thinking it has something to do with the func_rotating entity. Don't use this yet, until I can figure out why it crashes. I'm just giving you a status update. :tongue:
ImageImageImageImageImage
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Crossbow

Postby Painkiller » Thu Apr 30, 2020 12:11 pm

For now, I'm doing very well.

Thank you
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Crossbow

Postby Painkiller » Wed May 06, 2020 7:58 am

This error appears.

Maybe it's an unimportant error.
Since it is running without any problems.

Code: Select all

[SP] Caught an Exception:
Traceback (most recent call last):
  File "../addons/source-python/packages/source-python/listeners/tick.py", line 80, in _tick
    self.pop(0).execute()
  File "../addons/source-python/packages/source-python/listeners/tick.py", line 161, in execute
    return self.callback(*self.args, **self.kwargs)
  File "../addons/source-python/plugins/lights/lights.py", line 339, in on_actual_entity_spawned
    if 'npc_grenade_frag' in base_entity.classname:

UnboundLocalError: local variable 'base_entity' referenced before assignment
DeaD_EyE
Member
Posts: 34
Joined: Wed Jan 08, 2014 10:32 am

Re: [HL2:DM] Crossbow

Postby DeaD_EyE » Thu May 07, 2020 10:16 am

The error is in the function `on_actual_entity_spawned`.
`base_entity` was not defined in the function, before you're accessing it.

You can reproduce the error with following code:

Syntax: Select all

def foo():
print(a) # a does not exist in local scope nor on module scope
a= 42


On module level the code raises a `NameError`, in the function it raises an `UnboundLocalError`.
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Crossbow

Postby daren adler » Tue May 12, 2020 9:39 pm

VinciT wrote:I managed to recreate the effect.Image

Syntax: Select all

# ../weapon_effects/weapon_effects.py

# Source.Python
from colors import Color
from engines.precache import Model
from entities.constants import RenderMode, RenderEffects
from entities.entity import BaseEntity, Entity
from entities.hooks import EntityPreHook, EntityCondition
from listeners import OnEntityDeleted, OnEntityOutput
from mathlib import Vector


class ColorEx(Color):
"""Extended Color class."""

def __init__(self, r, g, b, a=255):
super().__init__(r, g, b, a)

self.raw = r + (g << 8) + (b << 16) + (a << 24)
if self.raw >= 2**31: self.raw -= 2**32

self.string = f'{r} {g} {b}'


# Sprite used for the spinning trails.
WEAPON_TRAIL_MODEL = Model('sprites/laser.vmt')
weapon_trails = {
'weapon_default': (ColorEx(255, 255, 255), ColorEx(0, 195, 255)),
'weapon_rpg': (ColorEx(255, 50, 20), ColorEx(255, 150, 50))
}


trail_pairs = {}


@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
def weapon_materialize_pre(stack_data):
"""Called when a weapon becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_weapon_trail(
base_entity.classname, base_entity.index, base_entity.origin)


@OnEntityOutput
def on_entity_output(output, activator, caller, value, delay):
if output == 'OnPlayerPickup':
try:
trail_pairs.pop(caller.index).remove()
except KeyError:
return


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity is being removed."""
try:
index = base_entity.index
except ValueError:
return

try:
trail_pairs.pop(index).remove()
except KeyError:
pass


def create_weapon_trail(classname, index, origin):
# Is this a weapon without a set color?
if classname not in weapon_trails and 'weapon' in classname:
classname = 'weapon_default'

# Is there a color set for this weapon?
if classname in weapon_trails:
# Check if there's a 'point_spotlight' tied to this index already.
try:
# If there is, remove it.
trail_pairs.pop(index).remove()
except KeyError:
pass

trail_pairs[index] = SpinningTrails(
origin, WEAPON_TRAIL_MODEL, colors=weapon_trails[classname])


class SpinningTrails:
"""Class used to create a spinning effect using 'func_rotating' and
'env_spritetrail' entities.

Args:
origin (Vector): Spawn position of the effect.
sprite (Model): Sprite for the 'env_spritetrail' entity.
**kwargs: Additional keyword arguments.

"""
def __init__(self, origin, sprite, **kwargs):
self.rotators = []
self.trails = []

self.create(origin, sprite, **kwargs)

def create(self, origin, sprite, **kwargs):
# Lift the effect a bit from the ground.
origin.z += 0.5
# How many trails should there be?
number_of_trails = kwargs.get('number_of_trails', 2)
# Rotation speed.
max_speed = kwargs.get('max_speed', 250)
# The radius of the first trail.
min_radius = kwargs.get('min_radius', 16)
# How far apart should the trails be?
spacing = kwargs.get('spacing', 4)
# Colors of the trails. If more than one color is specified, the colors
# will alternate. These should be instances of ColorEx.
colors = kwargs.get('colors', (ColorEx(255, 255, 255),))
colors_num = len(colors)
# Speed at which the trail fades (in seconds).
life_time = kwargs.get('life_time', 0.8)

for i in range(0, number_of_trails):
# No need to create more than two of these. One for each direction.
if i < 2:
rotator = Entity.create('func_rotating')
rotator.origin = origin
rotator.max_speed = max_speed
# How quickly does the 'func_rotating' speed up or slow down?
# (0 - very slowly, 100 - very quickly)
rotator.fan_friction = 100
# Change the direction of every other trail.
rotator.reversed = bool(i % 2)
# Set certain spawn flags for the entity.
# 1: Starts rotating as soon as it spawns.
# 64: Not solid.
rotator.spawn_flags = 1 + 64
rotator.spawn()
# Add the rotator to a list.
self.rotators.append(rotator)

# Was more than one color specified?
if colors_num > 1:
# Change the colors based on trail order.
color = colors[i % colors_num]
else:
color = colors[0]

trail = create_sprite_trail(
origin, sprite.path, 4, 1, color.string, life_time)
# Alternate the parent every other trail.
trail.set_parent(self.rotators[i % 2], -1)
# Offset the position of the 'env_spritetrail' on the
# 'func_rotating' entity. The amount of offset is based on the
# given 'min_radius', 'spacing', and order of the trail.
trail.set_property_vector(
'm_vecOrigin', Vector(min_radius + spacing * i, 0, 0))

# Add the 'env_spritetrail' to the list.
self.trails.append(trail)


def remove(self):
"""Removes both 'func_rotating' entities after a single frame delay."""
for rotator in self.rotators:
rotator.delay(0, rotator.remove)


def create_sprite_trail(
origin, sprite_path, start_width, end_width, color_str, life_time):
"""Creates an 'env_spritetrail' entity.

Args:
origin (Vector): Spawn position.
sprite_path (string): Path to the sprite material.
start_width (float): Starting width of the trail.
end_width (float): Ending width of the trail.
color_str (str): String containing the RGB values of the color.
(e.g. '255 255 255' for white)
life_time (float): How long does the trail last before it starts to
fade (in seconds)?

Returns:
Entity: The entity instance of the created 'env_spritetrail'.

"""
trail = Entity.create('env_spritetrail')
trail.sprite_name = sprite_path
trail.origin = origin

trail.life_time = life_time
trail.start_width = start_width
trail.end_width = end_width

trail.render_mode = RenderMode.TRANS_ADD
trail.render_amt = 255
trail.render_fx = RenderEffects.NONE
trail.set_key_value_string('rendercolor', color_str)
trail.spawn()

# Texture resolution of the trail.
trail.texture_res = 0.05
return trail

But it crashes after a while. I'm thinking it has something to do with the func_rotating entity. Don't use this yet, until I can figure out why it crashes. I'm just giving you a status update. :tongue:


This is a great plugin if you could get it to stop crashing, It dont show in logs or sourcepython logs any errors, it just crashes about every 5 mins or so,,without log errors,i cant figer out why it crashes,sure hope you can fix it,,from what ive read,,u seem real good at this,,:)
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Crossbow

Postby Painkiller » Wed May 13, 2020 6:44 am

On Linux it works without problems

Return to “Plugin Requests”

Who is online

Users browsing this forum: No registered users and 30 guests