Jump to content
Sign in to follow this  
carlostex

Best options to remote execute where you want?

Recommended Posts

I think one of the most valuable things you can do when making missions work is having the ability to remotely execute code wherever you want. For that i can't find a better method than CBA_fnc_globalExecute. Let me give an example.

Let's say we have an enemy AI soldier, and that we need to attach him an addaction. Because that AI soldier is local to the server, i need to addaction on all clients. So for that i would use multiplayer framework:

[player,_unit,rADDACTION,"Arrest Soldier", "arrest.sqf",[_unit, player, 1],1,true,true,"","(_target distance _this) < 4"] call RE;

Every client that gets within 4 meters of the AI soldier has the addaction available. Thing is, now the code that is inside arrest.sqf is local to the client who called it. So let's say that i want to something to execute on server only:

[-0, {hint "running only on server"}] call CBA_fnc_globalExecute;

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

As CBA wiki explains parameter -0 executes code on server only, -1 on clients, and -2 on all machines.

So with CBA_fnc_globalExecute i can put code in a script that only runs client side and make it execute wherever i want.

So my question is there any alternative? What else can i do when i want code to run only on a certain destination? For instance, if i would use SHK_Taskmaster, which is server driven, a task created via taskmaster would never fire because arrest.sqf is running in a client locally.

Solutions?

1-CBA_fnc_globalExecute

2-Please tell me

3-Please tell me

etc...

So why am i asking if CBA_fnc_globalExecute does the job? Because i recall Sickboy mentioning it is not a good idea to use it too much.

PS: and besides eventhandlers...

Edited by CarlosTex

Share this post


Link to post
Share on other sites

Using CBA_fnc_globalExecute the entire script is transmitted when executed. Instead I prefer CBA events, where only the parameters are transferred. Also the "per" option in the MPF framework can lead to undesired consequences since previous events that are not relevant anymore will be fired.

I have used the following approach without issue for a long time without issues:

1. Add (CBA) event handlers on machine where appropriate (typically clients, here including hosted server

2. Fire events a single time from anywhere (typically server)

3. Add JIP (join-in-progess) script to all clients, and events that setup the jip data.

Here is an example of how to use it for an arrest system, where the arrest action may later be removed, and including proper jip handling - remember events (and globalExecute does not handle JIPs.). "tag" should be some tag you want to use to prevent conflicting with other events. Addons and other scripts may one CBA events too.

First we create a functions library containing some functionality for the mission - why this code is not included in the events directly will be explained later:


//This sets a person as a criminal and attaches a bounty
Tag_new_criminal = {
_criminal = _this select 0;
_bounty = _this select 1;
//Add action. Bounty can vary
_id = _criminal addAction ["Arrest", "arrest.sqf", [_bounty]];
//Store id so action can later be removed -- this id can vary by client so must be stored locally for each client
_criminal setVariable ["criminal_id", _id];
};

//This clears a person of all charges.
Tag_cleared = {
_clearedCitizen = _this select 0;
//Find the id
_id = _criminal getVariable "criminal_id";
if (!isNil "_id") then {
	_clearedCitizen removeAction _id;
};
};

Now for the actual event handlers:


//-- Step 1 - add handlers

//For all playable machines client and hosted server (not isDedicated)

//This handles a new criminal event
["tag_new_criminal", {
_this call Tag_new_criminal;
}] call CBA_fnc_addEventHandler;

//Someone might be cleared of all charges
["tag_cleared", {
_this call Tag_cleared;
}] call CBA_fnc_addEventHandler;

Then sometime on the server you fire the events:


//Step 2 - raise events

// _dude suspected of murder
_bounty = 100 * floor (random 10 + 1);
["tag_new_criminal", [_dude, bounty]] call CBA_fnc_globalEvent;

//.... later we discover innocence:
["tag_cleared", [_dude]] call CBA_fnc_globalEvent;

That should all work fine. The server tell everyone someone is a criminal or cleared. All clients (and hosted server) receives the event and calls the functions. However, any JIPs joining later won't have any actions. So we need to deal with this.

For simplicity we have a global (gobal network wise too) array TAG_JIP_Criminals.

A new script jip.sqf run for all on init:


//-- Step 3a

if (isServer) then {
TAG_JIP_Criminals = [];
publicVariable "TAG_JIP_Criminals";
} else {
//Clients wait for data - then adds the action locally.
waitUntil {!isNil "TAG_JIP_Criminals"};
//This is why I put Tag_new_criminal in a separate function
{
	//Showed for clarity just passing _x would suffice
	_civ = _x select 0;
	_bounty = _x select 1;
	[_civ, _bounty] call Tag_new_criminal;
} forEach TAG_JIP_Criminals;
};

Here is one thing I like about the event system. You can use the same actions on the server to prepare TAG_JIP_Criminals by simply adding another event that also handles "tag_new_criminal" and "cleared":

//Step 3b

//Server only -- update data for JIPs

//Add the relevant data to the JIP array
["tag_new_criminal", {
//_this is [unit, bounty]
TAG_JIP_Criminals set [count TAG_JIP_Criminals, _this];
publicVariable "TAG_JIP_Criminals";
}] call CBA_fnc_addEventHandler;

//Remove the data so JIPs won't add the action
["tag_cleared", {
_civ = _this select 0;
//First find the index
_index = -1;
{
	if (_x select 0 == civ) exitWith {
		_index = _forEachIndex; //_forEachIndex only works in OA/CO.
	};
} forEach TAG_JIP_Criminals;
//Find him?
if (_index != -1) then {
	//Remove it - swap the last element in the list in and shrink the array by one
	_newSize = count TAG_JIP_Criminals - 1; //_newSize is also the index of the last element before shrinking
	TAG_JIP_Criminals set [_index, TAG_JIP_Criminals select _newSize];
	TAG_JIP_Criminals resize _newSize;
};
publicVariable "TAG_JIP_Criminals";
}] call CBA_fnc_addEventHandler;

It sure isn't the shortest solution. But it works for JIPs - and I like how everything is separated.

Edit: If you replace the word "criminal" with "task" and a couple of other adjustment you got yourself a MP, JIP compatiable task management system.

Edited by Muzzleflash

Share this post


Link to post
Share on other sites

For the remote engineering call out script i created, i use PV and EV. when first created you would call the engineers out from base and they would trundle along to the desired location. however if the player that called the script disconnected - the engineers would just stop in their tracks and that was it. So i changed i to have the server control what the engineers do once created.

The add action is on the damaged vehicle when it falls to a certain level. When you use the add action the engineers are created by the player on the server. To get them to move via the server i then use the following... it works well. No CBA as well.

this is in the code which is called via the add action.

// i create engineers and group them... then i call the below...

 FOCK_engmoveto = "FOCK_eng\FOCK_engmoveto.sqf"; publicVariable "FOCK_engmoveto"; // This data will only be received on other machines. Not on the machine where it is executed.
		  execVM FOCK_engmoveto; 

init or called from the init

"FOCK_engmoveto" addPublicVariableEventHandler
 {
   private ["_moveto"];
  _moveto = _this select 1;
execVM _moveto;
 };

FOCK_engmoveto.sqf


if (is server) then
{ 

do stuff

};

not sure how efficient this is - but it seems to work and the server is not lagging from it. It also seems to JIP as well. Hope it helps.

Share this post


Link to post
Share on other sites

Thanks Muzzleflash.

I'm trying to setup a eventhandler myslf. But i want to setup an eventhandler that runs whatever code i want:

if (isServer) then {
  // Setup eventHandler
  ["TEX_execute", _this select 0] call CBA_fnc_addEventHandler; //EventHandler	

  // Helper function
  TEX_fnc_remoteExecute = { ["TEX_execute", _this] call CBA_fnc_localEvent }; // no need to broadcast, we're already on the server
} else {
  // Helper function
  TEX_fnc_remoteExecute = { ["TEX_execute", _this] call CBA_fnc_globalEvent }; // broadcast to execute on server
};

And then call whatever code i want to run on the server:

[{hint "this is running on the server!"}] call TEX_fnc_remoteExecute.

I can't test this now so no idea if it works.

Edited by CarlosTex

Share this post


Link to post
Share on other sites

As pointed out by MuzzleFlash, and myself before, setting up EventHandlers is better than sending dynamic code over the net.

Why would you want to send dynamic code over the net? You already know beforehand how that code looks like, so the better approach is attaching an eventhandler with that code, and then only triggering that eventHandler, with parameters.

If you don't want to use it, then Adding EventHandlers that then execute your dynamically provided code is double and you can just fall back to using CBA's globalExecute. But again, why would you keep sending the code?

IMO you do not understand the EventHandlers well if you keep wanting to send dynamic code anyway :) And I would recommend getting to know them better.

The CBA whereLocalEvent can be used for only triggering the event on the machine where you want.

https://dev-heaven.net/docs/cba/files/events/fnc_whereLocalEvent-sqf.html (must be used in conjunction with https://dev-heaven.net/docs/cba/files/events/fnc_addLocalEventHandler-sqf.html)

Init:

// Only triggered where _unit is local
["myEvent", {
 _unit = _this select 0; _someParameter = _this select 1; _someTextParameter = _this select 2; 

 _unit globalChat _someTextParameter
}]  call CBA_fnc_addLocalEventHandler;

Trigger event:

["myEvent", [somePlayerObject, someParameter, "someTextParmeter"]] call CBA_fnc_whereLocalEvent;

Edited by Sickboy

Share this post


Link to post
Share on other sites

Yeah Sickboy i most certainly don't understand them well. The reason why i want to send the code it's because i need to execute diferent stuff on that specific machine several ocasions. That way i don't have to create 500 different eventHandlers whenever i need to run specific code that does different things.

For instance if i want to show a taskHint on all clients it's very easy to do it with CBA_fnc_globalExecute. I could create a specific eventhandler for it but i don't see the need! I could also do it with multiplayer framework but the documentation on that one is very weak and i could not get the syntax right for taskHint command. CBA_fnc_globalExecute did the job allright, and i could just make it run on all clients with -1 parameter.

Share this post


Link to post
Share on other sites

You dont need to create 500 different eventhandlers, because you send parameters that make the function do different things.

If you look back at the function I provided you to create Markers, you can see how that works; you trigger the event with about 5 marker parameters, and you can create any kind of marker, depending on the parameters.

You can do the same for taskCreate, and e.g hint, or radioChat or anything else for that matter.

Besides, 500 eventhandlers, or 500x globalExecute calls makes no real difference in amount of work, but it would make your code a lot better organized.

But again, you don't need an eventhandler for every small variation, you should only need few specific purpose eventhandlers that expect parameters you can send from the Sender side, and use them to perform the actions you desire.

---------- Post added at 12:49 ---------- Previous post was at 12:42 ----------

If you really want to cut down on the eventhandlers, you can do this:

Init:

["myEvent", {
  _eventType = _this select 0;
  _eventData = _this select 1;

 switch _eventType {
    case 1: { hint (_eventData select 0) };
    case 2: { (_eventData select 0) globalChat (_eventData select 1) };
 };
}] call CBA_fnc_addEventHandler

Trigger hint:

["myEvent", [1, ["MyHintText"]]] call CBA_fnc_globalEvent 

Trigger radio:

["myEvent", [2, [player, "MyRadioText"]]] call CBA_fnc_globalEvent 

However I would suggest more specific eventhandlers, instead of such "OneEventToRuleThemAll", as it improves maintainability, overview, and if you name your eventNames properly, makes your code a lot better readable.

The marker function should be best example imo, though please note that it was designed with marker creation on the server in mind.

Edited by Sickboy

Share this post


Link to post
Share on other sites

OK what do you think about this:

if (isServer) then {
  // Setup eventHandler
  ["TEX_serverVehicle", 
{ 
_getveh = _this select 0; //String
_posarray = _this select 1; //Array
_vehvar = _this select 2; //String	
_sveh = [getMarkerPos "dump", 0, _getveh, EAST] call BIS_fnc_spawnVehicle;
_veh = _sveh select 0;
_veh SetVehicleVarName _vehvar;
_veh Call Compile Format ["%1=_This ; PublicVariable ""%1""",_vehvar];
_pos = [_posarray] call TEX_ExtractRandom;
_veh setposASL _pos;
}] call CBA_fnc_addEventHandler; //EventHandler	

  // Helper function
  TEX_fnc_createVehicle = { ["TEX_serverVehicle", _this] call CBA_fnc_localEvent }; // no need to broadcast, we're already on the server
} else {
  // Helper function
  TEX_fnc_createVehicle = { ["TEX_serverVehicle", _this] call CBA_fnc_globalEvent }; // broadcast to execute on server
};

This is to create a vehicle server side only when called from a client. Is this the way to go?

I may build another one for taskHint. Except that one i need to run on all clients and not on dedi server.

Share this post


Link to post
Share on other sites

Great!

Nicely combined also with the helper functions, which are especially useful in this situation where you want to perform actions on the server etc.

Share this post


Link to post
Share on other sites

Now if i create one for taskhint, should i put condition in the eventhandler as !isServer and !isdedicated? The reason is because i only need taskhint to show on clients....

Share this post


Link to post
Share on other sites

Init:

if !(isDedicated) then {
  ["TEX_clientTaskHint", {
    // ...
  }] call CBA_fnc_addEventHandler;
};

Trigger:

["TEX_clientTaskHint", [taskParam1, taskParam2, ...]] call CBA_fnc_globalEvent;

And can always put in helper function:

TEX_fnc_taskHint = { ["TEX_clientTaskHint", _this] call CBA_fnc_globalEvent };

Edited by Sickboy

Share this post


Link to post
Share on other sites

A server can also be a client. If you only play on dedicated it will never be the case, however, some people play hosted servers where the server is also a client (player). So for any visuals, like taskHint, I would add the event handler where the machine is not a dedicated server (!isDedicated).

Share this post


Link to post
Share on other sites

Yeah i'll probably use with the helper function. Comes in handy, as i'm trying to use almost no triggers in missions. Prefer use waitUntils or loops with longer sleeps on server side scripts than triggers.

Right now only thing that i cannot get working is the following script by muzzleflash:

//////////////////////////////////////////////////////////////////
// By Muzzleflash
// Forcefields @ Lingor
//////////////////////////////////////////////////////////////////
waitUntil {!isNil "scud1"};
if (!isServer) exitWith {};
//timer.sqf
private ["_countdown","_serverTime","_totalTime","_numbersToTimeString","_text"];

_serverTime = _this select 0;

_totalTime = 1*60;

_numbersToTimeString = {
   private ["_hours", "_minutes","_chars"];
   _hours = _this select 0;
   _minutes = _this select 1;
   _chars = [];
   _chars set [0, (floor (_hours / 10)) + 48];
   _chars set [1, (floor (_hours mod 10)) + 48];
   _chars set [2, 58];
   _chars set [3, (floor (_minutes / 10)) + 48];
   _chars set [4, (floor (_minutes mod 10)) + 48];
   toString _chars
};
scud1 action ["scudLaunch",scud1];
_countdown = _totalTime - time + _serverTime;
_text = parseText format ["<t size='2' color='#ffffba0c'>%1</t>", ([floor (_countdown / 60), _countdown mod 60] call _numbersToTimeString)];
while {sleep 0.5; _countdown > 0} do {
   //Find how much time is left
   _countdown = _totalTime - time + _serverTime;
   if (_countdown > 0) then {
	hintSilent parseText format ["<t size='2' color='#ffffba0c'>%1</t>", ([floor (_countdown / 60), _countdown mod 60] call _numbersToTimeString)];
   };
if (_countdown < 1 && scudState scud1 > 2) exitWith { 
	scud1 action ["scudStart",scud1];
	["Task11","failed"] call SHK_Taskmaster_upd;
};
if (_countdown > 0 && !alive scud1 || _countdown > 0 && scudState scud1 < 1) exitWith {
	["Task11","succeeded"] call SHK_Taskmaster_upd;
};

};

{deleteMarker _x} foreach ["markerSCUD1","markerSCUD2","markerSCUD3","markerSCUD4"];  

Works at server level but it does not broadcast the counter to the clients. I know it is because hintSilent is local, but when i tried woth Multiplayer framework it wouldn't work either:

[nil,nil,rHINT, _text] call RE;

@muzzleflash

Yeah i know that but since this mission is supposed to be running for weeks, i don't see the point in trying to make it compatible in hosted.

Share this post


Link to post
Share on other sites
A server can also be a client. If you only play on dedicated it will never be the case, however, some people play hosted servers where the server is also a client (player). So for any visuals, like taskHint, I would add the event handler where the machine is not a dedicated server (!isDedicated).
Doh, thanks for noticing, fixed :)

Share this post


Link to post
Share on other sites

Yeah I recognize some parts of the scripts. It was meant to allow the count down to be done client side without broadcasting all the time. From the parameters it appears the script should be started from rEXECVM or an event handler. Something like this:

//Event is added for all
["scud_countdown", {
   //_this is [serverTime]
   _this execVM "timer.sqf";
}] call CBA_fnc_addEventHandler;

//....... at some point on the server it is activated
["scud_countdown", [time]] call CBA_fnc_globalEvent;

Edit: You should probably put the last 2 ifs in the while loop inside an if (isServer) ... however since SHK_Taskmaster only "works" on the server you should be safe..

You may experience inconsistencies in the time on different machine, since all use the server time, which may be much lower if the server is overloaded -- however, the purpose was to reduce network traffic.

Edit2: You are going to need to remove this line from the top: if (!isServer) exitWith {};

Edit3: The original post shows the intent more clearer and how to do it with rEXECVM: http://forums.bistudio.com/showthread.php?123936-Timer-Script-Need-Help!!!&p=2008065&viewfull=1#post2008065

Edited by Muzzleflash

Share this post


Link to post
Share on other sites

Yeah i'm calling it on a script that was fired via an addaction. I'm calling it through an eventhandler:

_nul = [time, "tasks\timer.sqf"] call TEX_fnc_serverExecVM;

And the eventhandler itself:

if (isServer) then {
  // Setup eventHandler
  ["TEX_serverexec", 
{ 
_param1 = _this select 0;
_path = _this select 1;
[_param1] execVM _path;
}] call CBA_fnc_addEventHandler; //EventHandler	

  // Helper function
  TEX_fnc_serverExecVM = { ["TEX_serverexec", _this] call CBA_fnc_localEvent }; // no need to broadcast, we're already on the server
} else {
  // Helper function
  TEX_fnc_serverExecVM = { ["TEX_serverexec", _this] call CBA_fnc_globalEvent }; // broadcast to execute on server
};

Script is firing allright and everything is working great, script is working fine, all the conditions are being checked allright. Only problem is that hintSilent is not broacasted so clients do not see the countdown!!!

All i need is the hintSilent to be broadcasted to the clients. Multiplayer framework does not seem to help either.

EDIT: Yeah the script is allready running serverside only, so i do not need to put (isServer) i confirmed it checking with SLX_XEH_MACHINE. I need no more than show the hint on clients. Server is running at almost 40 FPS, so i should not see a big inconsistency.

Again all that is missing is the hint being shown on the client machines, everything else is running and working just fine.

Edited by CarlosTex

Share this post


Link to post
Share on other sites

"tasks\timer.sqf" is supposed to run for everybody. You only add the "TEX_serverexec" on the server. And your helper (on the server) ensures regardless of whether the event was also declared on all clients, that event is only activated locally. Meaning the script is only ever run on the server and never on the clients.

Instead you need the script to be run everywhere.

// Setup eventHandler -- For all machines
["TEX_globalexec", 
{ 
   _param1 = _this select 0;
   _path = _this select 1;
   [_param1] execVM _path;
}] call CBA_fnc_addEventHandler; //EventHandler	

// Helper function
TEX_fnc_globalExecVM = { ["TEX_globalexec", _this] call CBA_fnc_globalEvent };

And then

_nul = [time, "tasks\timer.sqf"] call TEX_fnc_globalExecVM;

Notice the it is global this time not server.

Share this post


Link to post
Share on other sites

OK but isn't it easier to just broadcast the counter? I know that this reduces traffic but i don't see the point of running it on every machine.

So there is no solution for this if i just want to send the hint across the network?

Share this post


Link to post
Share on other sites

Yes it is easier. But as I mentioned earlier the point of the script was to reduce network traffic, not to be straightforward.

If you want to do that instead then use what you did in post 13 and use your TEX_fnc_serverExecVM. You will need to create a global event though that replicates the hintSilent thing:

//Add this for all playable machines
["Tex_counter", {
   _count = _this select 0;
   hintSilent parseText format ["<t size='2' color='#ffffba0c'>%1</t>", _count];
}] call CBA_fnc_addEventHandler;

Then you will need to replace:

hintSilent parseText format ["<t size='2' color='#ffffba0c'>%1</t>", ([floor (_countdown / 60), _countdown mod 60] call _numbersToTimeString)];

with

_text = [floor (_countdown / 60), _countdown mod 60] call _numbersToTimeString;
["Tex_counter", [_text]] call CBA_fnc_globalEvent;

Edited by Muzzleflash

Share this post


Link to post
Share on other sites

Works great muzzleflash, absolutely great!!!

@Sickboy

No more CBA_fnc_globalExecute i promise!!!! :p

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  

×