aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/docs
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
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'docs')
-rw-r--r--docs/notes/avalonia-gotchas.md53
-rw-r--r--docs/notes/run-scripts-routing.md124
-rw-r--r--docs/notes/test-gotchas.md29
3 files changed, 206 insertions, 0 deletions
diff --git a/docs/notes/avalonia-gotchas.md b/docs/notes/avalonia-gotchas.md
new file mode 100644
index 0000000..5150505
--- /dev/null
+++ b/docs/notes/avalonia-gotchas.md
@@ -0,0 +1,53 @@
+# Avalonia / Desktop gotchas
+
+Each one cost debugging time once. The code has comments at each site;
+this file is the index.
+
+- **The GL view isn't hit-testable by default.** `OpenGlControlBase` draws
+ through GL, not Avalonia's scene. Pointer events and `Cursor` go to the
+ panel beneath unless the control implements `ICustomHitTest`
+ (`GameView.HitTest`). Display-only overlays need `IsHitTestVisible =
+ false` so the pointer reaches the game.
+- **Deactivation ≠ lost focus.** Clicking another app deactivates the window
+ but keeps logical focus (`IsFocused` stays true), so `OnLostFocus` never
+ fires. Release held keys on `Window.Deactivated` too, because `OnKeyUp`
+ never arrives for a key released elsewhere.
+- **The sim only advances while frames render.** `GameLoop.Frame` runs
+ inside `OnOpenGlRender`. Stopping `RequestNextFrameRendering` (paused)
+ stops the sim, and "keep simulating while minimized" would need a timer.
+- **The render callback runs on the UI thread** (verified on this Linux/X11
+ setup). The overlay and `FrameTimeStats` share state without locks for
+ that reason. Re-check if a platform moves rendering off-thread.
+- **No pointer lock API** in Avalonia 12.1.2. Free look reads deltas between
+ pointer positions and stalls at the window edge. Recentering uses a
+ platform call (`CursorWarp`, X11 `XWarpPointer`; off where it fails).
+- **Escape vs focused controls.** A clicked `CheckBox` takes keyboard focus,
+ so `Esc` for the options menu is a window-level *tunnel* handler, not
+ `GameView.OnKeyDown`.
+- **Tests: float vs double.** `FixedStepClock.TickSeconds` is `float`. Compare
+ it against double arithmetic with a tolerance (~1e-6), not 9 places.
+- **A clicked in-game `Button` steals the game's keys.** It takes focus, so
+ WASD stops reaching `GameView` and Space presses the button. HUD buttons
+ are `Focusable = false` (`Ui.Action`).
+- **Raising `Button.ClickEvent` doesn't toggle a `CheckBox`.** Toggle logic
+ lives in `ToggleButton.OnClick`; scripted/pad presses use `Ui.Press`.
+- **`KeyBindings` inside a control resolves to Avalonia's
+ `InputElement.KeyBindings`**, not ours — qualify the game's own `KeyBindings` type fully.
+- **Invisible controls aren't templated.** `IsVisible = false` skips
+ measure/template, so the first real open pays for it (first NPC dialog
+ ~170 ms). `Ui.WarmUp()` shows a zero-opacity card briefly instead.
+- **Routed key events only reach controls on the route.** A `KeyDown` raised
+ on the focused `GameView` tunnels window → `GameScreen` → `GameView`; a
+ sibling panel (options) never sees it. Screen-wide key handling lives on
+ `GameScreen` (e.g. key-rebind capture).
+- **Scripted input vs X injection.** On KDE Wayland `xdotool` keys don't reach
+ a focused XWayland window (and may type into the user's terminal); drive
+ smoke runs with `smoke_run.sh --input FILE` (routed events in-process).
+- **The HUD is redrawn every frame over the GL view.** The GL control changes every frame
+ and covers the window, so the compositor re-draws every overlay each frame — text is
+ the expensive part. `CacheMode = new BitmapCache()` on HUD cards (`Ui.Card`) makes them
+ re-render only on change; set `RenderAtScale` to any scale transform above them or they
+ blur. Merging text blocks into fewer visuals doesn't help.
+- **Don't judge perf by CPU % on this machine** (±50 % with other load). Use the `instr:`
+ smoke step, interleaved runs, `PauseWhenInactive` off; see game-loop spec §7.
+
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.
diff --git a/docs/notes/test-gotchas.md b/docs/notes/test-gotchas.md
new file mode 100644
index 0000000..496f323
--- /dev/null
+++ b/docs/notes/test-gotchas.md
@@ -0,0 +1,29 @@
+# Test gotchas
+
+Traps met while writing Core tests. Add one when a flaky or misleading test
+cost real time.
+
+## Allocation tests (`GC.GetAllocatedBytesForCurrentThread`)
+
+- **The counter can jump by one allocation context (~8 KB)** under parallel
+ test load — seen as `8200 B` where the same window normally reads `128 B`
+ (2026-09-25, ~2 in 25 build+test runs). Not reproducible in isolation or
+ with an in-test GC-pressure thread. A budget below ~8 KB must measure
+ several windows and take the quietest (`AmbienceDirector_SteadyState…`);
+ a real per-event allocation shows in every window.
+- **`foreach` over an interface (`IReadOnlyList<T>`, `IEnumerable<T>`) boxes
+ the enumerator** — 32–40 B per loop. Hot paths index instead
+ (`CombatSystem.HitSphere`, `Overlaps`). `List<T>` / arrays typed concretely
+ are fine.
+- **Undrained buffers grow in doublings** in headless tests (`_sounds`,
+ notices, damage events cap at a max but grow to it first): one-off spikes of
+ 0.5–1 KB mid-window are that, not a leak. Budget per tick over ≥ 1000 ticks
+ or drain like the Desktop does.
+- Tests run on Debug builds: Core code is never JIT-optimised there, so tiered
+ compilation / escape analysis don't change what Core allocates.
+
+## Timing tests (`Stopwatch`)
+
+- A build server or parallel tests can steal the CPU for one measurement:
+ take the best of several runs (`AudioSpeedTests.RealtimeFactor`). A real
+ slowdown lowers all of them (checked with a `Thread.Sleep` mutant).