From 39066900773e7857faf2d02e7ed51b71d219d97d Mon Sep 17 00:00:00 2001 From: godosa Date: Tue, 6 Oct 2026 23:39:36 +0200 Subject: godosa-engine: initial public history --- .editorconfig | 19 + .gitignore | 23 + CLAUDE.md | 24 + CREDITS.md | 10 + Directory.Build.props | 9 + Directory.Packages.props | 18 + Godosa.slnx | 9 + LICENSE | 21 + README.md | 12 + docs/notes/avalonia-gotchas.md | 53 ++ docs/notes/run-scripts-routing.md | 124 +++++ docs/notes/test-gotchas.md | 29 ++ licenses.md | 22 + scripts/alsoft_wave.sh | 18 + scripts/audio_report.py | 101 ++++ scripts/digest_compare.py | 34 ++ scripts/godosa_licenses.py | 145 ++++++ scripts/mutate.sh | 52 ++ scripts/test_audio_report.py | 44 ++ scripts/test_digest_compare.py | 42 ++ scripts/test_godosa_licenses.py | 116 +++++ scripts/test_thread_cpu.py | 24 + scripts/test_thread_instr.py | 21 + scripts/thread_cpu.py | 60 +++ scripts/thread_instr.py | 42 ++ src/Godosa.Audio/Godosa.Audio.csproj | 18 + src/Godosa.Audio/OpenAlAudio.Reverb.cs | 83 +++ src/Godosa.Audio/OpenAlAudio.Streams.cs | 119 +++++ src/Godosa.Audio/OpenAlAudio.cs | 360 +++++++++++++ src/Godosa.Core.Tests/Audio/AudioMixTests.cs | 22 + src/Godosa.Core.Tests/Audio/Mp3Tests.cs | 90 ++++ src/Godosa.Core.Tests/Audio/SynthTests.cs | 122 +++++ src/Godosa.Core.Tests/Audio/WavReaderTests.cs | 127 +++++ .../Diagnostics/DebugFlagsTests.cs | 59 +++ .../Diagnostics/FrameTimeStatsTests.cs | 97 ++++ .../Diagnostics/GpuAndCsvTests.cs | 53 ++ src/Godosa.Core.Tests/Diagnostics/PerfCsvTests.cs | 38 ++ src/Godosa.Core.Tests/Godosa.Core.Tests.csproj | 13 + src/Godosa.Core.Tests/Input/GamepadScriptTests.cs | 56 ++ src/Godosa.Core.Tests/Input/InputScriptTests.cs | 73 +++ src/Godosa.Core.Tests/Input/MenuCursorTests.cs | 42 ++ src/Godosa.Core.Tests/Runs/RouteLegsTests.cs | 116 +++++ src/Godosa.Core.Tests/Runs/RunsTests.cs | 149 ++++++ .../Runs/Scripts/ActionInputTests.cs | 47 ++ .../Runs/Scripts/CheckpointCacheTests.cs | 185 +++++++ .../Runs/Scripts/ConditionTests.cs | 63 +++ src/Godosa.Core.Tests/Runs/Scripts/FakeGame.cs | 100 ++++ .../Runs/Scripts/ReviewFixTests.cs | 157 ++++++ .../Runs/Scripts/RunReportTests.cs | 83 +++ .../Runs/Scripts/ScriptLoaderTests.cs | 103 ++++ .../Runs/Scripts/ScriptParserTests.cs | 111 ++++ .../Runs/Scripts/ScriptRunFlagsTests.cs | 54 ++ .../Runs/Scripts/ScriptRunnerStructureTests.cs | 85 +++ .../Runs/Scripts/ScriptRunnerTests.cs | 224 ++++++++ .../Runs/Scripts/ScriptValidatorTests.cs | 63 +++ .../Runs/Scripts/StepLexerTests.cs | 58 +++ src/Godosa.Core.Tests/Runs/Scripts/VarsTests.cs | 84 +++ src/Godosa.Core.Tests/SettingsFileTests.cs | 90 ++++ src/Godosa.Core/Audio/AudioTypes.cs | 45 ++ src/Godosa.Core/Audio/BiquadFilter.cs | 37 ++ src/Godosa.Core/Audio/Mp3.cs | 133 +++++ src/Godosa.Core/Audio/Pcm.cs | 24 + src/Godosa.Core/Audio/SignalStats.cs | 51 ++ src/Godosa.Core/Audio/Synth.cs | 189 +++++++ src/Godosa.Core/Audio/WavReader.cs | 157 ++++++ src/Godosa.Core/Diagnostics/DebugFlags.cs | 20 + src/Godosa.Core/Diagnostics/EngineDebug.cs | 25 + src/Godosa.Core/Diagnostics/FrameTimeStats.cs | 139 +++++ src/Godosa.Core/Diagnostics/PerfCsv.cs | 27 + src/Godosa.Core/Godosa.Core.csproj | 5 + src/Godosa.Core/Input/GamepadState.cs | 58 +++ src/Godosa.Core/Input/InputScript.cs | 82 +++ src/Godosa.Core/Input/MenuCursor.cs | 21 + src/Godosa.Core/Runs/DigestLine.cs | 13 + src/Godosa.Core/Runs/FrameClock.cs | 23 + src/Godosa.Core/Runs/HeadlessLoop.cs | 20 + src/Godosa.Core/Runs/RouteLegs.cs | 117 +++++ src/Godosa.Core/Runs/RunFlags.cs | 39 ++ src/Godosa.Core/Runs/Scripts/CheckpointCache.cs | 112 ++++ src/Godosa.Core/Runs/Scripts/Condition.cs | 234 +++++++++ src/Godosa.Core/Runs/Scripts/Input.cs | 24 + src/Godosa.Core/Runs/Scripts/InputPlans.cs | 111 ++++ src/Godosa.Core/Runs/Scripts/LibVerbs.cs | 32 ++ src/Godosa.Core/Runs/Scripts/RunGame.cs | 68 +++ src/Godosa.Core/Runs/Scripts/RunOutput.cs | 21 + src/Godosa.Core/Runs/Scripts/RunReport.cs | 58 +++ src/Godosa.Core/Runs/Scripts/ScriptAst.cs | 25 + src/Godosa.Core/Runs/Scripts/ScriptLoader.cs | 218 ++++++++ src/Godosa.Core/Runs/Scripts/ScriptParser.cs | 231 +++++++++ src/Godosa.Core/Runs/Scripts/ScriptProgram.cs | 57 +++ src/Godosa.Core/Runs/Scripts/ScriptRunFlags.cs | 100 ++++ src/Godosa.Core/Runs/Scripts/ScriptRunner.cs | 568 +++++++++++++++++++++ src/Godosa.Core/Runs/Scripts/ScriptTypes.cs | 46 ++ src/Godosa.Core/Runs/Scripts/ScriptValidator.cs | 109 ++++ src/Godosa.Core/Runs/Scripts/StepLexer.cs | 104 ++++ src/Godosa.Core/Runs/Scripts/Vars.cs | 174 +++++++ src/Godosa.Core/Runs/SeedStreams.cs | 14 + src/Godosa.Core/Runs/StateDigest.cs | 51 ++ src/Godosa.Core/SettingsFile.cs | 49 ++ src/Godosa.Desktop/Buttons.cs | 15 + src/Godosa.Desktop/CursorWarp.cs | 66 +++ src/Godosa.Desktop/DisplayMode.cs | 19 + src/Godosa.Desktop/FrameTimeGraph.cs | 65 +++ src/Godosa.Desktop/GlProgram.cs | 35 ++ src/Godosa.Desktop/Godosa.Desktop.csproj | 12 + src/Godosa.Desktop/GpuTimer.cs | 71 +++ src/Godosa.Desktop/InputScriptPlayer.cs | 124 +++++ src/Godosa.Desktop/PadNavigator.cs | 85 +++ src/Godosa.Gamepad/Godosa.Gamepad.csproj | 11 + src/Godosa.Gamepad/SdlGamepadSource.cs | 107 ++++ 110 files changed, 8457 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 CREDITS.md create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 Godosa.slnx create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/notes/avalonia-gotchas.md create mode 100644 docs/notes/run-scripts-routing.md create mode 100644 docs/notes/test-gotchas.md create mode 100644 licenses.md create mode 100755 scripts/alsoft_wave.sh create mode 100755 scripts/audio_report.py create mode 100755 scripts/digest_compare.py create mode 100644 scripts/godosa_licenses.py create mode 100755 scripts/mutate.sh create mode 100644 scripts/test_audio_report.py create mode 100644 scripts/test_digest_compare.py create mode 100644 scripts/test_godosa_licenses.py create mode 100644 scripts/test_thread_cpu.py create mode 100644 scripts/test_thread_instr.py create mode 100755 scripts/thread_cpu.py create mode 100755 scripts/thread_instr.py create mode 100644 src/Godosa.Audio/Godosa.Audio.csproj create mode 100644 src/Godosa.Audio/OpenAlAudio.Reverb.cs create mode 100644 src/Godosa.Audio/OpenAlAudio.Streams.cs create mode 100644 src/Godosa.Audio/OpenAlAudio.cs create mode 100644 src/Godosa.Core.Tests/Audio/AudioMixTests.cs create mode 100644 src/Godosa.Core.Tests/Audio/Mp3Tests.cs create mode 100644 src/Godosa.Core.Tests/Audio/SynthTests.cs create mode 100644 src/Godosa.Core.Tests/Audio/WavReaderTests.cs create mode 100644 src/Godosa.Core.Tests/Diagnostics/DebugFlagsTests.cs create mode 100644 src/Godosa.Core.Tests/Diagnostics/FrameTimeStatsTests.cs create mode 100644 src/Godosa.Core.Tests/Diagnostics/GpuAndCsvTests.cs create mode 100644 src/Godosa.Core.Tests/Diagnostics/PerfCsvTests.cs create mode 100644 src/Godosa.Core.Tests/Godosa.Core.Tests.csproj create mode 100644 src/Godosa.Core.Tests/Input/GamepadScriptTests.cs create mode 100644 src/Godosa.Core.Tests/Input/InputScriptTests.cs create mode 100644 src/Godosa.Core.Tests/Input/MenuCursorTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/RouteLegsTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/RunsTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ActionInputTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/CheckpointCacheTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ConditionTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/FakeGame.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ReviewFixTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/RunReportTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ScriptLoaderTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ScriptParserTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ScriptRunFlagsTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerStructureTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ScriptRunnerTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/ScriptValidatorTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/StepLexerTests.cs create mode 100644 src/Godosa.Core.Tests/Runs/Scripts/VarsTests.cs create mode 100644 src/Godosa.Core.Tests/SettingsFileTests.cs create mode 100644 src/Godosa.Core/Audio/AudioTypes.cs create mode 100644 src/Godosa.Core/Audio/BiquadFilter.cs create mode 100644 src/Godosa.Core/Audio/Mp3.cs create mode 100644 src/Godosa.Core/Audio/Pcm.cs create mode 100644 src/Godosa.Core/Audio/SignalStats.cs create mode 100644 src/Godosa.Core/Audio/Synth.cs create mode 100644 src/Godosa.Core/Audio/WavReader.cs create mode 100644 src/Godosa.Core/Diagnostics/DebugFlags.cs create mode 100644 src/Godosa.Core/Diagnostics/EngineDebug.cs create mode 100644 src/Godosa.Core/Diagnostics/FrameTimeStats.cs create mode 100644 src/Godosa.Core/Diagnostics/PerfCsv.cs create mode 100644 src/Godosa.Core/Godosa.Core.csproj create mode 100644 src/Godosa.Core/Input/GamepadState.cs create mode 100644 src/Godosa.Core/Input/InputScript.cs create mode 100644 src/Godosa.Core/Input/MenuCursor.cs create mode 100644 src/Godosa.Core/Runs/DigestLine.cs create mode 100644 src/Godosa.Core/Runs/FrameClock.cs create mode 100644 src/Godosa.Core/Runs/HeadlessLoop.cs create mode 100644 src/Godosa.Core/Runs/RouteLegs.cs create mode 100644 src/Godosa.Core/Runs/RunFlags.cs create mode 100644 src/Godosa.Core/Runs/Scripts/CheckpointCache.cs create mode 100644 src/Godosa.Core/Runs/Scripts/Condition.cs create mode 100644 src/Godosa.Core/Runs/Scripts/Input.cs create mode 100644 src/Godosa.Core/Runs/Scripts/InputPlans.cs create mode 100644 src/Godosa.Core/Runs/Scripts/LibVerbs.cs create mode 100644 src/Godosa.Core/Runs/Scripts/RunGame.cs create mode 100644 src/Godosa.Core/Runs/Scripts/RunOutput.cs create mode 100644 src/Godosa.Core/Runs/Scripts/RunReport.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptAst.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptLoader.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptParser.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptProgram.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptRunFlags.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptRunner.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptTypes.cs create mode 100644 src/Godosa.Core/Runs/Scripts/ScriptValidator.cs create mode 100644 src/Godosa.Core/Runs/Scripts/StepLexer.cs create mode 100644 src/Godosa.Core/Runs/Scripts/Vars.cs create mode 100644 src/Godosa.Core/Runs/SeedStreams.cs create mode 100644 src/Godosa.Core/Runs/StateDigest.cs create mode 100644 src/Godosa.Core/SettingsFile.cs create mode 100644 src/Godosa.Desktop/Buttons.cs create mode 100644 src/Godosa.Desktop/CursorWarp.cs create mode 100644 src/Godosa.Desktop/DisplayMode.cs create mode 100644 src/Godosa.Desktop/FrameTimeGraph.cs create mode 100644 src/Godosa.Desktop/GlProgram.cs create mode 100644 src/Godosa.Desktop/Godosa.Desktop.csproj create mode 100644 src/Godosa.Desktop/GpuTimer.cs create mode 100644 src/Godosa.Desktop/InputScriptPlayer.cs create mode 100644 src/Godosa.Desktop/PadNavigator.cs create mode 100644 src/Godosa.Gamepad/Godosa.Gamepad.csproj create mode 100644 src/Godosa.Gamepad/SdlGamepadSource.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a8652a5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{csproj,props,slnx,xml,json,yml}] +indent_size = 2 + +[*.cs] +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_prefer_braces = when_multiline:suggestion +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_sort_system_directives_first = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba6647d --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +bin/ +obj/ +.vs/ +.vscode/ +.idea/ +*.user +__pycache__/ +*.pyc +.worktrees/ +out/ + +# private state (lives in the private project repo) +TASKS.md +tasks/ +workflow.toml +docs/plans/ +docs/specs/ +docs/AREA-NOTES.md +USER-NEXT.md +CATALOG.md +publish-denylist.local +archive/ +.wf/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e8ee1b2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,24 @@ +# CLAUDE.md — godosa-engine + +Shared engine library for Avalonia + Silk.NET games. Terse everywhere. +Private notes (tasks, specs, catalog) are kept outside this repo; never add them here (see `.gitignore`). + +## Rules +- Lib knows no game: no game types, game names, env var names or settings classes. The game passes values in + or implements a small interface (`IAudioMix`, key-target `Func`, env var name via `Configure`). +- `Godosa.Core` = pure .NET, tested. `Godosa.Desktop` / `Godosa.Audio` / `Godosa.Gamepad` = Avalonia+GL, OpenAL, SDL glue. +- `public` for anything a game uses. Warnings are errors. Package versions only in + `Directory.Packages.props`, equal to the games' (Avalonia 12.1.2, all Silk.NET 2.23.0). +- New tests mutation-checked: `scripts/mutate.sh` = KILLED. +- Breaking API is allowed: the games adapt when they take the update. + +## Build / test (≈ 10 s) +`dotnet build Godosa.slnx` (0 warnings) · `dotnet test Godosa.slnx` · `python3 -m unittest discover -s scripts -p 'test_*.py'`. +From a game: its solution includes the lib projects, so its `dotnet test` runs the lib tests too. + +## Git +- Branch `master`. Games use this repo as the submodule `lib/godosa-engine`. +- Work in a git worktree on a branch (`.worktrees/`), not in the main tree; ff-merge to `master` when green. + +## Where +`src/Godosa.*` · `scripts/` · `docs/notes/`. diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..848f086 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,10 @@ +# Credits + +| Kind | Who | Note | +|---|---|---| +| Author | godosa | | +| Libraries | see [licenses.md](licenses.md) | licence notices | +| Origin of the code | lifted from earlier games of the same author | | +| Tooling | AI assistance (Claude, Anthropic) | engineering tool; statement in README | + +The lib holds no game content and no code ported from any original game. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..ae58eb3 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + net10.0 + enable + enable + latest + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..e936019 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,18 @@ + + + true + + + + + + + + + + + + + + + diff --git a/Godosa.slnx b/Godosa.slnx new file mode 100644 index 0000000..1f3b428 --- /dev/null +++ b/Godosa.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c6247a1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 godosa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..210847b --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# godosa-engine + +Shared engine library for the Avalonia + Silk.NET games in this folder (timing, perf CSV, debug +flags, GPU timer, GL helpers, audio, gamepad). Entry points: `src/Godosa.*`; notes: `docs/notes/`. + +Licence: MIT, see [LICENSE](LICENSE). Third-party notices: [licenses.md](licenses.md). Credits: [CREDITS.md](CREDITS.md). + +## From the author + +These projects are things that have been tumbling about in my head for a long time, and I now feel like trying to do something with. The bulk of my focus here is on some of the games that I love, but every time I boot them up, I just fiddle about in the menu and set up mods and fixes for a couple hours, with my drive to play them fizzling out. This is my hope to solve that, and while I am a professional software engineer this would not have happened were it not for the rise of (relatively) cheap AI that could do the bulk of the work with me. If you don't approve, that's fine. I did these things for me, sharing them is something I do in the hopes to help others in similar situations, that just want to play the games they love. Thank you to all that made these playable in the first place, with some luck this finds you, and can bring some joy. + +Written with AI assistance (Claude, by Anthropic), used as an engineering tool. 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//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 ` (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 ` 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`, `IEnumerable`) boxes + the enumerator** — 32–40 B per loop. Hot paths index instead + (`CombatSystem.HitSphere`, `Overlaps`). `List` / 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). diff --git a/licenses.md b/licenses.md new file mode 100644 index 0000000..62dd6d5 --- /dev/null +++ b/licenses.md @@ -0,0 +1,22 @@ +# Third-party licences + +Source of truth for versions: `Directory.Packages.props`. Licence = the package's own nuspec +(`~/.nuget/packages///*.nuspec`), read 2026-09-29. Update this file in the same +commit as any package change. Rule: no copyleft code linked statically. + +| Package | Version | Licence | Used by | Notes | +|---|---|---|---|---| +| Avalonia | 12.1.2 | MIT | Godosa.Desktop | © The AvaloniaUI Project | +| NLayer | 3.0.0 | MIT | Godosa.Core | MP3 decoding (`Mp3Reader`, `Mp3Stream`) | +| Silk.NET.OpenGL | 2.23.0 | MIT | Godosa.Desktop | © .NET Foundation and Contributors | +| Silk.NET.OpenAL | 2.23.0 | MIT | Godosa.Audio | | +| Silk.NET.OpenAL.Extensions.Creative | 2.23.0 | MIT | Godosa.Audio | | +| Silk.NET.OpenAL.Extensions.Soft | 2.23.0 | MIT | Godosa.Audio | | +| Silk.NET.OpenAL.Soft.Native | 1.23.1 | LGPL-2.0-or-later | Godosa.Audio | OpenAL Soft as a separate shared library, loaded at run time (P/Invoke); not linked statically; the user can replace it | +| Silk.NET.SDL | 2.23.0 | MIT | Godosa.Gamepad | SDL2 itself: zlib licence, shared library | +| Microsoft.NET.Test.Sdk | 17.12.0 | MIT | tests only | not shipped | +| xunit | 2.9.2 | Apache-2.0 | tests only | not shipped | +| xunit.runner.visualstudio | 2.8.2 | Apache-2.0 | tests only | not shipped | + +A game that ships the lib must reproduce the MIT notices above and say that OpenAL Soft is LGPL +and where its source is (https://github.com/kcat/openal-soft). diff --git a/scripts/alsoft_wave.sh b/scripts/alsoft_wave.sh new file mode 100755 index 0000000..31644ec --- /dev/null +++ b/scripts/alsoft_wave.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Writes DIR/alsoft.conf so OpenAL Soft renders to DIR/audio.wav (wave backend, stereo +# int16) instead of the speakers, and removes an old DIR/audio.wav. Run the game with +# ALSOFT_CONF=DIR/alsoft.conf. Env: ALSOFT_EXTRA="key = value" lines added to [general]. +# Usage: alsoft_wave.sh DIR +set -euo pipefail +out=${1:?usage: alsoft_wave.sh DIR} +mkdir -p "$out" +rm -f "$out/audio.wav" +cat >"$out/alsoft.conf" <