diff options
| author | Adam Malczewski <[email protected]> | 2026-06-29 01:33:37 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-29 01:33:37 +0900 |
| commit | 4ee00a78fbbf5ff6e493dbffbf1b2d3083b90cfa (patch) | |
| tree | bb91ae1e6cb4e0c8538f9422076975d8c9a320c0 | |
| parent | c1ed35b6e11fd89006ac18f7893f07d4920e9dfc (diff) | |
| download | study-player-4ee00a78fbbf5ff6e493dbffbf1b2d3083b90cfa.tar.gz study-player-4ee00a78fbbf5ff6e493dbffbf1b2d3083b90cfa.zip | |
phase 2: update scar-tissue knowledge with mruby compatibility discoveries (no require, int32_t, keyword_init, ARGV, non-blocking seek)
| -rw-r--r-- | .agents/knowledge/study-player.md | 163 |
1 files changed, 77 insertions, 86 deletions
diff --git a/.agents/knowledge/study-player.md b/.agents/knowledge/study-player.md index a928dec..4142cec 100644 --- a/.agents/knowledge/study-player.md +++ b/.agents/knowledge/study-player.md @@ -1,88 +1,79 @@ -# Tribal knowledge: study-player rewrite (`StudyPlayer::`) - -Study-feature scar tissue for the rewrite of `../source/` into the -raylib-jamstack template. +# Tribal knowledge: Study Player (rewrite) ## At a glance -- **Key files:** `game/study_player/study_player.rb` (composition root), - `game/study_player/core.rb` (pure logic), `game/study_player/audio_adapter.rb`, - `game/study_player/ui.rb` + `game/study_player/ui/main.{rml,rcss}`. -- **Native helper:** `mrbgems/study_audio` (planned Phase 3) exposes raw silence - gaps because raylib `Wave` sample data is not accessible from Ruby. -- **Engine touch:** `src/main.c` needs a small patch to expose command-line args - as `ARGV` (Phase 2); otherwise the `argv → study-player view` requirement is - unreachable from Ruby. -- **Cross-refs:** rules `mruby-rebuild`, `dont-edit-generated`, `wsl-toolchain`; - knowledge `rmlui-binding`, `raylib-binding`, `flecs-binding`, `build-system`; - `notes/study-player-rewrite-plan.md`. - -## Why a native silence scanner is needed - -The generated raylib bindings expose `Rl.load_wave` and `Rl.wave_format`, but -`Wave.data` is a raw `void*` buffer that the generator intentionally skips. -There is no Ruby-visible `Wave#samples`. Ported silence detection therefore needs -a tiny C helper (`mrbgems/study_audio`) that does the I/O and returns a plain -Ruby array of `[start_seconds, end_seconds]` gaps. - -The **decision logic** (threshold comparison, 10 ms chunking, padding shrink, -portion math, auto-pause FSM) stays in pure Ruby under `StudyPlayer::Core`. -The native helper only solves the *data access* problem. - -##ARGV flow - -`src/main.c` currently only uses `argv[1]` as the script path. To satisfy -"audio file arg → study-player view", pass `argv[2..]` into the mruby VM as a -constant named `ARGV`: - -```c -mrb_value argv_ary = mrb_ary_new(mrb); -for (int i = 2; i < argc; i++) { - mrb_ary_push(mrb, argv_ary, mrb_str_new_cstr(mrb, argv[i])); -} -mrb_define_const(mrb, mrb->object_class, "ARGV", argv_ary); -``` - -Web builds inherit this because `Module.arguments` is mapped to `argc/argv` in -emscripten's generated `main()`. - -## Non-blocking seek (`skip_auto_update`) - -After `Rl.seek_music_stream`, `Rl.get_music_time_played` can briefly report the -old position. The original used `skipAutoUpdate = 3` frames to let the audio -engine settle. We model this as a Flecs component field: - -```ruby -PlaybackState = world.struct("PlaybackState", "{ ... int skip_auto_update; }") -``` - -A system applies the seek and sets the counter to 3; the update system skips -`get_music_time_played` while the counter is positive. - -## RmlUi layout quirks - -- Use `left:` / `top:` for absolute elements. RmlUi miscomputes `right:` in this - backend, so a panel `right: 60px; width: 240px` can render off-screen. -- For progress-bar clicks, use the event's `mouse_x` minus the element's - `absolute_left`, divided by `client_width`. -- Checkboxes in RmlUi read/write the `checked` attribute, not just the value. -- The RmlUi context should be initialized **after** `Rl.init_window` and fonts - must be loaded before the document. - -## Adding `mrbgems/study_audio` - -Because the RmlUi gem already forces `MRB_USE_CXX_EXCEPTION`, adding any gem is -an ABI toggle. Follow rule `mruby-rebuild`: - -```sh -rm -rf vendor/mruby/build -./rebuild.sh -``` - -## Constants to preserve - -- Silence threshold: **0.015**. -- Minimum silence duration: **0.75 seconds**. -- Padding zone: **0.25 seconds** on each side of a silence region. -- Default seek lead-in: **2 render frames** = `2.0 / 60.0` seconds - (ported from original; may tune later). -- Default `skip_auto_update`: **3 frames** at target 60 FPS. +- **Key files:** `game/study_player/study_player.rb` (monolithic entry point), + `src/main.c` (ARGV patch), `mrbgems/raylib/src/raylib_bindings.c` + (FilePathList#path_at). +- **Run:** `./zig-out/bin/game game/study_player/study_player.rb [audio.mp3]` +- **Cross-refs:** plan `notes/study-player-rewrite-plan.md`, template rules + `.agents/rules/*`, build `.agents/knowledge/build-system.md`. + +## mruby compatibility discoveries (Phase 2 scar tissue) + +### No require/require_relative/load in default gembox +The mruby default gembox (`conf.gembox 'default'`) does NOT include file +loading. `require`, `require_relative`, and `load` are all undefined. The +template's existing game scripts (ragdoll_demo.rb, etc.) are monolithic +single-file scripts for this reason. + +**Fix for Phase 2:** Inlined all modules into `game/study_player/study_player.rb` +(single ~440-line script). Individual module files (`components.rb`, `core.rb`, +etc.) are retained on disk as design documentation plus a `systems/` directory; +they are NOT loaded at runtime. + +**Future:** Adding `conf.gem 'mruby-require'` to `build_config.rb` would enable +multi-file loading. This triggers the `rm -rf vendor/mruby/build` rebuild rule +and is deferred to Phase 3 (plan §8 mentions `mrbgems/study_audio` which already +requires a full rebuild). + +### Flecs struct descriptor: `int` is invalid → use `int32_t` +Flecs meta descriptor parser does NOT recognise `int` as a type name. Valid +primitive type names in the descriptor string are: +- `int8_t`, `int16_t`, `int32_t`, `int64_t` (signed integers) +- `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` (unsigned) +- `float`, `double` +- `bool` +- `string` +- `char`, `byte` + +`int` → `int32_t` was required in `PlaybackState` and `StudyState` descriptors. + +### Struct.new keyword_init not supported +mruby's `Struct` does not support `keyword_init: true`. Use a plain class with +`attr_accessor` + `initialize` instead (see `StudyPlayer::Runtime`). + +### TOPLEVEL_BINDING not available +CRuby's `TOPLEVEL_BINDING` constant does not exist in mruby. + +### defined? not reliable as a guard +`defined?(ARGV)` is not available; use `begin/rescue` or just reference `ARGV` +directly (it is always set by the `src/main.c` patch). + +## Drag-drop: FilePathList#path_at hand-written method +The generated raylib bindings only expose `FilePathList#count`/`count=`. The +`paths` member is `char**` — a raw pointer the generator skips. A small +hand-written method `path_at(index)` was added to +`mrbgems/raylib/src/raylib_bindings.c` to enable indexed path access for +drag-and-drop. This method is registered on `Rl::FilePathList` after the +generated registrar runs. + +## Non-blocking seek (skip_auto_update) +After `Rl.seek_music_stream`, raylib's miniaudio stream may briefly report the +old position. The `skip_auto_update` counter (set to 3 frames = ~50ms at 60fps) +skips `Rl.get_music_time_played` sampling while decrementing each frame. The +`UpdateSystem` (ON_UPDATE) handles this cooldown; `SeekSystem` (PRE_UPDATE) +applies the seek and sets the counter. This mirrors the original C source's +`skipAutoUpdate` pattern. + +## ARGV patch (src/main.c) +Added `#include <mruby/array.h>` and a block in `main()` that builds an +`mrb_value` array from `argv[2..]` and defines it as `ARGV` constant. This +is the only way Ruby game code can access command-line arguments in this +architecture (there is no `ENV` for process args in the default gembox). + +## Wayland/WSLg screenshot path quirk +`JAMSTACK_SCREENSHOT=...` with a path starting with `/` resolves relative to +CWD, not as an absolute filesystem path. Use a bare filename (e.g. +`JAMSTACK_SCREENSHOT=output.png`) to write in the project root, or use a full +path without a leading `/` quirk. This is a raylib/Wayland interaction, not +a study-player-specific issue, but it surfaces during verification. |
