Asset Pack Script Scope
An Asset pack ships pieces — prefabs, decks, materials, textures, and the scripts that give those pieces behaviour. A game pack depends on it and places its pieces. This page is about what happens at the table, once a game that depends on your pack is actually being played. It is not an authoring guide; for that see Building an Asset Pack.
Four facts govern everything here, and none of them is guessable from the editor.
1. A pack's script runs under the pack's scope, not the game's#
When a table loads, it does not run one script host. It runs one host per scope: one for the game, and one more for every Asset pack the game depends on that ships a script. A pack's scope id is:
pack:<packId>@<version>
That is not a label. Saved data is stored under a key composed from the scope id and the key your
script names, and your script only ever supplies the second half — the host adds the prefix, and
your code has no way to see it, spell it, or reach past it. There is no
getSavedData("someOtherPack:counter"); the parameter to
world.getSavedData is the key, never the scope.
So the isolation is structural, not a rule anyone has to remember to enforce:
- One pack's script cannot read another pack's saved data.
- A pack's script cannot read the consuming game's saved data either. The game keeps its own mod id as its scope, unchanged. The script belongs to the pack that shipped it, not to the game that imported it.
- The two are also separate realms — separate sandbox frames — so there is no shared closure and no shared global to reach through. Everything in Sandbox limits applies to each of them independently.
What is not isolated is the table itself. Every scope's script still sees the same entities and the same host authority rules; a pack's script can act on a piece the game spawned. The partition is over saved data and code, not over the world.
2. The scope is pinned to the version — saved data does not follow a bump#
The scope id contains the pinned version, not just the pack id. Publish 1.2.0 of your pack, have
a game bump its dependency to 1.3.0, and the scripts in 1.3.0 start with an empty saved-data
scope. The 1.2.0 data is still on the table; nothing reads it any more.
This is deliberate, and it is the one fact most likely to surprise you in a bug report rather than in the editor:
- Treat every version bump as a fresh install for anything your script persisted. A counter, a per-game tally, a cached layout — all of it starts over.
- Do not put anything a player would mourn in a pack's saved data unless you are prepared to keep the pack's version stable, or to accept the reset.
- The alternative — carrying data forward across versions — would mean a
1.3.0script reading state written by a1.2.0script it has never seen, with no migration and no way to validate it. Losing a counter on a bump is a smaller failure than silently inheriting a shape that has changed.
If a value genuinely must survive, it belongs on the piece as replicated object state that the game owns, not in the pack's script scope.
3. Every script belongs to a prefab. A pack has no always-on scripts#
A game pack can list scene-level scripts that run for the whole table from the moment it loads
(see Execution order). An Asset pack cannot. The
sceneScriptIds key is refused by name when the pack is parsed:
A component pack may not attach scene-level scripts. It has no scene of its own: every script it ships must belong to a prefab (
metadata.scriptId), so that a player reading the capability disclosure can be told WHICH piece runs code. An always-on script belongs to the game pack that composes this one.
The binding runs the other way too, and this is the security-relevant half: a prefab's
metadata.scriptId must resolve inside the pack's own document. A prefab pointing at a script
the pack does not ship is refused at parse:
A prefab references a script this pack does not ship. A pack's prefab must never resolve against the consuming game's script table: an id collision would silently bind this piece to somebody else's code. Ship the script in this pack, or remove the reference.
That rule is what stops a pack from binding to a name and hoping the game that imports it happens to have one. There is no late binding, no fallback, and no lookup in the game's script table. The consequences for an author:
- A behaviour that must run before any of your pieces exist has nowhere to live in a pack. It belongs in the game pack, or on a piece the game is expected to place.
- The reverse also holds — a script nothing references is refused rather than shipped dead, because an unattached script can never run and can never be attributed to a piece in the capability disclosure a player reads before joining.
- The script must carry a compiled body. A source-only script ships a prefab that silently does
nothing; see
component-pack-script-not-compiled.
Because every script is attached to a named piece, the capability disclosure a player sees can name which piece runs code — which is the reason the rule exists at all, rather than tidiness.
4. A pack contributes a template, not a placement#
A prefab is a definition. It structurally cannot carry a position — there is no field to write one into, and a prefab that tries is refused rather than silently stripped:
A prefab carries no position. It is a definition, not a placement: the game that depends on this pack decides where each copy goes.
What your pack contributes to the consuming game is a template, addressable as
<packId>.<prefabId>. The game's scene references that template and supplies the position and the
rotation. One game can place your piece in four spots; another can place it once and hide it.
For a script, the practical consequence is: never assume where your piece is, how many of it exist, or that any of it exists at all. The same script body runs on every instance the game placed. Read the entity you were given rather than searching for a position you expect, and treat "none of my pieces are on this table" as a state your script must survive rather than a bug.
See also#
- Building an Asset Pack — the authoring side of everything above.
- Execution order — scene scripts, object scripts, and what already exists when yours runs.
- Host authority — which peer runs a script, whichever scope it is in.
- Sandbox limits — the per-frame hardening every scope gets.
- What gets rejected — the pack rules as refusal codes.
