Locality is a bitch.
— Every mission maker ever
Intended Audience: intermediate mission makers.
Locality is one of the harder things to grasp in Arma. Not because the basic concept is hard, but because it's an umbrella term for a few related concepts, and because it's usually much easier to tell people that something doesn't work "because locality" or "because Arma", than to take the time and explain what exactly went wrong.
With this document, I want to try and clean up some of that mystique and explain how you find and fix locality issues.
Under locality, we usually understand one of 3 things. They are related, and will usually work together to confuse mission makers. So let's go through them one by one.
First up, what is an object? An object is anything that exists in the 3D world of Arma. Be it a player character, an AI, a vehicle, or even the buildings and vegetation. It does not include things in your inventory.
Secondly, what machines there are: There will always be a server. When testing missions on your own, that's you, and there will be no others. When we run the mission on session, there will be one dedicated server (sitting in a datacenter somewhere), and every player joins with their machine as a client.
Now, locality. Every object in arma is owned by exactly one machine. That machine has total authority over that object, and is the boss for everything related to that object. We say the object is "local to that machine".
For most objects, that owner is quite easy to tell: Any buildings or vegetation, as well as AI will usually be owned by the server. Any player character will be owned by the players machine.
Vehicles are a little more tricky: they will start on the server, but will shift around to be local to whoever is driving the vehicle.
(FYI: That's why drivers never see their own vehicles desync - they are the boss of that vehicle and it's right where it should be. It's on the way from the driver, over the big scary internet, to the others players, that the desync happens.)
And for completeness: There's also zeus spawned AI, which will be local to the Zeus who spawned them. You rarely have to care about them tho.
A script is a collection of one or more script commands. In the simplest case, it's what you write in a trigger condition or activation. The language they are written in is SQF.
For the next paragraph we pick the server to look at, because it's easy to refer to it by name in writing/speaking. But the same will be true for any machine involved - the server is not any different in that regard.
When the server executes a script, that script only runs on the server. And it will only ever run on the server. It can never transfer itself to another machine and continue there. It doesn't know about other scripts running anywhere.
What we can do, is to execute the same script on another PC as well. But then that's a separate execution, and it won't have any interaction whatsoever with the script running on the server (unless we explicitly do some clever things do make them talk to each other).
Hence, in the next section, when referring to the "current PC", we always mean the PC that a particular script or script command is running on.
Let's have a look at how we usually run scripts in a mission: By using triggers, or by using actions.
Triggers have a "Server only" check box.
When that box is ticked, the condition and activation (both of which are scripts), will only run on the server.
When the box is not ticked, they run on every machine, independently from each other. Meaning that when one machine detects that the trigger should activate, it will only execute it's activation
on that machine. And it will only be considered activated on that machine. The other's will be completely unaware.
If you use a condition that can be checked on every machine, like if a vehicle is destroyed, then of course every machine's trigger will activate and execute its activation script. But they still won't be aware that the other machines are doing the same thing.
Those can be scroll menu actions (shame!) or ace interactions.
The script of an action is only executed on the machine of the player who activated them.
Things do things because you tell them with script commands.
Some commands, like showing text on the screen, only happen where you execute them. After all, it would be a little hard to show a secret message to a player if that would suddenly appear everywhere.
Other commands, like adding damage to a vehicle, will be seen by everyone. Usually in the form of a fireball.
For commands that affect objects, that's not the whole truth though. When there is a change on an object, usually everyone sees it. But not everyone is allowed to make it! After all, there is only one machine who owns the object and has complete control over it.
So some commands, only the owner of the object can do. For example, only the machine where an AI is local can set AI into the captive
state (that's usually the server).
Other commands, like setting damage to an object, everyone can do.
There is no set rule about which commands work how. There are guidelines, but the only real source of truth is the BI Wiki.
Have a look at the 3 commands we have mentioned here.
Near the top, you will find those little pictures:
They tell you if the command has a local effect
(only does something on the current PC) or global effect
(everyone sees it).
And if the argument (meaning the affected object) has to be local to the current PC (local argument
), or if it can be any object (global argument
).
When executing a command that expects a local argument on a machine where the argument isn't local, nothing happens. We can abuse that fact to simplify or triggers a little (See examples).
The most common case in which mission makers stumble over locality, is when something works during their local testing, but not on the dedicated server.
The reason for that is that in local testing, your PC is both the server and the client.
So doing something with an AI that only the owner can do? Will just work.
Showing some text to the user? Will just work.
It is also the only machine, so you wouldn't notice that other players don't see a message.
On the dedicated server, that looks a little different.
Setting an AI captive for example must happen on the server. You can't just do that from a Scroll Menu Action, because that runs on your PC.
You can't simply have the server display a hint and expect everyone to see it. Because if it's only executed on the server, then it will only have an effect on the server (and there's nobody there to read it... kinda tragic really).
Similar thing with showing a message after doing an action: You will see it yourself, because it happens on your PC. But it won't be shown for everyone.
But worry not, the rest of this document will deal with fixing those issues :)
The simple yet not very satisfying answer: go through your trigger, line by line, and think very hard about what's going on.
Be aware where your trigger is executed. I.e. the server. Or a player.
Important: Assuming that you have a not server only trigger, go through the involved machines separately. For example start by assuming that the server is executing it. Once you are happy with that, consider what happens when player A executes it.
If the trigger was about something that player A did, then also consider what the execution would look like for player B. They are all individuals, and won't magically know about each other's script!
Sometimes, you will find an easy way of fixing your trigger, by choosing different commands, or a different approach altogether. Other times, when the locality is just not working out, you might have to tell Arma to execute things elsewhere. See the next section for that.
Public variables can be used when you can only detect a condition on a single machine, but want a bunch of things to happen on all or other machines.
In the detecting trigger, you set and publish a variable.
In the reacting trigger, you check for that variable.
With that, you can set the server only flag how you want it for either trigger.
Make sure to initialize the variable (aka "give it a meaningful starting value that won't blow things up") in the custom_scripts.sqf.
Make sure to use a meaningful name for you variable. I usually use something like bso_mission_whatever_this_is_about
. E.g. bso_mission_players_have_been_detected
. If in doubt, make the name longer but more expressive.
And please give the triggers some meaningful names as well and keep them close together. Otherwise it's an absolute pain to hunt them down.
Be aware that the name of the variable can't be the same as the variable name of some object. If you have a vehicle with blufor_mhq
as variable name, you can't use a variable with the name blufor_mhq
without things going haywire.
Final note: global variables are not exactly the most clean, elegant, or performant way of doing this. So don't overdo it.
Note: I use "Trigger A" and "Trigger B" here because it's easier to write / remember. They are of course terrible names and shouldn't be used in a mission.
Trigger A
Trigger B
Trigger A is server only, because the detection of players is something that only the AI (living on the server) really know about, and it's comparatively heavy on performance. So we want to keep it on the server.
Trigger B is not server only, because we want it to happen everywhere. Once Trigger A signals that Trigger B should activate (by setting and publishing the variable bso_mission_players_detected
), Trigger B will show a hint on every machine.
Trigger A
Trigger B
Trigger A checks separately on every machine (aka not server only), if the player has a specific item in their inventory.
It does also check that same thing on the server. But because the server doesn't have a player, it can never activate there. But that's ok.
Trigger B does two things:
How does it know what to do where? Well.. it doesn't. I just does all the things everywhere. The message won't do much on the server, and the task is ignored on the client.
That might seem a little inefficient, but the impact is minimal, and it makes our life as mission maker a lot easier.
That's the kind of "trick" that make locality so confusing at first. But once you go line by line, machine by machine, it should become clearer.
Action
Trigger
The action is executed only on the client. The setDamage command has Global Argument / Global Effect, so we can do that on the client even if the trucks are local to the server.
But setting the task has to happen on the server. So we use a public variable to signal the server only Trigger that it should activate.
Side Note: the action was initially added on every client, using the appropiate section in mission/custom_scripts.sqf .
Trigger
If you recall, the locality of a vehicle follows the current driver.
In this example, we want to check if a vehicle is too close to an EMP source. We can sort of do that on every client, because the position of both is known.
To make sure that there is no inaccuracy with desync, we would ideally only check on the current driver's PC (probably wouldn't matter usually, but let's assume for the sake of practice).
Then if it is to close to the source, we want to kill the engine. That's a command with local argument and global effect, so it has to happen on the driver's PC.
How to we make sure it only ever runs on the driver's PC? We don't! We just execute it everywhere!
On the driver's PC, it will do exactly what we want. On every other PC it won't do anything because the setHitpointDamage won't have an effect unless the vehicle is local. Even if there's desync.