[SCRIPT] How to easily overcome locality issues

Party-approved programming
Post Reply
Black Mamba
Posts: 335
Joined: Sun May 27, 2012 12:11 pm

[SCRIPT] How to easily overcome locality issues

Post by Black Mamba »

Okay, I'll try not make this too much of a wall of text, but in the same time, I want this to be as well documented as possible.

So, I know diving into the scripting world isn't an easy thing to do. It might seem a bit overwhelming at first, and when you think you grasped it, you let yourself go at scripting in a multiplayer environment and drown into locality issues. Even if you are a more experienced scripter, you might have to reinvent the wheel everytime you have a go at it. So here is a hopefully helpful script for all the mission makers around, and the ones that would like to try it. The first part is more of a tuto/explanation about how locality actually works. More experienced scripters might want to skip directly to the second part, that contains the scripts and the comments that come with.

First Part:

Let's try and do this with an example: you want to create an adversarial mission, in which you want to give players the opportunity to disarm and/or handcuff one another.
So, after searching the whole internet and your pockets, you find those things:

Code: Select all

{_unit action ["dropWeapon",_unit, _x] } forEach (weapons _unit);
{_unit action ["DropMagazine", _unit, _x] } forEach (magazines _unit);
This will make a unit drop all of its weapons and magazines on the floor. Sounds good and easy.

Now, you want to make sure to know how those commands work. So you go to the wiki and go check the page for the command action.

I'll go over the details, as the syntax we have above is already functional. What is of interest for us today, are those two symbols on the top-left corner of the page.
The red one, that says EG, means Effects: Global. This means that if that command is executed, say, to make a unit drop its weapons, everybody on the server will see it drop its weapons. That's good. We'll only have to execute that command once, and everybody will see the effects.
The blue one, that says AL, means Arguments: Local. This means that the arguments to the command (here the unit) must be local to the computer the action will be performed on. In short, if you execute that on your computer while the unit is actually another player, nothing will happen.
Not good.

Why? Because, to disarm somebody, say player2, we will use the action menu, via the addAction command. Now, if you look at those two symbols here, it says that both arguments and effects are local to the computer that executes that command. Now, addAction is atricky one, yet is one that we use a lot when adding possibilities to the game.
The action must have been created on your computer for you to see it, and when you use it, it will launch a script on your computer only. Starting to see the problem?
When you use that action in game, the resulting script will execute on your computer only, but we need the resulting actions to be performed by your soon-to-be-disarmed comrade's computer.

Let's create the action on the player2 first:

Code: Select all

_id = player2 addAction addAction ["Search/Disarm", "disarm.sqf", [], 6, false, true, "", "lifeState _target == ""UNCONSCIOUS"""];
Don't worry too much about all the stuff in there for now, or check the addAction page again. Basically, this will create on your computer an action that will appear when you're close to that other player and if he is wounded. If you click on that action, it will launch on your computer the script contained in the disarm.sqf file, in the root of your mission folder.
Once again, if we just paste the initial code in that script, nothing will happen. So we need to find a way to inform player2's computer that he has to run that code itself.

This is where it usually gets tricky. Using CBA Extended Eventhandlers would be the way to go, but we can't do that here, because of the no-addon policy. What would happen if player2 was not running CBA? Nothing. You could also use BIS's built-in Multiplayer Framework. That is, if you can figure out how that works. And even if you do, that's probably the worst piece of code in the whole Armaverse*.

That's where the following script comes in handy: it allows you to easily run code from one computer to all the others, or to one specific computer.

To do that, we are going to create an event, on all machines, at the beginning of the mission. The place of choice to do that would be the init.sqf, as this script is run by all machines when the mission initializes.
But first, we are going to initialize the script:

Code: Select all

nul = call compile preprocessfile "BM_XEH.sqf";
then we create our event, let's name it Disarmed, and the code that must be executed when that event happens.

Code: Select all

["Disarmed", {
	_unit = _this;
	{_unit action ["dropWeapon",_unit, _x] } forEach (weapons _unit);
	{_unit action ["DropMagazine", _unit, _x] } forEach (magazines _unit);
	}
] call BM_addEventHandler;
Now, whenever that Disarmed event happens on one computer, that computer will execute the code to have the unit drop its weapons and ammo.
All we need to do now is to execute that event on the right computer.
So, back to our disarm.sqf, executed by you when you use the action, and let's paste this inside:

Code: Select all

_target = _this select 0; //the unit the action is attached to, here player2's unit.
[_target, "Disarmed", _target] call BM_localRemoteEvent;
The first _target indicates that the code must be run on the computer where the unit is local.
"Disarmed" indicates which event you want to run.
The second _target is the argument that will be passed to the event's code (here it also contains the unit as this is the only argument needed).

Now, let's say whe have a time bomb situation, you have an action on the bomb object to defuse it, but want everybody to know about it when it's done? Same here.
You create an event, say:

Code: Select all

["Defused", {
	_defuser = _this;
	hint format ["The bomb was defused by %1", _defuser];
        }
] call BM_addEventHandler;
The script executed by your action would look like this:

Code: Select all

_bomb = _this select 0;
_defuser = _this select 1;

// whatever code you need to defuse the bomb

// hint to everybody:
["Defused", _defuser] call BM_globalEvent;
And there you go!

Second Part: The actual script

Code: Select all

// BM - Extended EventHandlers
// Credits: Black Mamba [FA], inspired by CBA and based on Muzzleflash's work. Thanks a lot.
// ====================================================================================
// INIT

if (!isnil "BM_EventsLogic") exitWith {};
BM_EventsLogic = "Logic" createVehicleLocal [0,0,0];


// ====================================================================================
// Adds an eventHandler to the local machine(must usually be run globally, i.e in the init.sqf for example, at least it needs to be on the computers you want to raise the event on)
// _id = ["My_Event", {code to be executed when the event is raised}] call BM_addEventHandler;
// returns an index associated with the eventHandler (can be used to remove that eventHandler later)

BM_addEventHandler = {
	_event = _this select 0;
	_code = _this select 1;
	_eventHandlers = BM_EventsLogic getVariable _event;
	if (isnil "_eventHandlers") then {
		_eventHandlers = [];
		BM_EventsLogic setVariable [_event, _eventHandlers];
	};
	_id = count _eventHandlers;
	_eventHandlers set [_id, _code];
	_id
};

// ====================================================================================
// Removes an eventHandler with the given index on the local machine
// bool = ["My_Event", index] call BM_removeEventHandler;
// Returns true if the event was removed, false if it was not dound.

BM_removeEventHandler = {
	_event = _this select 0;
	_id = _this select 1;
	_eventHandlers = BM_EventsLogic getVariable [_event, []];
        _wasRemoved = _id >= 0 && _id < count _eventHandlers;
        if (_wasRemoved) then {
                if (!isNil {_eventHandlers select _id}) then {
                        _eventHandlers set [_id, nil];
                } else {
                        _wasRemoved = false;
                };
        };
        _wasRemoved
};

// ====================================================================================
// Executes an Event's code on the local machine
// ["My_Event", [arguments]] call BM_localEvent;

BM_localEvent = {
	_event = _this select 0;
	_args = if (count _this > 1) then {_this select 1} else {[]};
	_eventHandlers = BM_EventsLogic getVariable [_event, []];
	{
		if (!isNil "_x") then {
			_args call _x;
		};
	} forEach _eventHandlers;
};

// ====================================================================================
// Executes an event on all remote machines (except the local one)
// ["My_Event", [arguments]] call BM_allRemoteEvent;

BM_allRemoteEvent = {
	BM_RE = _this;
	publicVariable "BM_RE";
};

// ====================================================================================
// Executes the event only on the machine where object is local
// [object, "My_Event", [arguments]] call BM_localRemoteEvent;

BM_localRemoteEvent = {
	_object = _this select 0;
	if (local _object) then {
		_event = _this select 1;
		_args = _this select 2;
		[_event, _args] call BM_localEvent;
	} else {
		BM_lRE = _this;
		publicVariableServer "BM_lRE";
	};
};

// ====================================================================================
// Executes the event on all machines (including the local one)
// ["My_Event", [arguments]] call BM_globalEvent;

BM_globalEvent = {
	_this call BM_localEvent;
	_this call BM_allRemoteEvent;
};

// ====================================================================================
// Sets up the system 

"BM_RE" addPublicVariableEventHandler {(_this select 1) call BM_localEvent};

if (isServer) then {
	"BM_lRE" addPublicVariableEventHandler {
		_holder = _this select 1;
		_object = _holder select 0;
		_event = _holder select 1;
		_args = if (count _holder > 2) then {_holder select 2} else {[]};
		if (local _object) then {
			 [_event, _args] call BM_localEvent;
		} else {
			_owner = owner _object;
			BM_RE = [_event, _args];
			_owner publicVariableClient "BM_RE";
		};
	};
};
*Basically, BIS included Multiplayer Framework, aside from the fact that its syntax really is counterintuitive, sends all the code you want to execute on one single computer to the whole network, regardless of its presence already on the target client. Now imagine just ten people executing a same remote code at one point during a fifty people game: that's about 500 times that same code being spammed over the network; the server is likely to go into major desync. On the other hand, the presented code will only use two public variables (containing the name of the event ant the arguments to be passed to the code), thus reducing the network traffic a lot. As an addition, local remote events will only imply transfer between the caller, the server and the target, leaving all other clients unaffected.
I'll expand on that post if I find sme time and ideas to do it. Feel free to ask any questions regarding how this works here, or by pm.
Last edited by Black Mamba on Tue Jan 29, 2013 12:30 pm, edited 1 time in total.

Anvilfolk
Posts: 119
Joined: Thu Jan 17, 2013 3:56 pm
Location: Portugal
Contact:

Re: [SCRIPT] How to easily overcome locality issues

Post by Anvilfolk »

Just wanted to thank you for this. I've never done any ArmA 2 scripting, but I managed to follow you to about half way through, with the BIS wiki open. It's a great way to learn, and I'll give it another shot when I can.

So, again, thanks :)

User avatar
harakka
Posts: 365
Joined: Fri Jul 29, 2011 3:35 pm
Location: Finland

Re: [SCRIPT] How to easily overcome locality issues

Post by harakka »

I haven't tested this out yet, but seems pretty neat. I'll definitely try using this the next time I work on something with potential locality headaches.
Me and him, we're from different ancient tribes. Now we're both almost extinct. Sometimes you gotta stick with the ancient ways, the old school ways. I know you understand me.

User avatar
wolfenswan
Posts: 1209
Joined: Wed May 25, 2011 4:59 pm

Re: [SCRIPT] How to easily overcome locality issues

Post by wolfenswan »

Just to clarify:

If I'd designated an action and the only thing the .sqf executed by the action does would be calling BM_globalEvent, would the code be executed on the client that activated the action as well?

Also I've added a missing } in the Defuser code

Black Mamba
Posts: 335
Joined: Sun May 27, 2012 12:11 pm

Re: [SCRIPT] How to easily overcome locality issues

Post by Black Mamba »

Yup. GlobalEvent will execute on every single machine, including the local one, allRemoteEvent will execute on every machine but the caller (can be useful if something server side needs to trigger something on clients, but not on the dedi), localEvent will only execute on the local machine (basically useless by itself, but comes handy in complex locality mazes), and localRemoteEvent will execute on one single computer, where the object passed as first argument is local (pretty damn useful, that one. Also, I think, more efficient than the current CBA way of doing it).

Also, regarding actions, I find that one to be really useful. Basically negates the need for one sqf with a single line in it, for each action you want to add.

User avatar
wolfenswan
Posts: 1209
Joined: Wed May 25, 2011 4:59 pm

Re: [SCRIPT] How to easily overcome locality issues

Post by wolfenswan »

Yeah i'm using both in conjunction for a mission I'm working on. Really, really neat work you did there.

Post Reply