Multiplayer rules for scripts
Your script runs inside the simulation, on every computer in the game. If two computers get the same commands in the same order, they must end up with the same game. So a script has to give the same answers on both.
The rules
- Random numbers are fine.
game.Random(n)gives a whole number from 0 up ton, andgame.RandomFloat()one from 0 up to 1. They come from the game's seed: every player in a match, and every replay of it, draws the same numbers in the same order. Go'smath/randworks the same way here, because the sandbox seeds it from the game and not from the machine. - The clock is the game's clock.
game.Time()andevent.Everyare what you want.time.Nowandtime.Sincework too, but they report game time (counting from the year 2000), never the time on the computer. - Do not depend on map order. Go goes through a
mapin a different order each time on purpose. Here that order comes from the game's seed, so it is the same everywhere, but it changes when the seed does. If the order matters, because you spawn units or give orders in the loop, range over a slice, or sort the keys first. With the standard Go compiler the editor does this for you when it builds the map: afor ... rangeover a map whose key is a number, a string, a bool or a struct or array of those walks the keys in ascending order, andmaps.Keys,maps.Valuesandmaps.Alldo the same (a key with a pointer or interface in it cannot be ordered, and the build says so with the line number). The sort costs about 80 microseconds for a map of 1,000 entries. If you want the order without relying on the build,w4/maporderhasKeys,KeysFuncand an orderedMaptype. - Keep state in the script. A global variable is fine. It lives in the script's memory, and the same events give the same values everywhere.
- Do not wait for anything outside. There is no disk and no network for a script anyway.
The sandbox closes every door to the machine. When a script asks the system for randomness or the time, it gets the game's seed and the game's time. That is what lets a replay or an online match play out the same on every computer.
Randomness that does not need to agree
Something only one player sees, like a particle, a flicker of the interface or a camera shake, has no reason to match the other computers, and it would be wasteful to make it. Scripts that run on a single player's screen only are planned (see What is next). They will be allowed the real clock and real randomness, and they will not be allowed to change the game directly. If such a script rolls a value that matters, it sends it to the game as a command: the server applies it and every computer gets the same result.
Presentation is separate
Calls in w4/ui go to the players they are meant for. They are not part of the simulation's state. A script can show each player something different, and the game stays in step. Clicks and slash commands come back to the script as normal events, so every computer runs the handler.
Limits
Each call into your script, whether a handler, a timer or a click, may use about 200 million instructions. The count starts again for the next call. A script's memory is capped at 256 MB.
If a handler goes over, usually an endless loop, the game stops the script for the rest of the game and writes the reason to the log (script stopped in ...). The game itself keeps running. Check F12 if your map suddenly stops reacting.
A habit worth having
Write handlers so that doing nothing is safe. A unit may have died before your order arrives, and a player may click a button twice. The handle checks, Alive() and the like, are cheap.
Questions modders ask
My map works alone but two players see different things. Why? Almost always the script read something that differs between computers: the real time, a map iteration, a value from w4/ui that was stored back into the game, or a handler that only runs for the local player. Move the decision into an event handler that every computer runs, and keep what is shown to one player in w4/ui calls.
Can I use math (sin, cos, sqrt)? Yes. Go's own math package is plain Go compiled to WebAssembly and gives the same bits everywhere. The engine's own simulation uses the same deterministic maths for movement.
Can I use floating point at all? Yes. WebAssembly floats are IEEE, so the same operations give the same results on every machine. Keep the habit of comparing with a small margin instead of == for values you computed.
Why can't I read the local player's camera or selection? Those differ for every player, so a decision based on them would split the game. They belong to the interface (w4/ui), which is not part of the simulation.
Does the order of a for ... range over a map matter? Not for the games you build in the editor. Go walks a map in an order that depends on a hidden random number, so two plays of one script could differ. The build step rewrites every for k, v := range m over a map whose keys are numbers or strings to walk the keys in ascending order, so the script plays the same whatever the game's seed (your files are not changed, line numbers in errors stay the same). An entry you delete before the loop reaches it is not visited, as in Go. Loops it cannot rewrite are listed in the build log with their line: a map with a struct or array key, a range over a function call (put the map in a variable first) and for k, v = range m with = instead of :=. Those still walk in Go's order, which is the same on every computer for one game seed but not between seeds. The rewrite costs one sort of the keys per loop. It is applied to the Go build; the TinyGo build is not rewritten.