Jump to content
Sign in to follow this  
Kempco

PVEHs and How to execute code Remotly -explained-

Recommended Posts

The public variable event handler (PVEH) allows you execute code remotely (i.e. on all machines except the one it is initialized on). To illustrate how this is done, lets create 2 variables: “JTK_Call_PVEH†and JTK_Spawn_PVEH†and assign each a PVEH.

"JTK_Call_PVEH" addPublicVariableEventHandler {((_this select 1) select 0) call ((_this select 1) select 1)};
"JTK_Spawn_PVEH" addPublicVariableEventHandler {((_this select 1) select 0) spawn ((_this select 1) select 1)};

The first part in quotes is the name of variable the PVEH is assigned followed by the addpublicvariableEventHandler followed by the code that will be executed when said variable is "PV'd".

WTF does {((_this select 1) select 0) call ((_this select 1) select 1)} mean??

The PVEH, like any event handler passes its own array. If you think of it in terms of a "killed" event handler, the array passed when a "killed" EH fires is [unit,killer].

The PVEH passes the following array: [Old value of the variable, New value of the variable], the variable being the variable the PVEH is added to. The first part of the array, the old value, for the sake of executing code remotely, is not needed since the handler needs to know what code to execute. So we will only be concerned with the second part of the passed array: the new value of the variable i.e. (_this select 1) .

code = {((_this select 1) select 0) call ((_this select 1) select 1)}

To keep from having to create a different PVEH for each script you want to pass remotely, I create 2 PVEHs one for code I call, the other for code I spawn.

Example: Play Sound For All Units via an addaction.

//example of sqf passed via the addaction

_unit  = _this select 0;

_unit say3d "MY SOUND";//<==================================================================Executes Code for local unit
JTK_Call_PVEH = [[color="#0000FF"][_unit,"MY SOUND"][/color],[color="#008000"]compile "(_this select 0) say3D (_this select 1)"[/color]]; 		
publicVariable "JTK_Call_PVEH";//<==========================================================Executes Code for remote units 

In the above example, the variable JTK_Call_PVEH is being defined as [[_unit,"MY SOUND"],compile "(_this select 0) say3D (_this select 1)"] before being PV'd.( IMPORTANT: When the variable is first defined, there are no quotes. When you PV the variable, you must add quotes.) When you PV the variable the PVEH executes the code according to NEW value of variable it is added to.

The new value of JTK_Call_PVEH = [[_unit,"MY SOUND"],compile "(_this select 0) say3D (_this select 1)"]

code executed: {((_this select 1) select 0) call ((_this select 1) select 1)} OR [_unit,"MY SOUND"] call compile "(_this select 0) say3D (_this select 1)";

Using JTK_Spawn_PVEH;

The above example executes a small segment of code that be created dynamically and compiled on the fly. If you want to run an entire script and that script contains any sleep or waituntil commands, then that script will have to be spawned in.

First, precompile your script:

JTK_HALO_PlayerExec = Compile PreprocessFile (JTK_HALO_Path+"scripts\PlayerExec.sqf");

/*
Example using PVEH:
The following is a small segment from an FSM run server side.
*/


if (isMultiplayer) then {	
for "_i" from 0 to (count _players - 1) do {
	_unit = _players select _i; 
	_attachPos = _UnitPosArray select _i;
	_unit setVariable ["JTK_HALO_Player_Ready",false,true];
	_array = [_unit,_aircraft,_static,_attachpos];
	if (local _unit) then {
		_array spawn [color="#FF0000"]JTK_HALO_PlayerExec[/color];
	}else{[b]JTK_Spawn_PVEH[/b] = [_array,[color="#FF0000"]JTK_HALO_PlayerExec[/color]]; publicVariable "[b]JTK_Spawn_PVEH[/b]"};

};
};

What about CBA?

If you are using CBA things are much easier.

If you wanted to execute the above script globally using CBA you could use CBA_fnc_globalExecute function.

https://dev-heaven.net/docs/cba/files/network/fnc_globalExecute-sqf.html#CBA_fnc_globalExecute

/*
Example using CBA:
The following is a small segment from an FSM run server side.
*/
if (isMultiplayer) then {	
for "_i" from 0 to (count _players - 1) do {
	_unit = _players select _i; 
	_attachPos = _UnitPosArray select _i;
	_unit setVariable ["JTK_HALO_Player_Ready",false,true];
	_array = [_unit,_aircraft,_static,_attachpos];
	[-1, {(_this select 0) spawn (_this select 1)},[_array,JTK_HALO_PlayerExec]] call CBA_fnc_globalExecute;
};
};

The CBA_fnc_globalSay3d vs PVEH:

_unit  = _this select 0;

_unit say3d "MY SOUND"
JTK_Call_PVEH = [[_unit,"MY SOUND"],compile "(_this select 0) say3D (_this select 1)"]; 		
publicVariable "JTK_Call_PVEH";

becomes..

_unit  = _this select 0;
[_unit, "MY SOUND"] call CBA_fnc_globalSay3d;

Applying Restrictions To Globally Executed Scripts:

When executing scripts globally make sure those scripts start with code that restricts their execution accordingly. To highlight potential pitfalls lets take the CBA_fnc_globalSay3d example but this time we will define the source as "player" not "_unit".

_unit  = _this select 0;
[_unit, "MY SOUND"] call CBA_fnc_globalSay3d;

becomes...

[player, "MY SOUND"] call CBA_fnc_globalSay3d;

The source of sound to be played being propagated over the network is player. However, the definition of the "player" is not the same from client to client. On my computer my avatar is defined as "player" however on your computer, your avatar is the "player"; Therefore instead of playing the sound in 3d relative to the unit who executed the script, it will play the sound in 3d relative to each players position which defeats the purpose say3d in the first place.

The above example JTK_HALO_PlayerExec was used to execute a script for all players that were within a specific vehicle. When executed, each player inside that vehicle would watch a cut scene while they were teleported to a new position. Keep in mind that the code is being executed for everyone, not just certain If you do not include restrictions within the script being propagated this will cause issues..............LIGHT BULB

Writing this made me realize a potential problem with my script. Will finish later.

Share this post


Link to post
Share on other sites

Sending code across the network should generally be avoided, unless you cannot control handlers easily on the other machines.

An event system where you only send the parameters, and perhaps the event names, is much preferred instead.

Especially when you are going to use it frequently, or if it involves a lot of code.

Bandwidth is not unlimited and adding additional unneeded strain on the network is IMO the last thing you need.

Therefore the suggested approaches are IMO only appropriate for debugging, or very few and specific situations.

The better approach is executed for example in CBA's addEventHandler, globalEvent, localEvent, whereIsLocalEvent.

Here you register the code on the receiving system, instead of on the sending system, while you only trigger the event, optionally with parameters, from the sending system.

This event system is only a small wrapper around the game's PVEH system, but uses only a single PVEH, to limit unneeded PV (*N) syncing to JIP players.

And also allows for local eventing. (String compiling to code, or string interpolation for parameters can be avoided entirely as well)

It might not matter much for 20 characters, or single use, but such things soon turn into more characters, more uses etc.

Edited by Sickboy

Share this post


Link to post
Share on other sites

Sorry Muzzleflash, I'm confused by what you wrote (my fault, not yours). I have a situation that I can't use CBA for, so how do I go about using your code?

Do I save it in the modfolder and run it at mission init or player init?

How do I pass parameters to it as well? I need to spawn some code on all the other clients when the player fires his gun. Will this work with spawned code?

At the moment, I've got the following in my player init:

if (isNil{player getVariable "horde_pl_var_unit_has_eventhandlers"}) then
{
player addEventHandler ["fired", "_this spawn horde_fnc_spawn_particle_effect"];
player setVariable ["horde_pl_var_unit_has_eventhandlers", true, false];
};

horde_fnc_spawn_particle_effect then sends some code like this currently:

private ["_source","_unit","_weapon","_type","_pl_pos"];

_unit = _this select 0;
_weapon = _this select 1;

_type = getText (configFile >> "CfgWeapons" >> _weapon >> "horde_ce_particle_effects");

if (_type != "none") then
{
_pl_pos = getPosATL _unit;
_source = "#particlesource" createVehicleLocal _pl_pos;
// some lengthy stuff here

HORDE_PVAR_BROADCAST_EFFECTS = [[_pl_pos,_type], horde_fnc_spawn_remote_particles];	
publicVariable "HORDE_PVAR_BROADCAST_EFFECTS"; // send over network to other clients

sleep 8;

deleteVehicle _source;		
};

Then horde_fnc_spawn_remote_particles is:

private ["_source_pos","_effect","_distance","_source"];

_source_pos = _this select 0;
_effect = _this select 1;

_distance = player distance _source_pos;
if (_distance > 1000) exitWith {};															
_source = "#particlesource" createVehicleLocal _source_pos;
// more code for _effect cut out for brevity here

sleep 8;

deleteVehicle _source;

From what you pasted up, do I just insert

[EVENT, CODE] call TAG_AddEventHandler;

into the horde_fnc_spawn_particle_effect code in place of what I have already?

So something like:

private ["_source","_unit","_weapon","_type","_pl_pos"];

_unit = _this select 0;
_weapon = _this select 1;

_type = getText (configFile >> "CfgWeapons" >> _weapon >> "horde_ce_particle_effects");

if (_type != "none") then
{
_pl_pos = getPosATL _unit;
_source = "#particlesource" createVehicleLocal _pl_pos;
// some lengthy stuff here

["fired", [_pl_pos,_type] spawn horde_fnc_spawn_remote_particles] call TAG_AddEventHandler;

sleep 8;

deleteVehicle _source;		
};

Sorry, really confused and I have no way to test this at the moment.

Share this post


Link to post
Share on other sites
Sorry Muzzleflash, I'm confused by what you wrote (my fault, not yours). I have a situation that I can't use CBA for, so how do I go about using your code?

Do I save it in the modfolder and run it at mission init or player init?

You should run the file on mission init (preferable: call compile preProcessFile "events.sqf"; )

How do I pass parameters to it as well? I need to spawn some code on all the other clients when the player fires his gun. Will this work with spawned code?

You call it like this:

["MyEvent", ["My", "Arguments", "Here"]] call Tag_GlobalEvent; //Global event means it will run everywhere (machines) there you have added an event handler with that name. See below. 

Listeners are added like this:

["MyEvent", {
//You get there parameters here:
private ["_me", "_arguments", "_here"];
_me = _this select 0;
_arguments = _this select 1;
_here = _this select 2;
//Do weird greeting
hint format ["%1 are %2 %3", _here, _me, _arguments];
}] call TAG_AddEventHandler;

Can't remember which environment addPublicVariableEventHandler runs in, so if you plan to do sleeping or long running loops in your event 'handler code' you may want to spawn it, to prevent it from blocking other event handlers for the same event, eg.:

["MyEvent", {
//Don't block (sleep) other events
_this spawn {
	//Code here that might sleep or take long. Still have all parameters since they are passed in.
};
}] call TAG_AddEventHandler;

horde_fnc_spawn_particle_effect then sends some code like this currently:

private ["_source","_unit","_weapon","_type","_pl_pos"];

_unit = _this select 0;
_weapon = _this select 1;

_type = getText (configFile >> "CfgWeapons" >> _weapon >> "horde_ce_particle_effects");

if (_type != "none") then
{
_pl_pos = getPosATL _unit;
_source = "#particlesource" createVehicleLocal _pl_pos;
// some lengthy stuff here

//Changed here.. You raise the event with appropriate arguments. SpaRemPar = Spawn Remote Particles
["Horde_SpaRemPar", [_pl, _pos, _type]] call TAG_GlobalEvent;

sleep 8;

deleteVehicle _source;		
};

I modified the code above.

Then horde_fnc_spawn_remote_particles is:

private ["_source_pos","_effect","_distance","_source"];

_source_pos = _this select 0;
_effect = _this select 1;

_distance = player distance _source_pos;
if (_distance > 1000) exitWith {};															
_source = "#particlesource" createVehicleLocal _source_pos;
// more code for _effect cut out for brevity here

sleep 8;

deleteVehicle _source;

No reason to change this. It still get all the parameters here from the AddEventHandler shown first. I consider it good style to keep, the "real code" separate from the "message passing" code, be it CBA-style events, publicVariable, RE framework or whatever.

During your init or similar you add the event handler for all machines (or those that need):

["Horde_SpaRemPar", {
//You sleep in the called code, so it gets spawned.
       //All the parameters are in _this.. Eg. _this = [_pl, _pos, _type]   (horde_fnc_spawn_particle_effect)
_this spawn horde_fnc_spawn_remote_particles;
}] call TAG_AddEventHandler;

So you avoid sending all the code each time.

With some minor modifications horde_fnc_spawn_particle_effect and horde_fnc_spawn_remote_particles could be exactly the same you could call GlobalEvent from the fired event handler directly.

Edited by Muzzleflash

Share this post


Link to post
Share on other sites

That is awesome! Thank you :)

Just to check though - is it encouraged to keep the prefix TAG_ or should that be changed to something else (like the OFPEC author tag system)?

For example, should I go through the script you posted and change all of the instances of TAG_ to something like HORDE_ (and change mine accordingly)?

Edited by Das Attorney

Share this post


Link to post
Share on other sites
This event system is only a small wrapper around the game's PVEH system, but uses only a single PVEH, to limit unneeded PV (*N) syncing to JIP players.

This is a good idea, but with only a single variable, I can't see how this would be thread-safe. (If globalEvent were called simultaneously on two threads, you'd have a race condition.)

Share this post


Link to post
Share on other sites
Just to check though - is it encouraged to keep the prefix TAG_ or should that be changed to something else (like the OFPEC author tag system)?

No, you should change it to something else. HORDE would be fine. Remember to change it everywhere (also the 2 defines at the top).

This is a good idea, but with only a single variable, I can't see how this would be thread-safe. (If globalEvent were called simultaneously on two threads, you'd have a race condition.)

Only think it is unsafe if the "actual" code to be run is not thread safe:

From the globalEvent call: any publicVariabled data are guaranteed to arrive separate, so if you do 2 global events at the same time, then they will still arrive in some order.

From the localEvent: when the handler for addPublicVariableHandler runs it runs to completion before any other publicVariabled events are processed. Since localEvent also calls each handler there is no issue with the event "framework". If you own code is affected by race conditions then it will still be affected by if you used two variables.

The only place I can see where you might have this problem is if you addEventHandler while a events are already being processed. However that can be fixed by copying the handler array when processing:

TAG_LocalEvent = {
       private ["_event","_args","_eventHandlers"];
       _event = _this select 0;
       _args = if (count _this > 1) then {_this select 1} else {[]};
       _eventHandlers = [color="#009000"]+[/color]TAG_EventHandlers getVariable [_event, []];
       {
               //Handler may have been removed.
               if (!isNil "_x") then {
                       _args call _x;
               };
       } forEach _eventHandlers;
};

Share this post


Link to post
Share on other sites
Only think it is unsafe if the "actual" code to be run is not thread safe:

From the globalEvent call: any publicVariabled data are guaranteed to arrive separate, so if you do 2 global events at the same time, then they will still arrive in some order.

From the localEvent: when the handler for addPublicVariableHandler runs it runs to completion before any other publicVariabled events are processed. Since localEvent also calls each handler there is no issue with the event "framework". If you own code is affected by race conditions then it will still be affected by if you used two variables.

The only place I can see where you might have this problem is if you addEventHandler while a events are already being processed. However that can be fixed by copying the handler array when processing:

TAG_LocalEvent = {
       private ["_event","_args","_eventHandlers"];
       _event = _this select 0;
       _args = if (count _this > 1) then {_this select 1} else {[]};
       _eventHandlers = [color="#009000"]+[/color]TAG_EventHandlers getVariable [_event, []];
       {
               //Handler may have been removed.
               if (!isNil "_x") then {
                       _args call _x;
               };
       } forEach _eventHandlers;
};

I'm referring to the TAG_RemoteEvent function:

TAG_RemoteEvent = {
       PV_VAR = _this;
       publicVariable QUOTE(PV_VAR);
};

If that is called simultaneously by multiple threads, I think there could be a race between setting PV_VAR and calling publicVariable.

So, something like this:

Thread 1                                    Thread 2
["event", data] call TAG_RemoteEvent        ["event", data] call TAG_RemoteEvent
PV_VAR = _this;                             PV_VAR = _this;

- now PV_VAR is what it was last set to, which could be either what thread1 or thread2 passed

publicVariable QUOTE(PV_VAR);               publicVariable QUOTE(PV_VAR);

Of course the chances of this occurring would be pretty minimal (I probably would not worry about it too much).

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
Sign in to follow this  

×