Creating and manually firing VMF-like output connections

Post Python examples to help other users.
User avatar
iPlayer
Developer
Posts: 567
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Creating and manually firing VMF-like output connections

Postby iPlayer » Thu Nov 19, 2015 4:15 am

Based on the code from this thread. Thanks to satoon101, L'In20Cible, Ayuto and necavi for helping me firgure out basics of Source.Python.

So I thought I might just post it here.

Imagine you have a VMF-like part of an output connection that connects output of one entity (caller) to the input of another entity (target) and applies some restriction to such connection (delay and maximum allowed times to fire).
The whole connection would look like
"OnPressed" "my_door01,Toggle,,0,-1"
In this case when something is pressed (func_button probably), entity with a targetname "my_door01" recieves an input Toggle with no parameter, zero delay (immediately) and this connection can be fired as many times as you want (-1).

And you need to execute the my_door01,Toggle,,0,-1 part of it.

More samples:
func_door,Open,,0,-1 - opens all func_door's on the map
func_door*,Open,,0,-1 - opens all regular doors (func_door) and rotating doors (func_door_rotating) on the map
!player,SetHealth,42,5,-1 - sets the health of the first found player to 42 with a delay of 5 seconds
*,SetHealth,1,0,1 - sets the health of everything (actually of those entities that support that) to 1, but this particular instance of a connection should only fire once a round.

There're several existing approaches:
1. Strip 'cheat' flag from ent_fire console command, execute it on a client, then put the flag back.
Pros: Easy, behaves the same exact way as any other usual entity IO call
Cons: Your activator and caller is the player you're executing this command on, you can't explicitly change it. Also, you can't set a maximum allowed times to fire, you'll need to track it somehow by yourself.
Notes: ent_fire uses a bit different format where input name is separated with a space from a rest of the connection part, so you must reformat the string.

2. Spawn a dummy cross-game entity like info_target, add output OnUser1 with the needed content (func_door,Open,,0,-1) and then fire FireUser1 on it. Don't forget to remove the dummy on the next tick.
Pros: Easy, behaves the same exact way as any other usual entity IO call
Cons: Your caller is this dummy. I'm not sure about activator though.

3. Take a journey of finding those entities by yourself. Then you are trying to actually call the input on each of those.
Pros: Customizable, full control, ability to exclude/include any entities, you can also define Gaben as your caller or activator.
Cons: You basically reinvent what Source engine does for you. No guarantee that it will find the same entities as the previous two approaches would.

So, for the third approach you need to...
Import:

Syntax: Select all

from entities.classes import server_classes
from entities.datamaps import FieldType
from events import Event
from filters.entities import BaseEntityIter
from filters.players import PlayerIter
from listeners.tick import tick_delays
from memory import make_object


Map expected input types to python classes (do it somewhere in a global namespace of your module):

Syntax: Select all

_input_types = {
FieldType.BOOLEAN: lambda arg: arg == '1', # So that '0' won't become True
FieldType.FLOAT: float,
FieldType.INTEGER: int,
FieldType.STRING: str,
FieldType.VOID: None,
}


Define [B]Fire class:[/B]

Syntax: Select all

class Fire:
def __call__(self, target_pattern, input_name, parameter=None, caller=None, activator=None):
"""Find target entities using the given pattern and try to call an input on each of them"""
targets = self._get_targets(target_pattern, caller, activator)
for target in targets:
self._call_input(target, input_name, parameter, caller, activator)

def _get_targets(self, target_pattern, caller, activator):
"""Return iterable of targets depending on given pattern, caller and activator."""
if target_pattern.startswith('!'):
return self._get_special_name_target(target_pattern, caller, activator)

filter_ = self._get_entity_filter(target_pattern, caller, activator)
return filter(filter_, BaseEntityIter())

def _get_special_name_target(self, target_pattern, caller, activator):
"""Find target by a special (starting with '!') target name."""
if target_pattern == "!self":
return (caller, )

if target_pattern == "!player":
for player in PlayerIter():
return (player, )
return ()

if target_pattern in ("!caller", "!activator"):
return (activator, )

def _get_entity_filter(self, target_pattern, caller, activator):
"""Return a filter that will be applied to all entities on the server."""
if target_pattern.endswith('*'):
def filter_(entity):
targetname = entity.get_key_value_string('targetname')
return (targetname.startswith(target_pattern[:-1]) or
entity.classname.startswith(target_pattern[:-1]))
return filter_

if not target_pattern:
return lambda entity: False

def filter_(entity):
targetname = entity.get_key_value_string('targetname')
return target_pattern in (targetname, entity.classname)
return filter_

def _get_input(self, target, input_name):
"""Return input function based on target and input name."""
for server_class in server_classes.get_entity_server_classes(target):
if input_name in server_class.inputs:
return getattr(
make_object(server_class._inputs, target.pointer), input_name)

return None

def _call_input(self, target, input_name, parameter, caller, activator):
"""Fire an input of a particular entity."""
input_function = self._get_input(target, input_name)

# If entity doesn't support the input, we don't work with this entity
if input_function is None:
return

caller_index = None if caller is None else caller.index
activator_index = None if activator is None else activator.index

# Check if type is unsupported, but we actually support all types that can possibly
# be passed as a string to input: int, float, bool, str
# TODO: Implement support for entity arguments (passed as a special name like !activator, !caller etc)
if input_function._argument_type not in _input_types:
return

type_ = _input_types[input_function._argument_type]

# Case: input does not require parameter
if type_ is None:
parameter = None

# Case: input does require parameter
else:
# Try to cast the parameter to the given type
try:
parameter = type_(parameter)

# We don't give up the target if the value can't be casted;
# Instead, we fire its input with a default value just like ent_fire does
except ValueError:
parameter = type_()

# Fire an input
input_function(parameter, caller_index, activator_index)


Define OutputConnection class:

Syntax: Select all

class OutputConnection:
def __init__(self, fire_func, destroy_func, string, caller=None, activator=None):
try:
target_pattern, input_name, parameter, delay, times_to_fire = string.split(',')
except ValueError:
raise ValueError("Invalid output connection string")

delay = max(0.0, float(delay))
times_to_fire = max(-1, int(times_to_fire))

self._fire_func = fire_func
self._destroy_func = destroy_func

self.target_pattern = target_pattern
self.input_name = input_name
self.parameter = parameter or None
self.delay = delay
self.times_to_fire = times_to_fire
self.caller = caller
self.activator = activator

self._delayed_callbacks = []
self._times_fired = 0

def reset(self):
"""Cancel all pending callbacks and set fire count to zero."""
for delayed_callback in self._delayed_callbacks:
try:
delayed_callback.cancel()
except KeyError:
continue

self._delayed_callbacks = []
self._times_fired = 0

def fire(self):
"""Fire this output connection."""
if self.times_to_fire > -1 and self._times_fired >= self.times_to_fire:
return

def callback():
self._fire_func(self.target_pattern, self.input_name, self.parameter, self.caller, self.activator)

if self.delay == 0.0:
callback()

else:
self._delayed_callbacks.append(tick_delays.delay(self.delay, callback))

def destroy(self):
"""Remove a reference to the connection from this lib and stop resetting connection on every round start."""
self._destroy_func(self)

def __str__(self):
return "OutputConnection('{0},{1},{2},{3},{4}')".format(
self.target_pattern, self.input_name, self.parameter or "", self.delay, self.times_to_fire
)


Prepare your methods:

Syntax: Select all

output_connections = []    # We will store connections here
fire = Fire() # callable that is used to call inputs when they're ready


Define your methods:

Syntax: Select all

def new_output_connection(string, caller=None, activator=None):
"""Create and register a new OutputConnection instance using given values."""
output_connection = OutputConnection(fire, destroy_output_connection, string, caller, activator)
output_connections.append(output_connection)
return output_connection

def destroy_output_connection(output_connection):
"""Remove connection reference from this lib thus stopping resetting the connection every round."""
output_connections.remove(output_connection)



Attach an event handler to round_start event (you may use Event decorator):

Syntax: Select all

@Event('round_start')
def on_round_start(game_event):
for output_connection in output_connections:
output_connection.reset()


Use it like that:

Syntax: Select all

connection = new_output_connection("my_door01,Toggle,,0,-1")
connection.fire()
connection.destroy()


Once you destroy your connection, you can still use it, but it won't reset when a round starts anymore.
If you don't need your connection, don't forget to destroy it, otherwise it won't be garbage collected.
User avatar
Doldol
Senior Member
Posts: 164
Joined: Sat Jul 07, 2012 7:09 pm
Location: Belgium

Postby Doldol » Thu Nov 19, 2015 4:50 am

What? Why all this code? Say I wanted to call Toggle on entity with classname "my_door01", I'd just find that entity (probably by iterating over all entities),

then do something like:

Syntax: Select all

entity.call_input("Toggle",False,0,-1)


And that'd be it, right? Forgive me, but since you put it as an example, I don't see the use/purpose of all your code.
Why would I ever end up specifying my arguments as a string anyway?
User avatar
iPlayer
Developer
Posts: 567
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Thu Nov 19, 2015 4:54 am

I'd just find that entity

It can be wildcarded targetname, it can be wildcarded classname, it can be both (say there's an entity called func_door_rot and your target pattern is func_door*), it can also be a special name (!player, !self, !activator, !caller).
That where the first part comes from.

Why would I ever end up specifying my arguments as a string anyway

You imported a list of such inputs from either directly a VMF-file or just some config.
You only have an input name and an argument as a string. You need to use that to get actual corresponding Python value, otherwise call_input will fail.

Thirdly, I didn't know .call_input natively supports delays and counts how many times it was fired. Actually, how would it count that?
User avatar
satoon101
Project Leader
Posts: 2436
Joined: Sat Jul 07, 2012 1:59 am

Postby satoon101 » Thu Nov 19, 2015 5:04 am

If you want to find all func_door* entities, just use EntityIter but pass False as the exact_match parameter:

Syntax: Select all

for entity in EntityIter('func_door', False):
entity.call_input('Open')

# Or, since we already have CBaseDoor in our data
entity.open()
Image
User avatar
iPlayer
Developer
Posts: 567
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Thu Nov 19, 2015 5:08 am

satoon101 wrote:If you want to find all func_door* entities, just use EntityIter but pass False as the exact_match parameter:

Syntax: Select all

for entity in EntityIter('func_door', False):


It will only work for classname, not for targetname. And it doesn't use .startswith, it uses in statement, that's a big difference:

Syntax: Select all

if not self.exact_match and check_name in entity.classname

(filters/entity.py, 61)

That said, with EntityIter's implementation of exact_match=False the following code

Syntax: Select all

for entity in EntityIter('light', False)
will also include extra entities like point_spotlight but will also miss an entity with a targetname of light01.
Such behavior differs from what ent_fire would do.
User avatar
iPlayer
Developer
Posts: 567
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Thu Nov 26, 2015 6:25 pm

Some of the entities don't get networked thus my first post didn't work with them. I included some additional code (that's even more code!), should work fine now. I hope some of the Entity functionality would move to BaseEntity so that I can reduce the amount of my code again.
See also http://forums.sourcepython.com/showthread.php?998-Accessing-math_counters-from-SP

Doldol, this issue with non-networked entities also affected your

Syntax: Select all

entity.call_input("Toggle",False,0,-1)
User avatar
Doldol
Senior Member
Posts: 164
Joined: Sat Jul 07, 2012 7:09 pm
Location: Belgium

Postby Doldol » Thu Nov 26, 2015 6:45 pm

iPlayer wrote:Thirdly, I didn't know .call_input natively supports delays and counts how many times it was fired. Actually, how would it count that?


AFAIK it does indeed support delaying (You are still passing the same arguments to outputs, if delay is included in them, you can obviously use it.), but I don't know of it keeping track of how many times it was fired (I never said it did.), if they're stored as a property on the entity, then sure you can obviously retrieve that using the get_prop_<type> method of the Entity (etc) class

Why are you so dead-set on 100% emulating ent_fire/es_fire anyway, it does a bunch of things you almost never need/want? I get that is what you're trying to do here, but are you sure that is actually what you truely want and need? I am happy I don't need to mess with es.fire anymore, and that more streamlined functions are available.

iPlayer wrote:Some of the entities don't get networked thus my first post didn't work with them. I included some additional code (that's even more code!), should work fine now. I hope some of the Entity functionality would move to BaseEntity so that I can reduce the amount of my code again.
See also http://forums.sourcepython.com/showthread.php?998-Accessing-math_counters-from-SP

Doldol, this issue with non-networked entities also affected your
[PYTHON]entity.call_input("Toggle",False,0,-1)[/PYTHON]


I'd say it doesn't really affect the function (Which I'm guessing you're pointing out), it's more that in this case this class is obviously not made to handle non-networked entities.
User avatar
iPlayer
Developer
Posts: 567
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Thu Nov 26, 2015 8:11 pm

but I don't know of it keeping track of how many times it was fired


Yes, later I learned about delays, but haven't got around to actually removing that tick_delays part. Regarding to keeping track of how many times it was fired, well, here you need some instance to track. Because you can't judge if this is the same output connection or not only by its arguments. Say, two entities have exact same output connections that needed to fire 5 times each. You need to track how many times they fire each, not in total.

Why are you so dead-set on 100% emulating ent_fire/es_fire anyway

I'm not dead-set on emulating ent_fire, I'm dead-set on emulating connections in format that they are saved to VMF-files.

As said, I import a list of connections from a JSON-file:

Syntax: Select all

"OnJailRoundStart": [
"floor1_relay,Disable,,0,-1",
"basketball_start,Trigger,,0,-1",
"soccer_start,Trigger,,0,-1"
],


And this json is formed based on Valve Map File (VMF):

Code: Select all

entity
{
    "id" "106798"
    "classname" "jb_settings"
    connections
    
{
        "OnJailRoundStart" "floor1_relay,Disable,,0,-1"
        "OnJailRoundStart" "basketball_start,Trigger,,0,-1"
        "OnJailRoundStart" "soccer_start,Trigger,,0,-1"
    }
    "origin" "-248 48 8"
}



And this file is created in Valve Hammer Editor:
Image

The map creator just expects these connections to work as if they were real, existing Source connections. But they are fake. I need to fire them manually when needed.

And if a map creator that makes a map for my plugin decides to do this
Image
Do you think he will be thankful if I tell him "Well, you know, I don't actually support wildcarded targetnames, so you'd better referenced all your jail_door's from jail_door1 to jail_door64 manually".
Or just simply "Sorry, my plugin behaves a bit differently from how you would expect your connections to work as if they were real, so here's a list of differences you need to learn: differences.txt (5.4KB)"

I'd say it doesn't really affect the function (Which I'm guessing you're pointing out), it's more that in this case this class is obviously not made to handle non-networked entities.

You're right, Entity class is made for networked entities. But some of the Entity functionality actually works for non-networked entities too. That's why they'd better moved this functionality from Entity to BaseEntity. But before they do that, code keeps on being large.
User avatar
Doldol
Senior Member
Posts: 164
Joined: Sat Jul 07, 2012 7:09 pm
Location: Belgium

Postby Doldol » Fri Nov 27, 2015 7:46 am

iPlayer wrote:
Yes, later I learned about delays, but haven't got around to actually removing that tick_delays part. Regarding to keeping track of how many times it was fired, well, here you need some instance to track. Because you can't judge if this is the same output connection or not only by its arguments. Say, two entities have exact same output connections that needed to fire 5 times each. You need to track how many times they fire each, not in total.

Why are you so dead-set on 100% emulating ent_fire/es_fire anyway

I'm not dead-set on emulating ent_fire, I'm dead-set on emulating connections in format that they are saved to VMF-files.

As said, I import a list of connections from a JSON-file:

Syntax: Select all

"OnJailRoundStart": [
"floor1_relay,Disable,,0,-1",
"basketball_start,Trigger,,0,-1",
"soccer_start,Trigger,,0,-1"
],

...


I see, but if you're already preprocessing the VMF, go a step further, make the JSON hold the structure completely mapped into the JSON file?

Like:

Syntax: Select all

"OnJailRoundStart":{
"floor1_relay":["Disable",False,0,-1],
"basketball_start":["Trigger",False,0,-1],
"soccer_star":["Trigger",False,0,-1]
},


But I do understand the whole undertaking now :)
User avatar
iPlayer
Developer
Posts: 567
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Fri Nov 27, 2015 8:28 am

Well, thanks for this idea, haven't thought of it. In fact, I only finished switching to JSON yesterday, before that I used to use the KeyValues format for my transitional files. The reasoning behind that was "my precompiler import VMF as KeyValues, let's keep it simple and export KeyValues too, eh".

And in KeyValues such structure would look like hell. To make a list in KeyValues you do

Code: Select all

"listname" "element1"
"listname" "element2"
"listname" "element3" 


instead of beautiful JSON-ish

Code: Select all

"listname": [
    
"element1",
    
"element2",
    
"element3"


So I would kill myself if you suggested this idea 2 days ago, when I still was on KeyValues.

But now that seems pretty neat. The only thing is that I won't be able to get that false value when I precompile the map. Simply because I don't know the right type to cast to. Precompiler could look it up in FGD, but Source.Python does that dynamically, relying on the engine (SP gives me input_function._argument_type).

So I will end up using something like this

Code: Select all

"OnJailRoundStart": [
    [
"logic_branch""SetValueTest""0"0.0, -1],
], 

I still can cast the delay to float and the times-to-fire to int, assuming that Hammer always produces valid values.

This also solves the problem proposed by Satoon.

Thanks again.


EDIT: After a second thought, I'd probably extend your idea to this format

Code: Select all

"OnJailRoundStart": [
    {
        
"target_pattern":   "logic_branch",
        
"input_name":       "SetValueTest",
        
"parameter_raw":    "0",
        
"parameter":        null,   
        
"delay":            0.0,
        
"times_to_fire":    -1
    
}

Because I feel that a list itself should not represent an object. Both Python and JS support lists containing various item types, but I think in this case it's best to represent the connection itself with a dictionary.
My pattern here is: dicts for objects, lists for collections of objects.
User avatar
iPlayer
Developer
Posts: 567
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Fri Nov 27, 2015 3:23 pm

I actually misread a lot in your suggestion, Doldol, I was sleepy (and I am), and I made some significant changes to my previous post.

Main things changed:

1.
I don't get the point of taking the target pattern out of the rest of the connection:

Syntax: Select all

"floor1_relay": ["Disable",False,0,-1]

To me, this is not needed. Instead it is

Syntax: Select all

["floor1_relay", "Disable", False, 0, -1]

2.
I didn't see you were passing that False parameter to inputs like Disable and Trigger. They're void actually. So I changed the example to use logic_branch's SetValueTest instead.

3.
Explained the format I will most likely end up using (end of the post).
User avatar
Doldol
Senior Member
Posts: 164
Joined: Sat Jul 07, 2012 7:09 pm
Location: Belgium

Postby Doldol » Sat Nov 28, 2015 11:53 am

iPlayer wrote:I actually misread a lot in your suggestion, Doldol, I was sleepy (and I am), and I made some significant changes to my previous post.

Main things changed:

1.
I don't get the point of taking the target pattern out of the rest of the connection:

Syntax: Select all

"floor1_relay": ["Disable",False,0,-1]

To me, this is not needed. Instead it is

Syntax: Select all

["floor1_relay", "Disable", False, 0, -1]

2.
I didn't see you were passing that False parameter to inputs like Disable and Trigger. They're void actually. So I changed the example to use logic_branch's SetValueTest instead.

3.
Explained the format I will most likely end up using (end of the post).


No problem, Sleep deprivation sucks. I'm glad I could be of assistance.

1: I was thinking of it in terms of maybe wanting to fire multiple in/outputs to one entity. It's also more consistent to the eventually executed logic (execute input x on entity y).

2: I was using that as an example of the syntax, I just copied it over from what you provided, None (closes to void in Python) isn't supported by JSON, and is generally represented as an empty string. (But that's still a work-around). Maybe you could use a dictionary as keywords, and not provide the keyword for one that's void. (Edit: nvm you ended up doing something like that.)

Return to “Code examples / Cookbook”

Who is online

Users browsing this forum: No registered users and 1 guest