# Asset Pack Script Scope

> A prefab's script runs under its own pack scope, pinned to a version, so it cannot read another pack's saved data or the game's - and what follows for authors.

- Source: https://diceytable.com/docs/scripting/concepts/pack-script-scope
- Last verified: 2026-08-20

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](https://diceytable.com/docs/editor/workflows/build-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:

```text
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`](https://diceytable.com/docs/scripting-api/world#worldgetsaveddata) 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](https://diceytable.com/docs/scripting/concepts/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](https://diceytable.com/docs/scripting/concepts/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.0` script reading
  state written by a `1.2.0` script 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](https://diceytable.com/docs/scripting/concepts/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`](https://diceytable.com/docs/mods/scanner/what-gets-rejected#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](https://diceytable.com/docs/editor/workflows/build-an-asset-pack) — the authoring side of everything above.
- [Execution order](https://diceytable.com/docs/scripting/concepts/execution-order) — scene scripts, object scripts, and what already exists when yours runs.
- [Host authority](https://diceytable.com/docs/scripting/concepts/host-authority) — which peer runs a script, whichever scope it is in.
- [Sandbox limits](https://diceytable.com/docs/scripting/concepts/sandbox-limits) — the per-frame hardening every scope gets.
- [What gets rejected](https://diceytable.com/docs/mods/scanner/what-gets-rejected) — the pack rules as refusal codes.
