dot-props

A sandbox layer — a catalogue of spawnable props, per-player budgets and an undo stack, a physics gun that holds and freezes, and a gravity gun that punts.

Spawnable props, the limits that keep a server alive, and the tools that move them.

Requires dot-core.

Server-authoritative, and not predicted

Rigid-body physics is not reproducible across machines — the solver’s iteration order and the last bits of every float differ — so a predicted prop is a prop that is corrected constantly. The server owns every prop; the client draws what it is told; the tools send intent.

var spawner := DotPropSpawner.new()
spawner.authoritative = true                  # on the server only
spawner.catalogue = DotPropCatalogue.load_json("res://props.json").value
spawner.limits = DotPropLimits.new()
spawner.world_ref = DotNodeRef.of_path(^"../World")
add_child(spawner)

spawner.refused.connect(func(player, prop, reason): tell(player, reason))

spawner.advance(delta)
spawner.spawn(&"crate", player_id, aim_point, Basis.IDENTITY)
{
  "format": 1,
  "props": [
    {"id": "crate", "name": "Crate", "category": "props",
     "scene": "res://props/crate.tscn", "mass": 20},
    {"id": "safe", "category": "props",
     "scene": "res://props/safe.tscn", "mass": 900, "cost": 8},
    {"id": "pillar", "category": "scenery",
     "scene": "res://props/pillar.tscn", "can_grab": false}
  ]
}

The tools

var gun := DotPhysGun.new()
gun.spawner = spawner
gun.wielder = player_id

gun.grab(space_state, eye_position, aim_direction, view_basis)
gun.hold(eye_position, aim_direction, view_basis, delta)   # every physics tick
gun.push(scroll_amount, delta)
gun.release()
gun.freeze_held()
var grav := DotGravGun.new()
grav.pull(space_state, eye_position, aim_direction)
grav.carry(eye_position, aim_direction, delta)
grav.punt(space_state, eye_position, aim_direction)

The physics gun holds with a spring, not a transform: setting a transform on a rigid body fights the solver and produces a prop that jitters and shoves everything it touches.

Breaking a prop, and standing on one

DotPropDamage holds hit points for props and applies damage to nobody. The numbers live on DotPropDef — max_health and break_impact_speed — so a definition stays checkable without loading its scene; the state is keyed by instance id, so a breakable prop is still a plain scene with a RigidBody3D at its root. break_impact_speed is a speed rather than damage, because a crate has to survive being walked into and must not survive a bus. max_health = 0 is indestructible, since every prop written before the field decodes to zero. When something explodes it describes the blast and shoves other props; what that does to a person is dot-combat’s. Signals: damaged, broken, exploded.

DotPropCarry is what makes a crate an object rather than scenery: a character motor sweeps a shape and slides, so without it a crate is exactly as solid and as immovable as the floor. stand() presses a player’s weight into the prop under them, ride() carries them with it — angular velocity included, or a player on a spinning crate drifts toward the middle — and push() shoves one. It reads the motor’s ground_id, which is a local handle and deliberately not part of the simulation.

Props from a folder of models

DotPropFolder.load_into() turns a folder of .glb and .gltf files into catalogue props with no code: a model that appears in a scanned directory is a prop, a subfolder is its category, and a props.json or a JSON sidecar next to a model (and a .png thumbnail) says anything else, such as a name, category or credit. DotPropModel is the model file as a body’s picture, hull and mass. The collision is shaped like the model, so a prop is not a box around its bounds. DotPropCatalogue.follow_content() lists a player’s subscribed Workshop props in solo; online shows none, and the game’s own ids win.

Toys

Toys are networked physics toys and interactables a map places by data: balls, a see-saw, a roundabout, a trampoline, a bell. DotPropToyBox holds them. The server simulates and clients draw what it sends.

  • Shooting and blasting. shot(collider, point, direction, damage) turns a hit-scan’s damage into a push and returns the toy’s index (or -1 when it was not one). shot_along(origin, direction, reach, damage, wall_mask) is for a game whose own trace never meets a toy, because a toy’s footprint is never in the physics space. blast(at, radius, speed) throws toys at the same speed whatever they weigh, and a shot bell rings.
  • Carrying. A roundabout carries the people standing on it round its hub, through DotPropCarry.toys.
  • Cvars. sv_toys switches them off, sv_toys_awake caps how many simulate at once, and sv_toys_push sets how hard walking into one pushes it. DotPropToyCatalogue.merge_json() adds or retunes toys.

Where a game plugs in

DotPropCatalogue / DotPropDef

What can be spawned. Id, name, category, scene, mass, cost, can_grab, health, and a meta dictionary. A delivered prop carries a content_id, and ensure_content() fetches the packs the catalogue’s props live in.

DotPropDef.meta.scripta PATH

A prop that has code. A path, not a class_name — a mounted .pck cannot register class_name globals, so a path is the only shape a delivered prop can take. See game-playground.

DotPropLimits

Per-player counts, per-player frozen counts, a spawn cooldown, an undo depth, a total cap, and grab mass limits. Each one is defending against a specific way a sandbox server dies.

DotPropTool subclass

effective_reach, target, may_act_on. A new sandbox tool, or a weapon. DotPhysGun and DotGravGun are the shipped ones.

Signals

spawned, removed, refused on the spawner; grabbed, released, freeze_changed on DotPhysGun.