Questions on working with entities IO calls and keyvalues

Please post any questions about developing your plugin here. Please use the search function before posting!
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Questions on working with entities IO calls and keyvalues

Postby iPlayer » Mon Nov 16, 2015 3:24 am

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.
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Postby L'In20Cible » Mon Nov 16, 2015 4:34 am

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!
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Mon Nov 16, 2015 5:20 am

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.fire

See, 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?
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Mon Nov 16, 2015 8:50 am

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
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Mon Nov 16, 2015 1:21 pm

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.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Mon Nov 16, 2015 2:21 pm

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.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Wed Nov 18, 2015 7:21 pm

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.
User avatar
satoon101
Project Leader
Posts: 2697
Joined: Sat Jul 07, 2012 1:59 am

Postby satoon101 » Wed Nov 18, 2015 7:39 pm

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.
Image
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Wed Nov 18, 2015 7:41 pm

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.
User avatar
Ayuto
Project Leader
Posts: 2195
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Postby Ayuto » Wed Nov 18, 2015 7:54 pm

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().
necavi
Developer
Posts: 129
Joined: Wed Jan 30, 2013 9:51 pm

Postby necavi » Wed Nov 18, 2015 8:00 pm

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.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Wed Nov 18, 2015 10:20 pm

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,-1

Value 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.
necavi
Developer
Posts: 129
Joined: Wed Jan 30, 2013 9:51 pm

Postby necavi » Wed Nov 18, 2015 10:30 pm

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
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Postby L'In20Cible » Wed Nov 18, 2015 11:16 pm

Why aren't you simply using add_output?[python]target.add_output('OnSomething player,SetHealth,42,0,-1')[/python]
User avatar
Ayuto
Project Leader
Posts: 2195
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Postby Ayuto » Wed Nov 18, 2015 11:21 pm

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.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Wed Nov 18, 2015 11:48 pm

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.
User avatar
satoon101
Project Leader
Posts: 2697
Joined: Sat Jul 07, 2012 1:59 am

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

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.
Image
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

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

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"
}
User avatar
satoon101
Project Leader
Posts: 2697
Joined: Sat Jul 07, 2012 1:59 am

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

I would think that splitting the values would be up to the caller, not the functionality itself.
Image
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

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

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.

Return to “Plugin Development Support”

Who is online

Users browsing this forum: No registered users and 133 guests