Page 1 of 1
Questions on working with entities IO calls and keyvalues
Posted: Mon Nov 16, 2015 3:24 am
by iPlayer
Hello, Source.Python community!
I'm currently (by currently I mean at 6:00 am) porting my ES plugin to SP. Refactoring it, too. Everything seems to be OK, but still I've got 2 questions:
1. Say I have entities.entity.Entity instance or just entity index. How do I gain access to its keyvalues? In ES I did something like this
Syntax: Select all
int(es.entitygetvalue(index, 'mykey'))
Note that
mykey is not present in FGD and it's a non-standard key. But map creators can still set it.
2. Help iPlayer find an alternative to this function
Syntax: Select all
def fire(*args):
args = list(args)
if not (isinstance(args[0], int) and es.exists('userid', args[0])):
args.insert(0, es.getuserid())
args = args[:5]
es.fire(*args)
Another version of this (before I figured out my Windows SRCDS actually does not crash from es.fire) ended this way:
Syntax: Select all
es.server.queuecmd('es_xfire %s' % ' '.join(map(str, args)))
To shorten this up, I have a userid/player index and I have a string that he should fire.
I don't have access (index) to entity(es) that I fire input of and I'm not sure such entity exists.
Valid code:
Syntax: Select all
fire(userid, '*', 'SetParent', '!activator')
I just have a string to fire. How do I do this?
Sorry if this been resolved somewhere, but even your Wiki can't keep up with all the changes that are going on in Source.Python.
Posted: Mon Nov 16, 2015 4:34 am
by L'In20Cible
iPlayer wrote:Hello, Source.Python community!
Welcome to the forums!
iPlayer wrote:1. Say I have entities.entity.Entity instance or just entity index. How do I gain access to its keyvalues? In ES I did something like this
Syntax: Select all
int(es.entitygetvalue(index, 'mykey'))
Note that
mykey is not present in FGD and it's a non-standard key. But map creators can still set it.
In SP, you will need to explicitly type-cast the keyvalue you are getting/setting the value from. For example, you can get the model of an entity using the following methods
Syntax: Select all
model = entity.get_keyvalue_string('model')
However, to make our life easier, a lot are dynamically mapped as properties via their respective
data files.iPlayer wrote:2. Help iPlayer find an alternative to this function
Syntax: Select all
def fire(*args):
args = list(args)
if not (isinstance(args[0], int) and es.exists('userid', args[0])):
args.insert(0, es.getuserid())
args = args[:5]
es.fire(*args)
Another version of this (before I figured out my Windows SRCDS actually does not crash from es.fire) ended this way:
Syntax: Select all
es.server.queuecmd('es_xfire %s' % ' '.join(map(str, args)))
To shorten this up, I have a userid/player index and I have a string that he should fire.
I don't have access (index) to entity(es) that I fire input of and I'm not sure such entity exists.
Valid code:
Syntax: Select all
fire(userid, '*', 'SetParent', '!activator')
I just have a string to fire. How do I do this?
You can use the following method:
Syntax: Select all
entity.call_input(name, value=None, caller=None, activator=None)
But as for SetParent, I would rather recommend you to use the following method:
Syntax: Select all
entity.set_parent(other_entity, -1) # -1 being the attachment index, see studio package for more info.
iPlayer wrote:Sorry if this been resolved somewhere, but even your Wiki can't keep up with all the changes that are going on in Source.Python.
The best documentation is the source code!
Posted: Mon Nov 16, 2015 5:20 am
by iPlayer
Thanks for your reply. I know about the source code, and I must say it's very well documented. But all my researches stop as soon as it comes to C++ implementation.
I got the first answer. But I'm not sure about your second answer:
Syntax: Select all
entity.call_input(name, value=None, caller=None, activator=None)
As far I get it,
entity here is the actual entities.entity.Entity which input of should be fired.
But as I said, I don't have access to that entity. And by that I mean:
1. Target can actually be wildcarded, can turn out to be multiple entities, can be some special target (!caller, !activator, !self, !player etc) or it even can be a classname (func_door).
2. Maybe (just maybe) entity doesn't exist. Or maybe it does - I shouldn't care.
Maybe you'll improve my idea or find a better way etc. My idea as it is:
1. Parse *.vmf (Valve Map File) and represent (vmf is actually a keyvalues-file just like vdf, vmt etc) it as a python data structure
2. Find some special entities that I support (say
jb_game_control)
3. Save its connections:
Syntax: Select all
[
# entities list
# ...
{
"id": "577",
"classname": "jb_game_control",
"game": "singlewinner_koth",
# some unrelated key-values
# ...
"targetname": "koth_game_control",
"connections": {
"OnStart": [
"koth_spotlight,LightOn,,0,-1",
"koth_music,Volume,10,0,-1",
"koth_timer,Enable,,0,-1",
],
"OnEnd": [
"koth_spotlight,LightOff,,0,-1",
"koth_music,Volume,0,0,-1",
"koth_timer,Disable,,0,-1",
],
},
"origin": "392 488 8",
# more unrelated key-values
# ...
}
]
4. When the moment comes, emulate firing "OnStart" and "OnEnd" outputs by actually taking
"koth_spotlight,LightOn,,0,-1" and sending it to functions that act like
es.fireSee, I work with some sort of virtual entities that don't even make it to BSP file, but are still loaded by my plugin from VMF. But they store real outputs to real, existing entities.
The only way I see
entity.call_input can be used here is manually finding entities that been targeted in an output, creating entities.entity.Entity for each of these entities and finally calling .call_input().
But the way
es_fire does it (the
ent_fire way) is way more natural in my case - just because I won't be recreating what is already done by Source engine.
Maybe there's an option to force execute
ent_fire from client without turning sv_cheats on?
Posted: Mon Nov 16, 2015 8:50 am
by iPlayer
Got it! This will work:
Syntax: Select all
player.client_command("ent_fire func_door Open", server_side=True)
player.client_command("ent_fire * SetParent !activator", server_side=True) # Nasty stuff
Posted: Mon Nov 16, 2015 1:21 pm
by iPlayer
Okay, here's what I came up with
Syntax: Select all
def fire(source, target_pattern, input_name, parameter=None):
if target_pattern.startswith('!'):
if target_pattern not in ("!caller", "!activator", "!self"):
return
if source is None:
return
targets = (source, )
else:
targets = []
if target_pattern.endswith('*'): # Name searching supports trailing * wildcards only
target_pattern = target_pattern[:-1]
check_whole_string = False
else:
if not target_pattern:
return
check_whole_string = True
if check_whole_string:
for entity in EntityIter():
if target_pattern in (getattr(entity, 'target_name', ""), entity.classname):
targets.append(entity)
else:
for entity in EntityIter():
if (getattr(entity, 'target_name', "").startswith(target_pattern) or
entity.classname.startswith(target_pattern)):
targets.append(entity)
source_index = None if source is None else source.index
for target in targets:
try:
target.call_input(input_name, value=parameter, caller=source_index)
except ValueError:
continue
Tested with
fire(None, "func_door", "Open") - opens func_door's
fire(None, "func_door*", "Open") - this one will also open func_door_rotating's
fire(player, "!self", "SetHealth", "100")Edit: fixed bug with '*' target (all entities) not working properly.
Posted: Mon Nov 16, 2015 2:21 pm
by iPlayer
Now what I am actually going to use in my plugin
Syntax: Select all
from filters.entities import EntityIter
from listeners.tick import tick_delays
# fire() definition from the above message
class OutputConnection:
def __init__(self, destroy_func, string):
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._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._delayed_callbacks = []
self._times_fired = 0
def reset(self):
for delayed_callback in self._delayed_callbacks:
try:
delayed_callback.cancel()
except KeyError:
continue
self._delayed_callbacks = []
self._times_fired = 0
def fire(self):
if self.times_to_fire > -1 and self._times_fired >= self.times_to_fire:
return
self._times_fired += 1
if self.delay == 0.0:
fire(None, self.target_pattern, self.input_name, self.parameter)
else:
def callback():
fire(None, self.target_pattern, self.input_name, self.parameter)
self._delayed_callbacks.append(tick_delays.delay(self.delay, callback))
def destroy(self):
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
)
output_connections = []
def new_output_connection(string):
output_connection = OutputConnection(destroy_output_connection, string)
output_connections.append(output_connection)
return output_connection
def destroy_output_connection(output_connection):
output_connections.remove(output_connection)
def on_round_start(game_event): # Event handler for "round_start"
for output_connection in output_connections:
output_connection.reset()
Usage:
Syntax: Select all
connection = new_output_connection("koth_spotlight,LightOn,,0,-1")
connection.fire()
connection.destroy()
You can store this
connection across multiple rounds without destroying. It will track remaining number of times to fire and will reset on each round_start.
Posted: Wed Nov 18, 2015 7:21 pm
by iPlayer
I missed that when you call Entity.call_input with some particular value, the value should match the expected type of the input. For SetHealth it's int, for SetHUDVisibility it's bool.
So basically, the above class will only work with either no parameters (,,) or string parameters.
Is there no other option but using es_fire? It kinda sucks to keep EventScripts for that one command.
Another way that I found on AlliedModders: create info_target or any other dummy cross-game entity that would support user inputs, add OnUser1 output connection to it with a desired output connection string and then call FireUser1 on it. Then destroy it in the next frame. But this way we lose !caller while es_fire at least allowed you to set player as a !caller. On the other side, we obtain ability to set any existing entity as an !activator.
Posted: Wed Nov 18, 2015 7:39 pm
by satoon101
You can get the type from target.inputs[<input_name>], if I remember correctly. You could also look into enabling cheats, firing ent_fire, then disabling cheats. Or, even better, in my opinion, loop through all entities of the type you need and call the inputs individually.
Posted: Wed Nov 18, 2015 7:41 pm
by iPlayer
Thanks for your reply. I like the way with target.inputs more, because if I understand it correctly, enabling cheats is a huge security breach. There's a small window when somebody can create point_servercommand and get access to server console.
Posted: Wed Nov 18, 2015 7:54 pm
by Ayuto
I think you "could" use entity.get_input() to get the
input function. Then you only need to call Function.__call__ with your own InputData object, which you filled by using InputData.set_string().
Posted: Wed Nov 18, 2015 8:00 pm
by necavi
Also you should never globally enable cheats, its best to strip the cheat flag from the intended command, run it, and then immediately put the flag back.
Posted: Wed Nov 18, 2015 10:20 pm
by iPlayer
satoon101, target.inputs is a generator that just yields input names, it doesn't provide argument type in any way.
Ayuto, do you mean
_entities._datamaps.Variant.set_string? That would be
inputdata.value.set_string.
As far as I see, it doesn't cast any given string to the needed value type, it just sets it as a string, so the input will be called with a string value.
The code I tried:
Syntax: Select all
input_function = target.get_input(input_name)
input_data = InputData()
if caller is not None:
input_data.caller = caller.index
if activator is not None:
input_data.activator = activator.index
if parameter is not None:
input_data.value.set_string(parameter) # Parameter is always string here, so this line will never raise ValueError
Function.__call__(input_function, input_function._this, input_data)
When tested with
player,SetHealth,42,0,-1Value of input_data gets set to "42" and when the function is actually called, the health of the player is set to zero. No exceptions raised, but player dies from that.
For now I use
Syntax: Select all
_input_types = {
FieldType.BOOLEAN: bool,
FieldType.FLOAT: float,
FieldType.INTEGER: int,
FieldType.STRING: str,
}
...
for target in targets:
input_function = target.get_input(input_name)
caller_index = None if caller is None else caller.index
activator_index = None if activator is None else activator.index
if parameter is not None:
type_ = _input_types.get(input_function._argument_type)
# Check if type is unsupported, but we actually support all types that can possibly
# be passed as a string to input: int, float, bool, string
# TODO: Implement support for entities (passed as a special name like !activator, !caller etc)
if type_ is None:
continue
# 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_()
input_function(parameter, caller_index, activator_index)
necavi, even if I unset 'cheat' flag for
ent_fire and then
immediately set it back, how long does this "immediately" last? Can I strip the flag, use ent_fire and set the flag back in one tick? If so, I guess this is indeed safe. But if I have to bring the flag back in the next tick, this leaves a window for an attacker.
Posted: Wed Nov 18, 2015 10:30 pm
by necavi
I have always done so in the same tick with no issue, yes, that was the entire premise of this plugin, basically:
https://github.com/necavi/Merx
Posted: Wed Nov 18, 2015 11:16 pm
by L'In20Cible
Why aren't you simply using add_output?[python]target.add_output('OnSomething player,SetHealth,42,0,-1')[/python]
Posted: Wed Nov 18, 2015 11:21 pm
by Ayuto
iPlayer wrote:Ayuto, do you mean _entities._datamaps.Variant.set_string? That would be inputdata.value.set_string.
As far as I see, it doesn't cast any given string to the needed value type, it just sets it as a string, so the input will be called with a string value.
I thought this is exactly what you want?
iPlayer wrote:necavi, even if I unset 'cheat' flag for ent_fire and then immediately set it back, how long does this "immediately" last? Can I strip the flag, use ent_fire and set the flag back in one tick? If so, I guess this is indeed safe. But if I have to bring the flag back in the next tick, this leaves a window for an attacker.
That argument is invalid if you are already using es_fire, because commands like es_fire, es_give, es_remove, etc. set sv_cheats to 1, execute the cheat client command and set sv_cheats back to the previous value.
Posted: Wed Nov 18, 2015 11:48 pm
by iPlayer
L'In20Cible, because this output connection and the calling entity itself exist only in hammer editor. Then, when the map is being compiled:
1. Original mapname.vmf is copied to mapname.vmf.original
2. mapname.vmf is processed by my script that searches for specific entities (say point_iplayer), exports all useful information about them (origin, some keyvalues and output connections) and then completely removes these entities from the vmf. If you don't remove them, you will get tons of spam in server console when you load the bsp file about unrecognized point_iplayer entities.
3. mapname.vmf is then sent to the actual compilers - vbsp, vvis and vrad
4. mapname.vmf gets deleted forever and mapname.vmf.original is renamed to mapname.vmf.
Everything goes smoothly to the mapmaker. But he wanted that point_iplayer for a reason. For example, he wanted this entity to open the door when I join the server (OnIPlayerJoinsTheServer). My plugin takes the information extracted during compilation and makes sure that the door on the map opens as soon as I join.
Ayuto, didn't know that. Well, that time Mattie was responsible for turning the cheats on, but this time apparently I could turn out the guy to blame. Anyways, this manual approach is fine as long as it behaves identically to how real output connections do. And even gives more control over handling invalid inputs. Now I can possibly warn map creators about broken connections.
Posted: Thu Nov 19, 2015 5:00 am
by satoon101
One thing I don't get with your implementation is that you have to pass a string that uses commas as separators and then split that value by the commas. You never actually use the full string at any point. I would think a much better way to handle that would be to pass each of those as separate parameters.
Sorry, I forgot that inputs only iterates through the names. Using get_input would be the proper way to go.
Posted: Thu Nov 19, 2015 5:06 am
by iPlayer
I never actually use the full string at any point, but I recieve it in that format:
connections
{
"OnTimer" "server,Command,push jail_game_singlewinner_won,0,-1"
"OnTimer" "!self,Disable,,0,-1"
}
Posted: Thu Nov 19, 2015 5:07 am
by satoon101
I would think that splitting the values would be up to the caller, not the functionality itself.
Posted: Thu Nov 19, 2015 5:15 am
by iPlayer
Well, I was looking at it as at some black box that will eat raw line from vmf and be ready to be fired. So that the processor that parses VMF-file (or config) won't care about what data inside that vmf is. It just handles keyvalues, keyvalues, keyvalues...
Because the whole purpose of this black box (and the code I posted in cookbook) is to eat data in a format that is used for connection by VMF.
But maybe you're right, I will reconsider it once again.