aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/docs/notes/run-scripts-routing.md
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
committergodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
commit39066900773e7857faf2d02e7ed51b71d219d97d (patch)
treeffab8e4ddd971626776c3b1f4ce1da2c186c2187 /docs/notes/run-scripts-routing.md
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'docs/notes/run-scripts-routing.md')
-rw-r--r--docs/notes/run-scripts-routing.md124
1 files changed, 124 insertions, 0 deletions
diff --git a/docs/notes/run-scripts-routing.md b/docs/notes/run-scripts-routing.md
new file mode 100644
index 0000000..8b8a2d7
--- /dev/null
+++ b/docs/notes/run-scripts-routing.md
@@ -0,0 +1,124 @@
+# Run scripts — routing a player-mode route
+
+How a game ("game A" below; "game B" = a 3D game) gets a player-mode run script (input only: clicks, keys, the game's own UI) across a large world, and
+what any game reuses. The lib's runner and step format: `src/Godosa.Core/Runs/Scripts/`. Code:
+`src/Godosa.Core/Runs/RouteLegs.cs` (pure, tested in `Godosa.Core.Tests/Runs/RouteLegsTests.cs`).
+
+## The method: scout headless, then play once in the window
+
+1. **Scout** with debug-mode scripts run headless (no window, seconds per run): teleport, then ask questions through
+ game cheats that print answers to stdout (`cheat "route=…"`, `"hops=…"`, `"path=…"`, `"near=…"`, `"find=…"`).
+ Debug mode never reaches the player script; it only finds the facts.
+2. **Write** the player steps from those answers (`travel-to` legs, `walk-path` targets, portal clicks, answer
+ texts). Cite the fact in a comment above the step (why the detour, which script opens what).
+3. **Play** the chapter once in the window from the last checkpoint; the checkpoint cache makes reruns resume at the
+ first changed step. Fix from the failure report (`out/runs/<run>/report.txt` + `fail.snap`), rerun.
+
+Scouting from a fresh game misleads where the world changes with play (opened seals, dead NPCs, decayed bodies):
+check a finding against the run's own `fail.snap` (the save doc) when it disagrees with the window run.
+
+## Overland: `RouteLegs.Path` + `RouteLegs.Legs`
+
+- The world is a grid (sectors). `Path` = breadth-first, 8 neighbours, over the game's passability (`blocked(x, y)`).
+- The game's own router (what a click on the world map does) takes only some trips: greedy steps, short detour windows,
+ refusals across water or mountains. `Legs` cuts the BFS path into the farthest stops that router accepts
+ (`accepts(stop, cell)` = the game's router finds a way): each stop becomes one `travel-to` leg.
+- Legs are **direction dependent**: the reverse of a working leg list can be refused. Scout each direction.
+- `Legs` returns null with the stuck stop: report it (`the router is stuck at …`) instead of guessing.
+
+## Seals: `RouteLegs.Seals`
+
+Script-sealed cells (a pass a tile script opens, a bridge a toll opens) are blocked like walls until the story opens
+them. When `Path` finds nothing, `Seals` reruns it with the soft cells open and names the ones a way would cross (at
+most 8, start to goal); empty when it is walled off anyway or already open. A refusal that names seals tells the author
+to walk across instead of travelling, or to open the seal first. A sealed goal cell is still reachable on foot: travel
+to the nearest open cell, then `walk-path` in.
+
+## Local maps: `RouteLegs.Hops`
+
+Inside buildings and dungeons the way is walks plus portal clicks (doors, stairs, relocate scripts). `Hops` is a BFS
+over portal exits: `reaches(a, b)` = the game's tile pathfinder walks from a to b; the result is the portals to click,
+in order (empty: walk straight there; null: no chain).
+
+## In the steps (player verbs that keep a route going)
+
+- `walk-path` clicks ahead along the game's A* path, fights back when attacked, passes over a foe whose shots hit a
+ wall (it is out of line of fire from here), closes stray loot windows.
+- `travel-to` resumes after random encounters; refuses inside a town map (walk out first).
+- `journey <legs…>` (game A) = `travel-to` per leg, expecting fights: an encounter's talk that offers a fight gets
+ that option (the game's dialog data marks it: code `co`), the fight is fought, combat mode is left once nothing is
+ after the party, the leg goes on. Any other talk faults with the speaker: story talks stay scripted. The scout's
+ route answer prints one `journey` line. Worth copying: one verb owns the whole interruption loop, so scripts never
+ carry per-site encounter patches.
+- Setup cheats (no random encounters) are run state, not world state: a checkpoint must carry them, and a format change
+ must change the cache key, or a resume quietly plays a different game (game A met ambushes for hours that way).
+- `fight` steps two tiles along the path toward a foe it cannot hit from here.
+- Arrival snaps: travel near a named area lands at the area (the nearest wins), not at the clicked spot; plan the next
+ leg from where the PC actually lands.
+
+## Pace (watchable, recordable runs)
+
+- A long A* every poll was most of a window run's cost (game A: up to 25 ms per game frame); plan the way once per
+ step, keep it while the PC is on it, plan again off it or after a click that did not move the PC.
+- Frames per drawn frame must stay a fixed count (determinism: steps locate on what is drawn), so the wall-clock pace
+ swings with what each frame costs. Even it out with a hold, not a count: each game frame due at k / fps, held until
+ then; a lag beyond 0.1 s re-anchors instead of bursting (game A `RunPace`, `--pace 120`; off by default).
+- The bigger stalls were holds: a window run that holds the game while its scene rebuilds stalls whenever something
+ forces rebuilds — game A rebuilt the whole static scene on every critter pose frame and on travel fatigue damage
+ (each world-map tick). Measure first (frames held per second, by cause), then key rebuilds on what the static scene
+ actually draws and skip holds while the scene is covered (world map).
+- Determinism in a window: clicks pick on the drawn scene, so which game frame was drawn last must not depend on wall
+ time. Fixed game frames per draw (no wall-time budget), a completed step ends the batch and the next render only
+ draws, and overlays that come and go on UI callbacks (a loading splash) must not take hits.
+
+## Gotchas met so far (game A)
+
+- Bodies decay (a game day): loot quest items when the foe dies, not chapters later.
+- Killing gatekeepers can flip a faction's reaction town-wide (a reputation), not just the witnesses.
+- Off-screen objects can't be clicked: walk next to them first; the step's poll budget can run out while walking.
+
+## Gotchas met so far (game B, Source-style 3D maps)
+
+- Dialogue effects run at the line's end, not when it starts: an NPC line's action (a quest state, a G var) lands
+ after its audio (a 9.8 s mp3), so wait out the line before probing or answering. Time waits from the speech file's
+ length (`Mp3Reader.Seconds`); an oracle without audio (Wine here) ends lines early, so probe both at a fixed late time.
+- Map exits can start hidden: a `trigger_once` in front unhides the `trigger_changelevel` 0.2 s later. A teleport
+ straight onto the exit then does nothing; land on it and wait, or walk through.
+- Pickups are picked by the use ray or by a near-item cone, which a teleport onto the item's spot can miss (eye
+ inside / looking past it): stand a step beside the item and look at it.
+- Map-to-map with landmarks: after a changelevel the PC lands at the landmark offset, not at the teleport spot; plan
+ the next leg from the `arrive <map> <origin>` line.
+- Teleports land a few units above the floor: a spot a hair under it starts the player inside the world (every move
+ blocked from the first frame; game B: z 36.1 vs a floor at 36.03+).
+- A straight `walk-to` that stops short is often a closed door (stuck right in front of it): `face` it, use, wait for
+ it to swing, walk on. Use reach is measured from the eye (96 units in Source): walk close before `press-use`.
+- First-person aiming: closed loop. Send the mouse move for the remaining turn every frame and finish within a
+ tolerance (1°); the mouse filter (m_filter) and integer pixels make one-shot turns land short.
+- NPCs start talks on their own (an alley bum, a pier hawker, a guard spotting the player): the walk ends with the
+ talk open (game B `walk-to`/`walk-path` stop on a talk). Answer, `wait not dialog-open`, repeat the walk.
+- Node graphs (Source `.ain`) skip stairwells, tunnels and building interiors: `walk-path` to the last covered spot,
+ then chain `walk-to` legs found with a floor scan (game B `floor-map`: floor height per grid cell under the player
+ hull; start just above the level you want, or roofs and upper flights answer). Stairs show as steady height steps.
+- A hull is 32 wide: thin invisible posts and rails decide which line works. Compare with the original before calling
+ it a collision bug (game B pier: the original's player clears a post by 0.125 units at y −1608; ours aimed at −1600
+ and stuck; the brush was the same).
+- Gates and doors can be locked or unlocked by quest state when the map loads (game B `beachHouseOpen()` on
+ OnMapLoad): a debug scout started on that map sees a different world than the quest run.
+- A changelevel lying on a stair or ledge may never meet the hull when walking down (the hull rides the edge): come back
+ up into it, or stand still inside it.
+- Original movement as an oracle: teleport the PC, hold forward, sample the origin on a timer; where it stops is where
+ your route must turn.
+- Scout facts from the map data itself (entity lump: changelevel targets + landmarks, `OnStartTouch`/`OnPlayerPickup`
+ outputs, the level script's quest functions, the dialogue's conditions) before trying moves in the game.
+- Quests with many solutions: pick the one the runner can do today and cheat the PC into it in setup (game B
+ `cheat vstats get charisma 5`: the game's own console cheats, so the rest of the run stays player mode). Dialogue
+ checks hide failed replies: `answer n` counts the shown ones, so a stat change renumbers them.
+- Teleport lands next frame: wait a frame or two before a verb that reads the player's position (path planning).
+- Doors rotate about their origin (the hinge): aim use at the leaf's middle. A door can be locked from one side only
+ (game B: the knob on the player's side carries the difficulty): scout the side, not just the door.
+- When a node graph routes through a door the runner cannot open, that door is the designers' intended way: find what
+ opens it (lockpick, key, quest state) before hunting for another path.
+- Watching runs: eased turns look right but change routing: walking while easing round a point closer than the turn
+ radius circles it (stuck fault). Hold forward only when facing the point (tighter when it is near), let go at sharp
+ corners. Run the runner in lockstep with the window's frame loop (one runner frame per drawn frame, each side waits
+ for the other) so the world is never touched from two threads and digests match headless.