The Moddable Root pattern — offer modding without making your whole game moddable
Most studios do not want every scene in the game opened up to modders. They want one sanctioned room where mods are made, and a way to play what people make — without exposing the boss fight, the save system, or the storefront.
That is this pattern. Two roles, one spine:
| Role | Who runs it | What it is |
|---|---|---|
| Authoring Root | the modder | A bare, deliberately small scene with the BedderScript faces (F1–F4) mounted. Everything the modder can touch is in this scene. That boundary is the mod surface. |
| Player / Loader | your end user | The same room with the editor faces stripped out. It picks a saved mod (or an imported .abspack) and replays it. |
Read §3 before you design anything. The single constraint in it decides your scene layout.
1. How BedderScript modding actually works
Nothing here is a scene snapshot. A mod is a booklet — an ordered list of small ops that replay onto your shipped scene.
Documents/<productName>/
Scripts/ <- .bedder files the player can edit (BsScriptStore)
Assets/ <- imported models/images/audio + catalog.json (BsAssetCatalog)
SceneMods/
<SceneName>.json <- the WORKING booklet for that scene
<SceneName>/Saves/<name>/
booklet.json <- a named save: a snapshot of the working booklet
card.json <- name / description / author / created / thumbnail / opCount
(On iOS / tvOS / Android there is no Documents; the root is Application.persistentDataPath —
Runtime/Components/BsScriptStore.cs:34.)
The moving parts, all real types in this package:
| Piece | Type | Where |
|---|---|---|
| One op | BsSceneOp (op / id / kind / name / value / parent) |
Runtime/Components/BsSceneDelta.cs:21 |
| The booklet | BsSceneDelta.ops |
Runtime/Components/BsSceneDelta.cs:38 |
| The op vocabulary | create · import_asset · create_from_asset · set_texture · destroy · rename · add_component · remove_component · set_property · assign_script · remove_script · reparent · create_terrain · set_terrain_heights · set_terrain_alphamaps · set_terrain_details |
BsSceneDelta.cs:14-18 |
| The replay robot | BsSceneApply.Run(BsSceneDelta) returns BsApplyReport |
Runtime/Components/BsSceneApply.cs:40 |
| Booklet storage | BsSceneModStore (Append · Save · Load · SaveAs · ListSaves · ActivateSave · ClearWorking) |
Runtime/Components/BsSceneModLoader.cs:15 |
| Boot replay | BsSceneModLoader (MonoBehaviour) |
Runtime/Components/BsSceneModLoader.cs:190 |
| Object identity | BsId.guid, BsId.Ensure(go), BsId.Find(guid) |
Runtime/Components/BsId.cs:10 |
Ops address objects only by BsId guid. An object with no BsId cannot be targeted by a
mod — that is your first and bluntest exposure control (§5).
There is one code path: the in-session LOAD button and the boot-time replay both call
BsSceneApply.Run. Never fork it. A missing target skips politely and lands in
BsApplyReport.Notes — a mod cannot brick your scene (BsSceneApply.cs:1-3, :48-57).
2. What a modder actually does in the root
With the faces mounted, a modder in a shipped build:
| Key | Face | What it gives them |
|---|---|---|
| F1 | CODE (BedderEder) | Write / edit .bedder scripts. APPLY hot-swaps into the live BedderScriptRunner. |
| F2 | Blox | Blocks over the same script. One script, two faces. |
| F3 | Scene | Hierarchy, inspector, fly camera, gizmos, Project window, Import, Save as… / Open menu. |
| F4 | Bug report | Screenshot + repro to Documents/<product>/Bugs/. |
Every click in F3 stages an op. Nothing is written until LOAD, which appends the staged ops
to the working booklet (BsScenePanel.cs:5533 calls BsSceneModStore.Append). Save as… then
snapshots that booklet under a name (BsSceneModStore.SaveAs, BsSceneModLoader.cs:112).
3. The constraint that decides your scene layout
Booklets are keyed by the name of the ACTIVE scene. Not by a GUID, not by a reference you set
— by SceneManager.GetActiveScene().name, in four places:
BsSceneModLoader.OpenSave—Runtime/Components/BsSceneModLoader.cs:205BsSceneModLoader.ResetScene—:215BsSceneModLoader.Start(the replay) —:226- the F3 panel's
SceneNameproperty —Runtime/IdeLib/Scene/BsScenePanel.cs:395
There is no scene-name override seam. So a mod authored in a scene called ModRoot replays
only where the active scene is also called ModRoot.
That rules out the naive reading of "two scenes" — an Authoring.unity and a separate
Player.unity will never see each other's mods. Two shapes actually work:
Shape A — one scene asset, two rigs (start here)
ModRoot.unity is both roles. A tiny boot component decides which loaders are alive:
- authoring —
BsIdeLoader+BsBloxLoader+BsScenePanelLoader+BsBugReportLoader+BsSceneModLoader - play —
BsSceneModLoaderonly, plus your own save picker
The booklet key matches by construction. This is the shape to ship first.
Shape B — a shell scene that loads the root additively
Your game's menu lives in its own scene; the moddable room is loaded on top:
var op = SceneManager.LoadSceneAsync("ModRoot", LoadSceneMode.Additive);
op.completed += _ => SceneManager.SetActiveScene(SceneManager.GetSceneByName("ModRoot"));
SetActiveScene is not optional — without it the active scene is still your shell and the
booklet key is wrong.
Sharp edge.
BsSceneModLoader.OpenSaveandResetScenecallSceneManager.LoadScene(scene)in Single mode (BsSceneModLoader.cs:208,:218). In Shape B that destroys your shell scene. Either put the save picker insideModRoot, or make your shell aDontDestroyOnLoadbootstrap object instead of a scene. Also:ModRootmust be in Build Settings, because both calls load it by name.
4. Scene A — the Authoring Root
What goes in it
Only what you are willing to let a modder see, move, delete, re-parent, re-script and re-texture. A good root is boring: a floor, a light, a spawn point, the handful of prefabs that define your game's vocabulary, and the player controller. Everything a modder needs to make something that feels like your game — and nothing else.
The rig
One GameObject, conventionally BS_IDE, carrying:
| Component | Namespace | Purpose |
|---|---|---|
BsIdeLoader |
Showcase |
F1 CODE face (kb.f1Key, BsIdeLoader.cs:120) |
BsBloxLoader |
Showcase |
F2 blocks (BsBloxLoader.cs:92) |
BsScenePanelLoader |
Showcase |
F3 scene panel (BsScenePanelLoader.cs:98) |
BsBugReportLoader |
Showcase |
F4 bug card (BsBugReportLoader.cs:38) |
BsSceneModLoader |
BedderScript.Unity |
boot replay (package type) |
Plus an EventSystem with an InputSystemUIInputModule — BedderScript is new Input System
throughout; every face key is read from UnityEngine.InputSystem.Keyboard.current.
Be clear about where these come from. The four
Bs*LoaderMonoBehaviours are showcase reference code shipped in the ABS project payload underAssets/ArTchie Studios/Showcase/, not types insidecom.bedder.script. The package ships the faces —EderCodeView,BsScenePanel,BsPackageImportCard,BsBugReport. The loaders are roughly 120 lines each that mount a face on a canvas and bind a key. Copy them into your own namespace and adapt them; that is what they are for (MANUAL.md:340).
Sticker every object
BsSceneApply addresses objects by BsId.guid. Objects without one are invisible to the booklet.
Run once per scene and commit the scene:
Tools / BedderScript / Add BsIds To Open Scene — BsIdBaker.cs:12
Tools / BedderScript / Set Up Bedder Editor in Scene does both jobs at once: creates the
UI_EventSystem, creates BS_IDE with all five components, and stickers every object
(BsEditorSceneSetup.cs:19-53).
Seed the starter scripts
Player builds have no Assets/ tree. BsShipScriptsOnBuild (an IPreprocessBuildWithReport,
callbackOrder 40) mirrors your editor scripts folder into StreamingAssets/BedderScripts at
build time; on first run BsScriptStore.EnsureSeeded() copies them into
Documents/<product>/Scripts so the modder starts with worked examples they can edit
(BsShipScriptsOnBuild.cs:9-16, BsScriptStore.cs:98).
5. Choosing the mod surface — the four dials
The root scene's contents are the coarse control. These four are the fine ones.
Dial 1 — BsId coverage
No BsId, no ops. If you additively load a system scene alongside the root (your audio manager,
your analytics), leave those objects unstickered and the booklet cannot name them. This is the
cheapest, most reliable fence you have.
Dial 2 — the script host surface
What user scripts can do is exactly what your host exposes. ExtensibleEngineHost.RegisterApi
is the seam (Runtime/Embedding/ExtensibleEngineHost.cs:23):
var host = new ExtensibleEngineHost();
host.RegisterApi("spawn_enemy", args => { /* your verb, your rules */ return Value.Null; });
Runtime/Body/BsBodyHost.cs:24-60 is a worked example — a whole platformer body API registered as
roughly twenty named verbs.
Honest limit.
BedderScriptRunnerconstructsnew UnityEngineHost()directly (Runtime/Components/BedderScriptRunner.cs:47), and so doesBedderScriptBehaviour(BedderScriptBehaviour.cs:23). Neither exposes a host factory. To run scripts on a scoped host today you driveBedderScriptDirector, which takesmakeHostandmakeLimitsdelegates (Runtime/Embedding/BedderScriptDirector.cs:41-46), or you subclass the runner. If you go the Director route, note thatassign_scriptops createBedderScriptRunnercomponents (BsSceneApply.cs:479-480) — a scoped-host game should decide whether to allow that op at all.
Related: the default Unity host gives scripts whole-scene reach (find plus destroy /
set_position on anything nameable). That is machine-safe but game-integrity-trusting. If mods
must not grief the scene, scope a host that resolves names against an allow-list
(SECURITY.md:56-59).
Dial 3 — sandbox limits
SandboxLimits.ForIos is the default everywhere (50 000 instructions per frame, 2 s wall clock,
1 000 spawned objects). SandboxLimits.ForDesktop raises them roughly ten-fold and the wall clock
to 30 s (Runtime/Core/Runtime/SandboxLimits.cs:26-38). Every field is settable. A hostile
script's worst case is that it trips a limit and its invocation stops with an error your host sees
(SECURITY.md:45).
Dial 4 — the module resolver
ScriptEngine.ModuleResolver maps import names to source (Runtime/Core/ScriptEngine.cs:49).
import names are attacker-chosen strings. If you map them to file paths, canonicalize — a
../../secrets that works is your bug, not the sandbox's (SECURITY.md:50-53).
Where to point the user folders
All three roots are overridable, so you can put mod data beside your own saves:
| Override | Type | Default |
|---|---|---|
BsScriptStore.UserScriptsRootOverride |
Func<string> |
<UserDataRoot>/<product>/Scripts (BsScriptStore.cs:49) |
BsAssetCatalog.RootOverride |
string |
<UserDataRoot>/<product>/Assets (BsAssetCatalog.cs:33) |
BsSceneModStore.RootOverride |
string |
<UserDataRoot>/<product>/SceneMods (BsSceneModLoader.cs:18) |
BsSceneApply.ScriptSourceResolver |
Func<string,string> |
reads <scriptsRoot>/<stem>.bedder (BsSceneApply.cs:38) |
Set them before first access. BsScenePanelLoader.cs:34-47 shows the resolver swap done in
Awake, deliberately early enough that boot-time replay already sees it.
6. Trust tiers — there are TWO, and they are not the same
This is the part people get wrong. Do not conflate them.
Tier 1 — the script sandbox (narrow, audited)
A .bedder script cannot reach the CLR, the filesystem, the network, or arbitrary Unity API.
No reflection on script input. No eval, no IL, no codegen. No IO capability exists to misuse.
Walking an exposed object into CLR metadata is impossible by construction, not by configuration
(SECURITY.md:17-29). The guarantee is the sum of what your host exposes — see Dial 2.
Tier 2 — the booklet and the asset catalog (deliberately WIDER)
The booklet and the catalog are the player modding their own machine — the same trust tier as
editing a save file. Within that tier they get powers scripts deliberately do not
(SECURITY.md:72-84):
add_component/set_propertyresolve any loaded component type by name and set members by reflection (BsSceneApply.ResolveComponentType,BsSceneApply.cs:675-677). The in-game inspector needs full reach. That is the local tier working as designed.- Asset files pass an extension allow-list (image / audio / model only — never code), a 50 MB
cap, and are sha-deduped copies (
BsAssetCatalog.cs:112-170). - Malformed model / texture bytes fail the import; they do not execute.
The line you must not cross yet
A booklet downloaded from another player is not save-file tier — it is hostile-input tier.
SECURITY.md:86-91states the three things that must exist before any booklet or asset sharing feature ships: (1) an op allow-list mode foradd_component/set_property, (2) rejecting catalogfileNameentries containing path separators at load, (3) a per-file booklet op cap. None of those exist today. Until they do, the only supported booklet source is the local player's own machine.
Practical reading for a studio: content packs (.abspack) are the sanctioned sharing container.
Raw booklets are not. A pack carries assets and scripts through the audited import allow-list.
A booklet carries a reflection-powered op list through nothing.
7. Scene B — the Player / Loader
Strip the four editor loaders. Keep BsSceneModLoader. Add your own front end.
Replay a mod
// Resume the working booklet automatically on Start:
sceneModLoader.autoLoad = true; // BsSceneModLoader.cs:195 — OFF by default
autoLoad off means the scene starts pristine and the player chooses. Start() calls
BsAssetImporters.Preload(delta.ops, …) first so async imports finish before the robot runs, then
BsSceneApply.Run(delta) (BsSceneModLoader.cs:221-234). Do not call Run yourself without the
preload — asset ops will skip with a note.
A save picker, in about ten lines
using BedderScript.Unity;
foreach (BsSceneSaveCard card in BsSceneModStore.ListSaves(sceneName))
{
// card.name · card.description · card.author · card.created · card.opCount · card.folder
AddButton(card.name, () => BsSceneModLoader.OpenSave(card.name));
}
AddButton("Reset", BsSceneModLoader.ResetScene);
OpenSave makes that save the working booklet, reloads the scene pristine, and replays exactly
once — even when autoLoad is off (BsSceneModLoader.cs:203-210, :197-199). ResetScene drops
the working booklet and reloads the shipped scene (:213-219). BsSceneSaveCard already carries
description / author / thumbnail fields so a richer picker is a UI change, not a format
change (BsSceneModLoader.cs:177-186).
Import a .abspack without shipping the whole F3 panel
BsPackageImportCard is a standalone modal you can mount on any canvas — it does not require
BsScenePanel (Runtime/IdeLib/Scene/BsPackageImportCard.cs:18):
using BedderScript.Ide.Scene;
var card = new BsPackageImportCard
{
ScriptsRoot = () => BsScriptStore.UserScriptsDirectory, // where .bedder files land
Status = msg => ShowToast(msg),
Imported = () => RefreshMyLibraryUi(),
};
card.Open(myCanvas.transform, absolutePathToPack);
The card shows the same ritual as Unity's own import dialog: a folder tree, a tick per item,
All / None, and a finish line naming what was skipped and why (BsPackageImportCard.cs:1-6,
:185-210). On Import:
- models / images / audio go to
BsAssetCatalog.ImportFileintoImports/, sorted and bundled (:883) .bedderfiles go straight into the scripts root, no conversion (:986-998).csfiles are converted byBsCsToBedder, on by default, unticked by the user if they prefer (:1012). The converter is a translator assistant that marks what it could not map withTODO(bs), not a compiler. The finish line reports the flag count.
BsUnityPackage.ReadManifest pulls the pack's identity card — name / author / description /
created from abspack/manifest.json (Runtime/Importers/BsUnityPackage.cs:45, :120). Plain
.unitypackage files have no manifest and return null; both extensions import identically.
8. Distribution and the rights confirmation
Today the pack is authored from the Unity Editor:
Tools / BedderScript / Export ABS Pack… (BsAbsPackExporter.cs:40)
Pick assets or folders in the Project window, + Add selection, tick, Export. Prefabs bake to
.bsmesh (mesh + material colors + albedo textures, which ride along beside the mesh); models,
textures, audio and .bedder files go in as bytes. Meshes with Read/Write disabled get their
importer flipped for the bake and restored afterward — the user's import settings stay theirs
(BsAbsPackExporter.cs:189-244). The container is written by
BsAbsPackWriter.Write(outPath, entries, manifest) — gzip + tar, the same layout as a
.unitypackage plus abspack/manifest.json (Runtime/Importers/BsAbsPackWriter.cs:25).
The rights gate is not optional, and you should not remove it
The Export button sits inside an EditorGUI.DisabledScope that requires a pack name, at least one
ticked item, and a ticked rights confirmation (BsAbsPackExporter.cs:125-128). Above it sits a
warning that says, verbatim:
You are responsible for the contents of this pack. Only include assets you have the RIGHT to redistribute — your own work, or content whose license explicitly allows it. Third-party Asset Store purchases, extracted game files, and most downloaded content may NOT be redistributed. Shipping someone else's assets without permission violates their license and is often illegal.
If you build your own exporter — in-game or otherwise — carry this gate forward. The person exporting is the one who must hold redistribution rights, and a modding feature that quietly makes it easy to redistribute a paid Asset Store pack is a liability you are handing your players.
9. The recipe
Authoring Root
File / New Scene, name itModRoot. Keep it deliberately small: floor, light, spawn point, your vocabulary prefabs, the player controller. Nothing else.- Add the player's
BedderScriptRunnerand point it at a starter.bedder. - Run
Tools / BedderScript / Set Up Bedder Editor in Scene. This createsUI_EventSystem(EventSystem+InputSystemUIInputModule), createsBS_IDEwithBsIdeLoader,BsBloxLoader,BsScenePanelLoader,BsBugReportLoader,BsSceneModLoader, and stickers every object with aBsId. - Leave
BsSceneModLoader.autoLoadoff for authoring — a modder wants the pristine room and opens their save deliberately. - Put worked example
.bedderfiles in your editor scripts folder so they ship into the build and seed intoDocuments/<product>/Scriptson first run. - Add
ModRootto Build Settings. Save the scene — theBsIdguids only persist if you do.
Player / Loader
- Duplicate the rig decision, not the scene. Add a small boot component to
ModRootthat disables the four editor loaders when the game launches in play mode:
using BedderScript.Unity;
using UnityEngine;
public sealed class ModRootBoot : MonoBehaviour
{
public bool authoringMode; // set by your launcher / menu / build define
public Behaviour[] editorFaces; // BsIdeLoader, BsBloxLoader, BsScenePanelLoader, BsBugReportLoader
public BsSceneModLoader modLoader;
private void Awake()
{
foreach (var face in editorFaces) { if (face != null) { face.enabled = authoringMode; } }
if (modLoader != null) { modLoader.autoLoad = false; } // the picker decides
}
}
Disable in Awake, before the loaders' Start builds their canvases.
- Build your player-side front end from §7:
BsSceneModStore.ListSavesto buttons toBsSceneModLoader.OpenSave, plusBsSceneModLoader.ResetScene, plus aBsPackageImportCardif you accept.abspackcontent. - If you want a separate shell scene, use Shape B in §3 — additive load plus
SceneManager.SetActiveScene— and read the Single-mode sharp edge before you commit to it.
Distribution
- Author packs with
Tools / BedderScript / Export ABS Pack…. Keep the rights gate. - Tell modders where their work lives:
Documents/<product>/SceneMods/ModRoot/Saves/<name>/.
10. Where this pattern is incomplete — read before you promise modding
These are real gaps, verified by reading the shipping code. They do not stop the pattern; they bound what you can honestly advertise.
A named save does not carry its scripts or its assets
BsSceneModStore.SaveAs writes exactly two files: booklet.json and card.json
(BsSceneModLoader.cs:112-129). Nothing else. And the booklet references content indirectly:
- Scripts by stem.
assign_scriptstores onlyop.name(the file stem); the source text is resolved at replay time byScriptSourceResolverfrom the local scripts folder (BsSceneApply.cs:472-478). Script text is never in the booklet. - Assets by catalog key.
import_assetstoresop.id= the catalog key and looks it up withBsAssetCatalog.Find(BsSceneApply.cs:141-147). Asset bytes are never in the booklet.
So a save is portable only within one machine's Documents tree. Copy
Saves/<name>/booklet.json to a second machine and every assign_script and import_asset op
skips with a note.
Catalog keys are per-machine random GUIDs
Worse than a missing-file problem: BsAssetCatalog.ImportFile mints
key = Guid.NewGuid().ToString("N") for every import (BsAssetCatalog.cs:260). The key is not
derived from content. Re-importing the identical file on a second machine produces a different
key, so a transplanted booklet cannot be repaired by re-importing the same assets. There is a
sha256 on every entry (BsAssetCatalog.cs:23) and a sha-based dedupe path within one machine
(:221), but nothing resolves a booklet op by sha. A content-hash fallback in
BsSceneApply.OpImportAsset is the obvious fix, and it is not written.
Same-stem script collision on import
When a .abspack carries player.bedder and the recipient already has one, the import lands it
as player_2.bedder (BsPackageImportCard.cs:990-994). A booklet op saying
assign_script "player" then binds the recipient's script, not the pack author's. Ship
scripts with distinctive stems.
There is no in-game exporter
BsAbsPackExporter is an EditorWindow behind #if UNITY_EDITOR
(BsAbsPackExporter.cs:10-23). Nothing in Runtime/ calls BsAbsPackWriter — a search across
the package finds the writer referenced only from that editor tool. So today:
- A developer can author and ship an
.abspack. - A player in a shipped build can import one (
BsPackageImportCard) but cannot create one. The round trip does not close in the player.
The container writer deliberately lives in the runtime assembly, so this is a UI job rather than an architecture job — but the UI does not exist. If you build it, carry the §8 rights gate.
Booklet sharing is not hardened
Restated because it matters: §6's three prerequisites are not implemented. Do not build a "download this mod" button on top of raw booklets.
What that adds up to
| You want to offer | Supported today |
|---|---|
| Modders build in a sanctioned room, in a shipped build | Yes |
| Their work persists across restarts on their machine | Yes (named saves) |
| You ship them starter content and vocabulary | Yes (.abspack + seeded scripts) |
| They hand a finished mod to another player | No — no in-game export, no portable asset keys, no hardened booklet import |
Design your announcement around that table.
11. API quick reference
| Call | Signature | File |
|---|---|---|
| Replay a booklet | BsApplyReport BsSceneApply.Run(BsSceneDelta) |
Runtime/Components/BsSceneApply.cs:40 |
| Script source seam | Func<string,string> BsSceneApply.ScriptSourceResolver |
BsSceneApply.cs:38 |
| Preload async assets | void BsAssetImporters.Preload(IEnumerable<BsSceneOp>, Action) |
Runtime/Components/BsAssetImporters.cs:94 |
| Working booklet path | string BsSceneModStore.PathFor(string sceneName) |
Runtime/Components/BsSceneModLoader.cs:26 |
| Append staged ops | void BsSceneModStore.Append(string, IEnumerable<BsSceneOp>) |
:34 |
| Snapshot under a name | BsSceneSaveCard BsSceneModStore.SaveAs(scene, name, description, author) |
:112 |
| List saves | List<BsSceneSaveCard> BsSceneModStore.ListSaves(string sceneName) |
:131 |
| Open a save | bool BsSceneModLoader.OpenSave(string saveName) |
:203 |
| Back to pristine | void BsSceneModLoader.ResetScene() |
:213 |
| Auto-replay flag | bool BsSceneModLoader.autoLoad |
:195 |
| Stamp an id | BsId BsId.Ensure(GameObject) |
Runtime/Components/BsId.cs:37 |
| Find by id | BsId BsId.Find(string guid) |
BsId.cs:16 |
| Scan a pack | List<BsUnityPackageItem> BsUnityPackage.Scan(path, out why) |
Runtime/Importers/BsUnityPackage.cs:53 |
| Read pack identity | BsAbsPackManifest BsUnityPackage.ReadManifest(path) |
:120 |
| Write a pack | void BsAbsPackWriter.Write(outPath, List<Entry>, BsAbsPackManifest) |
Runtime/Importers/BsAbsPackWriter.cs:25 |
| Mount the import modal | void BsPackageImportCard.Open(Transform parent, string packagePath) |
Runtime/IdeLib/Scene/BsPackageImportCard.cs:185 |
| Register a host verb | void ExtensibleEngineHost.RegisterApi(string, Func<Value[],Value>) |
Runtime/Embedding/ExtensibleEngineHost.cs:23 |
| Scoped host + limits | new BedderScriptDirector(makeHost, makeLimits) |
Runtime/Embedding/BedderScriptDirector.cs:41 |
| Sandbox presets | SandboxLimits.ForIos · SandboxLimits.ForDesktop |
Runtime/Core/Runtime/SandboxLimits.cs:26 |
See also
SECURITY.md— the full threat model, the enforced limits table, the embedder's duties, and the local-tier scope note.Documentation/Guide-Bedder.md— the five-minute tour of the four faces.MANUAL.md§6 (the scene-op spine) and §7 (the in-game editor, face by face).
Source in the box: Assets/ArTchie Studios/ArTchie-Bedder-Suite/com.bedder.script/Documentation/Moddable-Root-Pattern.md