ArTchie Studios
ArTchie-Bedder-Suite / Docs / BedderScript

BedderScript Manual

What the language and runtime do today — every promise here is verified by an automated proof battery (87 tests: 86 green + 1 [Ignore]d known gap). No roadmap material here; if it's in this manual, it runs.


1. Language

Variables & types

var speed: number = 8        // typed
var name = "player_one"      // inferred
var thing = null             // null-initialized ⇒ type `any` (assign anything later)
const MAX: int = 99          // const — assignment is a compile error

Types: number, int, float, bool, string, any, Vector2/3/4, Quaternion, Color, Rect, GameObject. Type annotations are checked; warnings are non-fatal, errors stop compile.

Control flow

if (x > 0) { } else { }
while (running) { }
do { } while (again)
for i from 0 to 10 { }           // range loop
for item in list { }             // arrays; for maps use: for k in keys(m) { }
switch (state) { case 1: ...  default: ... }
break / continue
var v = cond ? a : b             // ternary

Word operators: and, or, not. Comparison via == != < <= > >=. x ?? fallback null-coalesce. a is Type, a as Type casts.

Functions & lambdas

function damage(base: number, crit: bool) -> number {
    return crit ? base * 2 : base
}
var d = damage(10, true)         // script functions are callable from anywhere

var cmp = (x, y) => y - x        // lambdas: (a, b) => expr  or  x => expr
sort(list, cmp)

Known gap (honestly): lambdas do not capture enclosing locals yet — a lambda only sees its own parameters and globals. The battery carries the spec test, [Ignore]d until built.

Events

event scored(points: number)     // declare
on scored(points) {              // handle (any number of handlers)
    total = total + points
}
emit scored(15)                  // fire, with args

The BedderScriptRunner emits: awake (after top-level run), start, update(dt), fixed_update(dt), late_update(dt), enable / disable / destroy, and the physics set — trigger_enter / trigger_exit / trigger_stay and collision_enter / collision_exit / collision_stay (3D and 2D colliders, same names). Physics events arrive name-first: on trigger_enter(other_name, other) — the other object's name, then its GameObject; declare one parameter or two. Your game can broadcast anything else via the embedding door.

Waits (latency is native)

on start() {
    wait 2                       // seconds — wait(2) also parses
    wait_frames 10
    wait_until door_open
    log("all three waited")
}

Both surface forms are legal: wait 2 and wait(2) compile to the same opcode.

The VM suspends and resumes across ticks — no coroutine plumbing in your script.

Errors

try {
    throw "boom"
} catch (err) {
    log(err)
} finally { }

Classes (basic)

class Greeter {
    function shout(x: number) -> number { return x * 2 }
}

Classes parse and compile (methods, self, new); treat deep inheritance/super chains as lightly-proven — battery coverage is basic.

Modules

import mathx                     // loads module "mathx" ONCE, inline, flat namespace

The embedding application must install a resolver: engine.ModuleResolver = name => source. Security: module names are attacker-chosen strings — never map them to file paths without canonicalization (see SECURITY.md). No resolver ⇒ compile error IMP001 (imports are never silently ignored). Missing module ⇒ IMP002. Circular imports ⇒ IMP003.


2. Standard library

Function-style calls: push(arr, x), not arr.push(x). .length works on arrays/strings as a property; len(x) as a function.

Mathabs floor ceil round sign sqrt pow exp ln log sin cos tan asin acos min max clamp lerp smooth_step ping_pong move_towards repeat random random_range set_random_seed

Stringslen substring(s, start[, len]) split(s, sep) replace(s, a, b) trim to_upper to_lower starts_with ends_with contains index_of find

Arrayspush pop len contains index_of insert(a, i, v) remove_at(a, i) reverse join(a, sep) clear sort(a[, comparator]) — comparator is any function/lambda returning negative/zero/positive.

Mapskeys(m) values(m) has_key(m, k) remove_key(m, k) clear(m) — iterate with for k in keys(m).

Conversionsstr int float number bool typeof (note: typeof(x) in call position has a parse quirk — pending fix; is works for type tests).

Vectorsvec2(x, y) vec3(x, y, z) vec_length vec_norm vec_lerp(a, b, t) dot distance (vectors are maps with x/y/z fields — index them: p["x"]).

Diagnosticsprint log warn error assert(cond, msg).


3. Engine API (default Unity host)

Self & objectsself() (the GameObject the script is bound to), spawn(name), destroy(go), find(name), find_tag(tag), set_active(go, on), get_name(go) / set_name(go, s), get_tag(go) / set_tag(go, s).

Transformget_position(go) → vec3, set_position(go, x, y, z), translate(go, x, y, z), get_rotation(go) → euler vec3, set_rotation(go, x, y, z), set_scale(go, x, y, z), look_at(go, x, y, z), transform_direction(go, v) → vec3 (local direction to world space — replaces the sin/cos yaw projection dance in movement scripts). set_position, translate, look_at, set_rotation, set_scale, set_velocity and add_force also accept a vec3 in place of the three numbers: set_position(self(), target) works when target came from vec3()/get_position().

Physicsadd_force(go, x, y, z), set_velocity(go, x, y, z), raycast(origin_vec3, dir_vec3[, max_dist]) → hit info or null, move(go, dx, dy, dz) (collision-aware displacement — CharacterController.Move when one is present, translate otherwise), is_grounded(go) (CharacterController), check_sphere(x, y, z, r). The Runner also forwards OnControllerColliderHit as the event controller_hit(name, obj, dir_x, dir_y, dir_z).

Events between scriptssend_event(go, "name", args…) fires on name(...) handlers on the target object's scripts only (returns true when a running script received it). raise_signal remains the broadcast. Event chains are depth-capped by the sandbox, so two objects ping-ponging events cannot hang the game.

Componentsadd_component(go, "Rigidbody"), get_component, has_component, remove_component. Allow-list: Transform, Rigidbody, Rigidbody2D, Collider, Collider2D, Animator, AudioSource, Light, Camera, MeshRenderer, SpriteRenderer.

Inputinput_axis("Horizontal"), input_down("Jump"), input_pressed, input_up — Unity Input Manager axis/button names first, key names as fallback.

AV & UIplay_sound stop_sound play_animation set_animation_param set_ui_text set_ui_visible.

Persistencesave_set(key, value) save_get save_has save_delete.

Timenow() get_time() delta_time().


4. Running scripts

The component door

BedderScriptRunner (add to a GameObject): scriptAsset (a .bedder TextAsset) or inline source; useDesktopLimits for editor-grade budgets. Lifecycle: Awake = compile + run top level + awake event; Start = start; Update = tick (drives on update and waits); FixedUpdate / LateUpdate emit fixed_update(dt) / late_update(dt); all trigger/collision messages (3D and 2D) forward as events, name-first. self() is the GameObject the runner sits on.

The embedding door

For a game that treats scripts as content it owns (levels, saves, mods), the BedderScript.Embedding layer plugs the engine into your game: BedderScriptDirector attaches a script to a GameObject, ticks them all each frame, broadcasts game events, and reports per-script errors; ExtensibleEngineHost.RegisterApi is the custom-API seam — anything you register becomes a free function in the language for scripts run under that host (unknown names fail loudly). This is the host-integration surface, in the BedderScript.Embedding namespace.

Sandbox limits

SandboxLimits (per engine): MaxInstructionsPerFrame/PerCall, MaxLoopIterations, MaxRecursionDepth, MaxCallCount, MaxEventChainDepth, MaxSpawnedObjects, MaxCollectionSize, MaxScriptMemoryBytes, MaxCoroutineCount, MaxStackDepth, MaxExecutionSeconds. Presets: ForIos, ForDesktop. All settable.


5. Files & tooling

  • .bedder inside Unity (ScriptedImporter → TextAsset). .bs externally (the language is BS — never BC). Legacy .bc files still open; Unity's native importer owns that extension (LLVM bitcode), so never use it under Assets/.
  • Edit-mode IDE: Tools → BedderScript → Open Bedder IDE — or just double-click any .bedder in the Project window. A dockable editor window with the full IDE (explorer, tabs, completion, click-to-jump problems/outline, quick-fix, find/replace, format) — no Play mode needed. Save writes to disk and reimports the asset; ✦ Ask AI sends the open script as reference to an AI assistant with your question (Unity AI wiring lands when connected — today the door explains itself). Hot-swapping a RUNNING game is the in-game F1 IDE's job (▶ APPLY). Tools → BedderScript → Play In-Game IDE is the other door: a throwaway playground scene in Play mode with the F1 overlay up.
  • Putting a script on an object (edit mode): right-click the GameObject → BedderScript → Assign Script… — a picker lists every .bedder in the project; it reuses the object's existing runner (swapping the script) or adds one, with Undo. (The runner's Script Asset slot also accepts a dragged .bedder directly.)
  • VS Code: extension beddereder.bedderscript — highlighting for both extensions, Copilot-completes.
  • Validation without running: ScriptEngine.Validate(source) → diagnostics; also what the in-game IDE's problems panel uses.
  • The proof battery: every language promise is an EditMode test. The rule of the house: a feature isn't in the language until its battery test is green.
  • Scripts in builds (BsScriptStore + BsShipScriptsOnBuild): the included build hook runs automatically before every build and mirrors your scripts folder (Assets/ArTchie Studios/BedderScript) into StreamingAssets/BedderScripts — scripts written in the editor ship with the game. First launch seeds the player's editable copies into Documents/<product>/Scripts (persistentDataPath on mobile) with a hash manifest, and a game update refreshes ONLY files the player never edited. On boot, an edited copy outranks the TextAsset baked into the build — in-game IDE edits survive restarting a shipped game. The IDE's Save status always shows the full folder path, so players can find their work; a host can relocate the folder by setting BsScriptStore.UserScriptsRootOverride before first script access.
  • Snapshots (Hist ▾ in both the IDE and Blox): user-controlled checkpoints, never automatic. Take = copies the script's files into .snapshots/<name>/<time>_<label>/; restore = copies content back over the live files (asset GUIDs never move) and reloads the open tab/canvas.
  • ONE SCRIPT (design law): the .bedder text is the only script there is — blocks are clothes on that body. F2 always DERIVES blocks from the current text (the IDE's live buffer first, disk second) through the real parser; unmappable lines and comments ride as raw BS » blocks. Every block edit regenerates the text and pushes it into the IDE's open tab live; Save writes the .bedder (nothing else — .blox project files are retired). If the text changes, F2 re-derives. There is no second copy and therefore nothing to drift. Overwriting content the blocks were never derived from keeps a one-time <name>_orig.bedder backup.

6. Scene edits that survive — the scene-op spine

The same philosophy as scripts, one level up: the shipped scene is the untouchable floor, the user's edits are a small replayable layer on top, and applying is deliberate.

The four pieces (all in Runtime/Components/):

  • BsId — the sticker. A permanent unique code on a GameObject. Scene ops address objects ONLY by sticker — never by name-path or sibling index — so edits survive renames, moves, and game updates.
  • BsSceneDelta — the booklet. An ordered list of small JSON ops: create (Empty + primitives), create_probuilder (Cube/Stair/Arch/Pipe/… — needs the optional com.unity.probuilder package, skips politely without it), create_terrain (native Unity Terrain + TerrainData; kind is res,width,height,length e.g. 257,200,50,200), set_terrain_heights (deflated 16-bit heightmap in value, survives ▶ LOAD / boot replay), set_terrain_alphamaps (deflated 8-bit splat weights + layer list — tint/name/tile — in value), set_terrain_details (deflated 8-bit grass density + prototype tint/name in value), import_asset, create_from_asset, set_texture, destroy, rename, add_component, remove_component, set_property, assign_script, remove_script, reparent (child BsId → parent BsId, or "" for scene root). The booklet IS the user's mod — untouched objects always come from the shipped scene.
  • .bsmesh — save as prefab. Any placed object with a readable mesh (a ProBuilder shape, an imported OBJ, a resized primitive) can be frozen from the F3 inspector ("💾 Save as prefab") into a .bsmesh file in the asset catalog — final mesh + material colors, never the gestures — and re-placed forever after from Project ▸ Imports like any imported model.
  • BsSceneApply — the robot. One function replays a booklet onto the loaded scene. The SAME function runs for an in-session ▶ LOAD and for boot-time reload — never fork it. Replay is idempotent (a create whose sticker already exists does nothing); a missing sticker skips politely and is reported, never fatal. Components resolve by type name across all loaded assemblies (ALL components, not a curated subset); properties set by reflection ("1,2,3" vectors, "r,g,b,a" colors, euler "x,y,z" rotations). assign_script attaches a BedderScriptRunner reading the stem from the user Scripts folder and hot-boots it via ReloadSource.
  • BsSceneModLoader — the shelf reader. On scene Start, loads Documents/<product>/SceneMods/<scene>.json and hands it to the robot.

One-time wiring, per scene:

  1. Open the scene → Tools → BedderScript → Add BsIds To Open Scene → save. Why: every existing object gets its sticker baked into the scene file, so shipped scenes are always addressable. Safe to re-run any time — existing stickers are never touched; only new objects get new ones. Re-run it after adding objects you want editable, before shipping.
  2. Add Component → BsSceneModLoader on any object that always exists (convention: the same object that holds the IDE/Blox loaders) → save. Why: this is what makes user scene edits reload on every launch. It has no settings; it does nothing when no mod file exists.

Smoke test (no UI needed): write a booklet by hand to Documents/<product>/SceneMods/<scene>.json

{ "ops": [ { "op": "create", "id": "smoke1", "kind": "Sphere", "name": "RobotWasHere",
             "value": "", "parent": "" },
           { "op": "set_property", "id": "smoke1", "kind": "UnityEngine.Transform",
             "name": "position", "value": "0,5,0" } ] }

Press Play → a sphere appears at (0,5,0). Delete the file to undo.

Status: the spine is battery-proven, and the F3 Scene panel (§7) writes booklets from clicks — hierarchy, inspector, gizmos, Project window, asset placement all stage ops through this one spine.


7. The in-game editor — the four faces

Host games wire these with small loaders (the showcase Bs*Loaders are the reference). F1–F3 are tabs (opening one closes the others); F4 is an overlay that opens over anything and gates all other input while open. Whenever any face is open, the player script yields (ide_shown/ide_hidden events) and the cursor unlocks.

Key Face
F1 IDE (BedderEder) — edit .bedder scripts, APPLY hot-swaps into the running game
F2 Blox — block view of the SAME script (ONE SCRIPT: §5), edits regenerate the text
F3 Scene panel — hierarchy, inspector, viewport, Project window
F4 Bug reporter — name / description / repro / screenshot → Documents/<product>/Bugs/

F1 IDE (BedderEder)

The CODE face is EderCodeView (showcase loader: BsIdeLoader). Explorer + file tabs + editor well + problems/outline. ▶ APPLY hot-swaps the active tab into the matching running BedderScriptRunner. Save writes every open tab to disk (the player-build folder is Documents/<product>/Scripts). Completions, verify, go-to-definition, and quick-fix call the existing IdeSession / TextDocument APIs — they are not a second editor brain. Scene / Project Open in CODE uses the same door: BsIdeLoader.OpenLiveEderCodeView.OpenProjectFileIdeSession.OpenDocument. If that file is already an open tab, the live buffer is reused (disk is not re-read), so unsaved typing survives F3 → F1. Hiding CODE flushes the editor widget into the document first.

Problems — click a row to jump the caret to that diagnostic (line + column). Rows carry an E / W / i badge and a severity color bar; fix on the row applies that diagnostic's first available QuickFix when IdeSession attached one.

Outline — click a symbol to jump to its declaration. If analysis has no symbols (broken parse), the list falls back to surface function / on / let / const declarations in the current buffer.

Shortcuts while F1 is visible. The F4 bug card is modal and owns all input while open. Name prompts and context menus do the same. F3 is next-find only while the find card is open, so it does not steal the Scene face key.

Input Action
Ctrl/Cmd+S Save open tabs to disk
Ctrl/Cmd+Z Undo (TextDocument). Status flashes when the stack is empty.
Ctrl/Cmd+Shift+Z or Ctrl+Y Redo. Status flashes when the stack is empty.
F5 Verify / Analyze
Ctrl/Cmd+Space Open completions (Tab still commits the selected item)
typing Completions also open while typing an identifier or after . (not inside strings/comments)
Ctrl/Cmd+. Quick Fix — first available diagnostic fix at the caret, else safe auto-correct, else Verify
F12 Go to definition of the identifier at the caret
Ctrl/Cmd+F Find / replace card
F3 / Shift+F3 Next / previous match (only while Find is open)
Esc Close Find, or dismiss the completion list
Tab Complete, or commit the selected suggestion
Fix (toolbar) Same as Ctrl+.

F3 Scene panel

Layout — panels dock left/right/bottom; drag a panel header to move it (ghost chip shows the target), drag the 12px splitters to resize. Layout ▾ saves named layouts (Documents/<product>/Layout/), lists them, and restores default.

Scene camera (the imposter view — the main camera stops rendering while F3 is open). Gameplay input is blocked for every Bedder face; while F3 is open, BedderScript on update / fixed_update / late_update also pause, so WASD flies the scene cam instead of walking the player (gravity, FollowCam, and move() all stand down). A ground grid is drawn on XZ at y=0 (or XY at z=0 in 2D), spaced to the current snap increment.

Input Action
RMB hold + mouse Look around (fly mode). In 2D, RMB pans
W A S D Move · Q / E down / up · Shift ×4 sprint. In 2D, WASD pans in the XY plane
MMB drag Pan in the camera plane (screen-space at the orbit pivot)
Alt+LMB drag Orbit around the selection (else last F / view center). In 2D, pans
Scroll (while flying) Fly speed ×1.15 per notch — Shift+scroll fine-tune ×1.03 (toast shows the multiplier)
Scroll (over the viewport, not flying) Dolly. In Ortho / 2D, zooms orthographic size
Ortho button Perspective ↔ orthographic (keeps the current heading)
2D button Ortho + look down +Z at the XY plane (Unity 2D scene view). Toggle off restores the previous 3D heading
F Frame the selected object (primary / last click)
F1 Open the related .bedder / .bs in CODE as a live buffer (Project script preview, else BedderScript on the selection). Same as Open in CODE. Already-open tabs keep unsaved typing. No related file = show the last CODE tab.
Shift+click / Ctrl+click Range-select through the visible hierarchy, or toggle (viewport uses the same modifiers)
Drag a hierarchy row Reparent onto the drop-target row (all selected; skip cycles). Drop on empty tree space = unparent to scene root
Ctrl+D Duplicate the selection (new BsId, offset, staged as booklet create + imposter)
Ctrl+Shift+P (also Ctrl+P) Command palette. Type to filter, ↑↓ highlight, Enter runs, Esc or click-outside closes. Commands: Create Empty (and Create Empty Child when a stickered object is selected), Duplicate, Focus, Open in CODE, Import (the Project Import button / folder picker), Toggle Ortho, Toggle 2D. Grey rows cannot run (nothing selected, or no .bedder on the selection).
Delete Stage destroy for every selected stickered object — the mesh hides immediately (gone from the viewport and hierarchy). Ctrl+Z un-hides it. ▶ LOAD is the real delete
Ctrl/Cmd+Z Undo the last pending booklet op group (create / delete / transform / reparent this session). Status flashes when the stack is empty.
Ctrl/Cmd+Shift+Z or Ctrl+Y Redo. Status flashes when the stack is empty.
Esc Clear the selection (Forge Esc still exits geometry edit; a command palette Esc closes the overlay instead)
Empty viewport click Clear the selection (a 3D pick miss — hierarchy chrome is not a miss)

Gizmos (on the selected object). A tool strip at the top-left of the viewport mirrors the keys: W / E / R, Local/Global, Pivot/Center, Snap (+ increment field), V, Ortho, 2D.

Input Action
W / E / R Move / Rotate / Scale (buttons stay in sync). Also leave Terrain sculpt.
Terr Terrain sculpt/paint on the selected Terrain (second strip: Raise / Lower / Smooth / Flatten / Paint + layers)
X or Local/Global Toggle world vs local axes
Pivot/Center Handles at the transform, or at renderer/collider bounds center
Snap button Sticky snap on/off. Shift+click cycles the move increment (0.125 / 0.25 / 0.5 / 1 / 2 / 4)
Snap increment field Type a move step (0.01–100). Rotate stays 15°, scale 0.1
Ctrl (while dragging) Snap even when the button is off — uses the current increment
V (hold or the V button) Vertex snap while translating. Readable MeshFilter / skinned / mesh-collider verts if mesh.isReadable; otherwise the renderer/collider AABB's 8 corners. Does not snap while rotating or scaling. Built-in Unity primitives are often non-readable, so those fall back to corners
Drag axis / plane quad / ring / cube The usual Unity handles; center square = screen-plane move

Inspector — selected objects open with a GameObject header (active checkbox, editable name, tag, layer) above Transform and the other components. Rename stages the booklet rename op (Ctrl+Z undoes it). Active / tag / layer apply live on the object; they have no booklet channel yet, so the status line says so. Every component is discovered by reflection: checkboxes, sliders, enum menus, X/Y/Z scrub grips, HSV color picker. Drag any value label to scrub it (Shift = ×0.1 fine). Edits apply LIVE and stage a set_property op. A BedderScript component adds Open in CODE (F1) (the script body is not an inspector field — inline scripts with no file stay here). The section header folds from the arrow / title only — clicking the enable checkbox or never folds it, exactly like Unity. Multi-select keeps showing the last-clicked object's inspector, and an edit there applies to every selected object that has that component — property, Transform vector, component enabled, material tint — all in one Ctrl+Z group. Objects missing that component (and pending imposters) sit the edit out. Where the selection disagrees on a value you get a mixed readout instead of the primary's number: for fields, vectors (per axis), sliders and toggles, Mixed on enum menus, an empty swatch for colors. Setting a value clears the mix; an axis still showing is left alone, so a typed Y never flattens everyone's X. ✗ Remove component stays on the last-clicked object only — a stray ✗ can't strip a component off the whole set. Actions: + Add Component ▾, + Script ▾ (from the user Scripts folder), 🎨 Texture ▾ (from imported images), 💾 Save as prefab (any readable mesh → .bsmesh in the catalog), 🗑 Delete (hides immediately; ▶ LOAD commits).

Create ▾ — Empty + primitives, Terrain (200×200, 257 heightmap — see Terrain below), and ProBuilder ▸ shapes (Cube, Stair, CurvedStair, Arch, Door, Pipe, Cylinder, Prism, Torus, Cone) when the optional package is installed. New objects appear as imposters (solid look, no colliders, no gameplay) exactly where the real ones will be. Terrain keeps its collider so you can sculpt it before ▶ LOAD. The same model covers every create and delete: Duplicate, Create, Project Place, and a model dropped on the viewport all stage a booklet op and show an imposter immediately. Delete hides the live mesh immediately (it leaves the hierarchy too). Nothing is committed until ▶ LOAD, which runs the booklet through the robot and saves it for boot replay. There is no second “apply now” path — a drag-drop is not a silent FinishLoad. Right-click hierarchy rows for per-object menus (Open in CODE (F1) when a BedderScript file is on the object, Create / Rename / Duplicate / Delete); right-click empty tree space to create at root. Drag a selected row onto another row to reparent (Unity-like); multi-select reparents the whole set under the drop target, skipping any object that would parent under its own descendant. Drop on empty hierarchy space unparents to the scene root (same as Create at root). The drop target row highlights with the selection color. Click replaces the selection; Ctrl/Cmd+click toggles; Shift+click ranges through the visible tree (same modifiers in the viewport). The last click is the primary — inspector and gizmos follow it; Delete and Duplicate act on the whole set. Duplicate clones stickered objects (or the primary if the list is empty), assigns a new BsId, offsets slightly, and stages booklet create ops matching the existing create path. Objects without a BsId are skipped, same as Delete. Esc or an empty viewport click (no object under the cursor) clears the selection; clicking hierarchy chrome that isn't a row does not.

Filter… — the hierarchy has a filter field at the top of the tree (below the dock grip, not on the W/E/R gizmo bar). Typing filters the rows live by GameObject name, case-insensitive contains; the query is trimmed, so an all-whitespace filter lists everything and cube still finds Cube. Nothing is a wildcard — *, . and [ match themselves. A matching child still shows its ancestors (they open for the search only; clearing the query restores the folds you left), and non-matching siblings stay hidden while the filter is on. An empty tree reads No objects yet; a query that matches nothing reads No matching objects. Esc in the field clears the query and keeps the caret there, a second Esc leaves the field, and after that Esc deselects as usual — a command palette Esc still just closes the overlay.

Row chrome (type chip / lock / eye) — every hierarchy row carries three widgets on its right, in that order: a type chip (pending, Terrain, Camera, Light, Canvas, Audio, Mesh, Empty — first match on the object itself wins), then a lock, then an eye. The name to their left stays the selectable label, and the chip is display-only. The eye toggles the GameObject's active state — the same switch as the inspector header checkbox, and the two stay in sync either way you flip it. Inactive objects stay listed in the tree and just dim (name and chip both), so nothing vanishes on you. The lock takes an object out of the viewport's reach: clicks in the 3D view pick straight past it and a locked object gets no gizmo handles, while a hierarchy click still selects it and the inspector still edits it. Both are per-object — locking a parent does not lock its children — and both are also on the row's right-click menu as Lock / Unlock and Hide / Unhide. Clicking either widget never changes the selection and never starts a reparent drag. Eye and lock are session-local: neither stages a booklet op, so ▶ LOAD does not persist them and they reset when Bedder restarts.

TerrainCreate ▾ → Terrain drops a native Unity Terrain (200×200 m, 50 m height range, 257×257 heightmap) in front of the scene camera, surface at y=0. It shows in the hierarchy (amber · pending until ▶ LOAD). Select it and a second tool strip appears: Raise / Lower / Smooth / Flatten / Paint / Grass, plus Size (world metres, default 24) and Opacity (0–1, default 0.35). A second row holds layer chips (1–4) with + / . New terrains start with one grass-green layer; + adds a generated 64×64 albedo tint (Dirt / Rock / Sand — no extra packages). Grass scatters 1–2 runtime billboard tufts (tinted blade textures, detail map 256 / patch 8) with the same Size/Opacity brush; Shift erases density to 0. LMB stamps under the cursor with a soft falloff; a cyan ring tracks the brush on the surface. Shift inverts Raise/Lower, and on Paint erases the active layer toward the others (usually Grass). W / E / R leave sculpt/paint and restore object handles (terrain pivot is the min-XZ corner, Unity-style). Terr on the gizmo strip toggles the tools back on. Flatten samples the height at mouse-down and levels the stroke to it. One Ctrl+Z undoes a whole mouse-up stroke or a layer add/remove (redo with Ctrl+Y). Sculpt stages set_terrain_heights; paint and layer edits stage set_terrain_alphamaps; grass strokes stage set_terrain_details▶ LOAD writes them into Documents/<product>/SceneMods/<scene>.json so a reload keeps the hills, splats, and grass. Not this pass: trees, holes, neighbor stitch, heightmap import.

Undo is over pending booklet ops staged this session — not Hist. Hist ▾ snapshots remain checkpoints of the saved scene-mod file. Ctrl/Cmd+Z pops the last op group: undoing a create removes the imposter and the staged op; undoing a staged delete un-hides the object (it was hidden the moment Delete staged, never destroyed until ▶ LOAD); undoing a gizmo or inspector transform restores the previous TRS and the pending set_property; undoing a terrain stroke restores the heightmap and the pending set_terrain_heights; undoing a paint stroke or layer add/remove restores the alphamaps / layer list and the pending set_terrain_alphamaps; undoing a grass stroke restores the detail density and the pending set_terrain_details; undoing a hierarchy reparent restores the previous parent (one undo for the whole multi-select drop) and pops the pending reparent op. ▶ LOAD (or Clear pending) commits or discards the session stack — Clear pending also un-hides any staged deletes. Shortcuts fire only while F3 is open and focus is not in an InputField, name prompt, popup, or the F4 bug card — F1 IDE Ctrl+Z stays with TextDocument.

Project window — roots: Scripts, Imports (the asset Library), SceneMods, Assets (editor only), and any folder you add with + Add source… (the system folder dialog; sources persist per product and a ✕ on the row forgets them). Rows carry real icons: texture files show a thumbnail of the image (cached by path + mtime); folders, model-bundle folders, prefabs, scripts, meshes, audio, and packages each get a distinct chip. File rows also show a dim type/size on the right. A Filter… box at the top of the file list matches names (substring); Icons toggles a 64px thumb grid for the current folder (List is the default). Selection works like a file explorer: click, shift+click for a range, ctrl+click to toggle — then Import (N), Move ▾ (N), or Delete (N) act on the whole set. With nothing selected, Import all takes the folder. A selected .unitypackage / .abspack is Open package… (not a competing Import). A selected .bedder / .bs offers Open in CODE; double-click the row does the same (live buffer in F1, not a stale copy). A selected .prefab is Place in scene (or drag onto the viewport): the prefab is copied/registered into Imports/Prefabs and staged as booklet create_from_asset — ghost + pending Hierarchy row immediately, status Staged: place <Name> — ▶ LOAD makes it permanent. Ctrl+Z undoes the pending place. ▶ LOAD writes it into the scene mod so a reload keeps the object. Player builds place catalog-backed prefabs only; AssetDatabase is editor-only for the copy/bake, never on the replay path.

Import stays in the folder you were browsing — it does not jump the Project root to Imports. A status line reports Imported 3 → Imports/Textures (click show in Imports on that row to jump when you want). Re-importing bytes that are already in the catalog says already in library, not a failure. If you are already inside Imports, the current folder just refreshes.

Imports land sorted: Models/ (each model as a self-contained bundle folder with its textures), Textures/, Audio/ — make your own folders with + New folder and file things with Move ▾. Click an import to preview it in the inspector and pre-configure components/scripts before placing; drag a model into the viewport to place it (imposter now, booklet pending, same as Create / Duplicate — ▶ LOAD makes it real); drag a .prefab the same way (it registers into Imports/Prefabs first, then stages create_from_asset); drag a texture onto an object to apply it live and stage the paint (textures never place as objects); scripts drop onto the object under the cursor and stage assign_script (▶ LOAD attaches it).

Supported: images (.png .jpg .jpeg .tga .bmp .gif .tif/.tiff .psd), audio (.wav .ogg .mp3 .aiff/.aif), models — .obj with runtime MTL + textures, .fbx (in-house importer: binary 7.x, the older big-endian 6.x, and ASCII — static meshes, submesh materials, diffuse textures incl. .tga/.bmp/.gif/.tif; rigs and animation are not imported), .glb/.gltf (via the optional glTFast package), and .bsmesh. Files are COPIED into the catalog (sha-deduped, 50 MB cap) — imports never depend on the source folder afterward.

Image notes (honest): .gif is the first frame only (LoadImage if the host codec accepts it, else an in-package LZW decoder). .tif/.tiff are 8-bit gray/RGB/RGBA, uncompressed or PackBits — LZW/JPEG/tiled TIFF will copy into Imports but will not decode at Place; export PNG. .psd loads Photoshop's own flattened composite (8-bit RGB/gray, raw or PackBits), not a layer-stack replay — ZIP or 16/32-bit PSDs will not decode; export PNG or TGA. .exr .hdr .webp stay blocked (no runtime decoder). Status on an unsupported type names what is supported in one line.

Selecting a .cs file in a source folder and hitting Import converts it with BsCsToBedder into Scripts/<stem>.bedder (_2 if the name is taken) and reports the TODO(bs) count. C# is never copied into Imports.

Import whole Unity packages. Select a .unitypackage (or .abspack) in any source folder → ⬇ Open package… → the familiar dialog: a collapsible folder tree with per-item and per-folder ticks. Everything lands at the SAME paths the tree showed, under Models/ Textures/ Audio/ Prefabs/. The importer reads the package's own .mat and .prefab wiring, so prefabs assemble as placeable units with the right texture on the right submesh — atlas-textured packs included. .bedder files in a pack go straight to the scripts folder. C# scripts convert to BedderScript by default (untick to skip) and are marked honestly: the converter is a translator assistant that makes mistakes; everything it cannot map is commented and tagged TODO(bs) for a human to review. The finish line reports how many TODO(bs) flags need a look.

Author your own packs. Tools → BedderScript → Export ABS Pack… (Unity editor) exports a targeted .abspack — models, textures, audio, .bedder scripts, and prefabs (baked to .bsmesh, every mesh combined, with each material's albedo texture shipped beside the mesh and rewired on import) — that players import through the same dialog, manifest name and author shown in its title. You are responsible for having the right to redistribute everything you include; the export gate requires that confirmation.

F4 bug reporter

Fields for name / description / how-to-reproduce, plus 📷 Screenshot (the card hides itself for the capture). Saves report.md + screenshot.png per report under Documents/<product>/Bugs/<time>_<name>/. While open it is modal: camera, gizmos, panel hotkeys, and the player script all yield.

Appendix — full API reference

Every built-in function, generated from the engine's own census (the same table that drives typechecking and IDE completion), so this list is always complete. Regenerate after engine changes: Tools → BedderScript → Regenerate API Reference.

143 functions.

Core

  • print(value) — Write a value to the log.
  • assert(condition, message) — Stop with message when condition is false.
  • len(value) -> int — Length of a string, array, or map.
  • str(value) -> string — Convert any value to a string.
  • int(value) -> int — Convert to a whole number.
  • float(value) -> float — Convert to a decimal number.
  • number(value) -> number — Convert to a number.
  • bool(value) -> bool — Convert to true/false.
  • typeof(value) -> string — Name of a value's type.
  • log(value) — Log a message.
  • warn(value) — Log a warning.
  • error(value) — Log an error.

Math

  • min(a, b) -> number — Smaller of two numbers.
  • max(a, b) -> number — Larger of two numbers.
  • abs(n) -> number — Absolute value.
  • floor(n) -> int — Round down.
  • ceil(n) -> int — Round up.
  • round(n) -> int — Round to nearest whole number.
  • clamp(n, lo, hi) -> number — Limit a number to a range.
  • lerp(a, b, t) -> number — Blend from a to b by t (0..1).
  • sin(radians) -> float — Sine.
  • cos(radians) -> float — Cosine.
  • sqrt(n) -> float — Square root.
  • pow(n, e) -> float — n raised to e.
  • random() -> float — Random number 0..1.
  • random_range(lo, hi) -> float — Random number in a range.
  • tan(radians) -> number — Tangent.
  • asin(n) -> number — Arc sine (radians).
  • acos(n) -> number — Arc cosine (radians).
  • atan2(y, x) -> number — Angle of a vector (radians).
  • exp(n) -> number — e raised to n.
  • ln(n) -> number — Natural logarithm.
  • sign(n) -> number — -1, 0, or 1.
  • smooth_step(a, b, t) -> number — Eased blend from a to b.
  • ping_pong(t, length) -> number — Bounce t between 0 and length.
  • move_towards(cur, target, maxDelta) -> number — Step toward target without overshooting.
  • repeat(t, length) -> number — Wrap t into 0..length.
  • set_random_seed(n) — Make random() deterministic.

Time

  • now() -> float — Seconds since play started.
  • delta_time() -> float — Seconds since last frame.
  • get_time() -> float — Seconds since play started.
  • set_timeout(seconds, fn) -> id — Run a function after a delay.

Components

  • add_component(go, "Rigidbody") -> comp — Add an allow-listed Unity component.
  • get_component(go, "Light") -> comp — Get a component, or 0 when missing.
  • has_component(go, kind) -> bool — True when the component exists.
  • remove_component(go, kind) — Remove a component.

Transform

  • get_position(go) -> vec3 — World position as a vec3 map.
  • set_position(go, x, y, z | go, v) — Move to a world position (numbers or a vec3).
  • translate(go, dx, dy, dz | go, v) — Move relative to current position.
  • look_at(go, x, y, z | go, v) — Face a world point.
  • set_rotation(go, x, y, z | go, v) — Set rotation from euler angles.
  • set_scale(go, x, y, z | go, v) — Set local scale.
  • transform_direction(go, v) -> vec3 — Local direction to world space.
  • get_rotation(go) -> vec3 — Euler angles as a vec3 map.

Physics

  • add_force(go, x, y, z | go, v) — Push a rigidbody.
  • set_velocity(go, x, y, z | go, v) — Set rigidbody velocity.
  • get_velocity(go) -> vec3 — Rigidbody velocity as a vec3 map.
  • move(go, dx, dy, dz) -> bool — Collision-aware move (CharacterController when present).
  • is_grounded(go) -> bool — CharacterController grounded check.
  • check_sphere(x, y, z, r) -> bool — Anything solid inside a sphere?
  • raycast(origin, dir [, maxDist]) -> hit | null — Cast a ray, get hit info.

Objects

  • set_active(go, on) — Show or hide an object.
  • get_name(go) -> string — Object name.
  • set_name(go, name) — Rename an object.
  • get_tag(go) -> string — Object tag.
  • set_tag(go, tag) — Set an object's tag.
  • spawn(name) -> go — Create an object from the catalog.
  • destroy(go) — Remove an object from the scene.
  • find(name) -> go | find(s, sub) -> int — Find an object by name (object form wins the typecheck).
  • find_tag(tag) -> array — All objects with a tag.
  • spawn_entity(name, x, y [, w, h]) -> go — Spawn a catalog entity at a position.
  • self() -> go — The object this script runs on.

Vector

  • vec3(x, y, z) -> vec3 — Make a vector map with x/y/z.
  • vec_lerp(a, b, t) -> vec3 — Blend between two vectors.
  • vec_length(v) -> float — Vector magnitude.
  • vec_norm(v) -> vec3 — Vector scaled to length 1.
  • dot(a, b) -> float — Dot product.
  • distance(a, b) -> float — Distance between two points/objects.
  • vec2(x, y) -> vec2 — Make a 2D vector map.
  • color(r, g, b [, a]) -> color — Make a color map.

Events

  • send_event(go, name, args...) -> bool — Fire on name(...) on the target's scripts.
  • raise_signal(name) — Broadcast a named signal.

Collections

  • push(arr, value) — Append to an array.
  • pop(arr) -> value — Remove and return the last element.
  • contains(coll, value) -> bool — String/array/map contains a value.
  • index_of(coll, value) -> number — Position of a value, -1 when absent.
  • insert(arr, index, value) — Insert into an array.
  • remove_at(arr, index) — Remove an array element by position.
  • reverse(arr) — Reverse in place.
  • join(arr, sep) -> string — Join elements into a string.
  • clear(coll) — Empty an array or map.
  • sort(arr [, compare]) — Sort in place.
  • keys(map) -> array — Map keys.
  • values(map) -> array — Map values.
  • has_key(map, key) -> bool — Map contains a key.
  • remove_key(map, key) -> bool — Remove a map entry.

Input

  • input_up(action) -> bool — Input released this frame.
  • input_down(action) -> bool — Pressed this frame (GetButtonDown) — the demo scripts' jump.
  • input_axis(axis) -> float — Analog axis (-1..1).
  • input_pressed(action) -> bool — Held (GetButton) — sprint, mining.

Av

  • set_animation_param(name, param, value) — Set an Animator parameter.
  • stop_sound(name) — Stop a playing sound.
  • play_sound(name) — Play a sound.
  • play_animation(name) — Play an animation state.

Ui

  • set_ui_text(name, text) — Set a UI text element.
  • set_ui_visible(name, on) — Show or hide a UI element.

String

  • substring(s, start, len) -> string — Slice of a string.
  • split(s, sep) -> array — Split a string into parts.
  • replace(s, from, to) -> string — Replace every occurrence.
  • trim(s) -> string — Strip surrounding whitespace.
  • to_lower(s) -> string — Lowercase.
  • to_upper(s) -> string — Uppercase.
  • starts_with(s, prefix) -> bool — String begins with prefix.
  • ends_with(s, suffix) -> bool — String ends with suffix.

Persistence

  • save_set(key, value) — Persist a value.
  • save_get(key) -> value — Read a persisted value.
  • save_has(key) -> bool — Persisted key exists.
  • save_delete(key) — Delete a persisted value.

Session

  • set_var(key, value) — Set a session variable.
  • get_var(key) -> number — Read a session variable.
  • add_var(key, delta) -> number — Add to a session variable.
  • move_piece(dx, dy) — Nudge the bound piece (2D host).
  • set_ambient(level) — Ambient light level.
  • set_solid(on) — Make the bound object solid or pass-through.
  • set_color(color) — Tint the bound object.
  • set_tuning(key, value) — Override a tuning knob.
  • revert_tuning(key) — Restore one tuning knob.
  • reset_tuning() — Restore all tuning knobs.
  • hide_self() — Hide the bound object.
  • destroy_self() — Destroy the bound object.

Player

  • teleport_player(x, y) — Move the player instantly.
  • impulse_player(x, y) — Push the player.
  • grant_ability(name) — Give the player an ability.
  • revoke_ability(name) — Take an ability away.
  • kill_player() — Kill and respawn the player.
  • win_level() — Finish the level.
  • player_speed() -> number — Player's current speed.
  • distance_to_player() -> number — Distance from the bound object to the player.
  • set_player_scale(x, y) — Resize the player.
  • get_player() -> go — The player object.
  • player_overlaps_self() -> bool — Player touching the bound object?
  • player_hit_underside() -> bool — Player bumped this from below?
  • contact_normal_y() -> number — Y of the player's last contact normal.

Runner events (on name(args) { ... })

  • awake() — after the script's top level runs
  • start() — first frame
  • update(dt) — every frame
  • fixed_update(dt) — physics step
  • late_update(dt) — after update, for cameras
  • enable() / disable() — object toggled on/off
  • destroy() — object is being destroyed
  • trigger_enter(name, obj) / trigger_exit(name, obj) / trigger_stay(name, obj) — trigger volumes (3D and 2D)
  • collision_enter(name, obj) / collision_exit(name, obj) / collision_stay(name, obj) — collisions (3D and 2D)
  • controller_hit(name, obj, dx, dy, dz) — CharacterController hit something

Source in the box: Assets/ArTchie Studios/ArTchie-Bedder-Suite/com.bedder.script/MANUAL.md