diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /docs/notes/run-scripts-routing.md | |
| download | godosa-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.md | 124 |
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. |
