_Session paused 2026-07-23 (updated): since the note below was first written, a separate
user-directed pass closed out the INFRA-* doc-audit follow-ups and BUILD-009 (CI). Concretely:
the P0-1 behavioral trace, INFRA-003's programmatic reference-doc check, and INFRA-005's merged
X+Z+Y resolve were all confirmed genuinely done (not overclaims); one real doc mismatch (24 vs 25
rows) was fixed; golden-capture/trace were confirmed to actually run in this container via
xvfb-run; and BUILD-009 (GitHub Actions CI for GalaxyEggbertCNA) was built, live-debugged
through 4 real pushes (abbreviated-SHA, missing meta-gl sibling, missing mobile-eggbert test
data — see §3), and confirmed fully green on a real run. Two items from that pass remain
deliberately open, not silently closed: INFRA-004's unverified-icon table is still bookkeeping
only, not wired into the renderer/CI as an enforcement gate; INFRA-006 is now complete after its
final behavior audit. Its former pure cross-cutting membership lists and remaining uniform
parameters now live in ObjectDefinitionRegistry; the deliberately retained conditions are
state-machine branches or narrowly specified cheats, not uncentralized type classifications.
INFRA-004 remains intentionally untouched.
Everything else in the backlog still needs the user's own input before proceeding. The user will
author TILE-005's real 3D worlds manually in the finished in-game editor, so do not generate or
pre-author those levels without a later explicit request. The remaining engineering candidates need
visual/design judgment (AscenseurVertigo/Suspended render geometry, seam-line transparency) or
an asset (the real Blupi model/rig). The code-quality/edge-case audit category is confirmed
exhausted for the prior session (8 rounds, last one genuinely clean — see §3/§10). Do not
manufacture more audit rounds by default; ask the user which backlog item to pick up next, or wait
for new direction._
Last updated: 2026-07-25. The in-game 3D world editor's original plan is COMPLETE: all 13
approved milestones (EDITOR-100 through EDITOR-112) are implemented and verified. User-directed
follow-up work through EDITOR-130 is also complete, including the Eggbert-ordered palette cleanup,
XYZ placement controls/readout, object cell alignment, the Galaxy-only background-thumbnail menu,
functional touch deletion, all 96 source mappings, persisted Level start, editable BigDecor,
correct explo.png scenery rendering, and a world-browser return to the main menu. Editor work resumed
2026-07-23 by explicit user authorization (was paused 2026-07-19 — see
plan.md §6's
own status note for the full history), then finished the same session: EDITOR-111 (sky-region
picker) and EDITOR-112 (unsaved-changes guard + a boundary-straddling box-fill test + section
consolidation) both landed, verified on all 3 native backends. The keyboard-input/window-focus
concern noted when work was paused is still unresolved — re-confirmed twice more this same session
(once before resuming, once again during EDITOR-112's own live-verification attempt, where BOTH
keyboard and mouse input stopped reaching the game entirely, despite working fine for EDITOR-111's
live check earlier the same session) — genuinely intermittent, not a code bug, see plan.md §6's
"Known problems" for the full history. No work remains on the original approved plan; any further
editor work is a separately selected follow-up. EDITOR-127 is now complete: exhaustive static
and real-pointer checks cover all 96 source cells, the full suite is clean, and the final 800×480
editor menu was inspected from a live capture. The subsequent live-reported Palmtree blue-tile
regression and missing browser Back path are fixed as EDITOR-128/129.
Architecture cleanup 2026-07-25: E3D-MIG-110..112 / CLEANUP-S3D-001 is complete. The
retired pre-CNA source tree, its CMake option/targets, and its two obsolete verifier tools were
removed after CNA reached playable parity. The last pre-removal state is recoverable at git commit
4afd53e; a clean CMake inventory contains no retired target, the full -j2 build succeeds, and
all 90 applicable CTest tests pass. The ignored 277MB legacy build/ tree was also removed;
historical entries below are intentionally retained as migration history.
Definition cleanup 2026-07-25: INFRA-011 is complete. The 12 shared definition headers moved
from include/GalaxyEggbert/def/ to include/GalaxyEggbert/Def/, and their declarations now live
in GalaxyEggbert::Def. All consumers use the new namespace directly, without legacy aliases.
The duplicate controller-local SecretPower enum was removed in favor of the shared definition.
The full -j2 build succeeds and all 90 applicable CTest tests pass.
Editor ownership cleanup 2026-07-25: EDITOR-130 is complete. All 25 editor implementation
files moved from src/GalaxyEggbertCNA/Editor/ to src/GalaxyEggbert/Editor/; editor declarations
now live in GalaxyEggbert::Editor, and editor-owned filenames/types no longer carry the legacy
GE prefix (WorldEditor, EditCommandStack, EditorPalette, BoxRegion, and so on). The
game-layer names used by that tree are explicit dependencies, not editor ownership. The full -j2
build succeeds and all 90 applicable CTest tests pass.
Game ownership cleanup 2026-07-25: INFRA-012 is complete. All 45 game implementation files
moved from src/GalaxyEggbertCNA/Game/ to src/GalaxyEggbert/Game/; their declarations now live
in GalaxyEggbert::Game (including GalaxyEggbert::Game::QuadBatch), and game-owned filenames,
classes, and namespaces no longer carry the legacy GE prefix (BlupiController, WorldRuntime,
TerrainRenderer, InputPad, InteractionSystem, SaveData, Sound, Hud, and so on).
src/GalaxyEggbertCNA/ now contains only the CNA application host and entry point. The full -j2
build succeeds and all 90 applicable CTest tests pass.
CNA host naming cleanup 2026-07-25: INFRA-013 is complete. The thin application-host class and
files are now named GalaxyEggbertGame; the redundant Cna fragment was removed because the
enclosing GalaxyEggbert::CNA namespace and src/GalaxyEggbertCNA/ path already state the backend.
The executable target remains GalaxyEggbertCNA. CMake, the entry point, documentation, comments,
and type registration use the new name. The full -j2 build succeeds and all 90 applicable CTest
tests pass.
Central definition cleanup 2026-07-25: INFRA-014/015/016 are complete.
BlockDefinitionRegistry is now the authoritative CNA-independent definition of all supported
voxel ids, including render mode, texture, animation, collision, alpha, and gameplay semantics.
ObjectDefinitionRegistry likewise covers ObjectType0..203, including placement, semantic and
patrol policy, shared declarative interaction capabilities, and phase/override-aware resolved
visuals. Terrain/game/editor consumers use these registries; old BlockTypes/ObjectIcons
classification helpers are thin compatibility accessors.
The new exhaustive VerifyDefinitionRegistries test covers the complete domains and parity
decisions. The full -j2 build succeeds and all 91 applicable CTest tests pass.
INFRA-006 final audit 2026-07-25 — complete. The original instruction to replace every
ObjectType condition with a generic handler table was narrowed to the only safe interpretation:
centralize pure membership/constant rules, but preserve distinct state machines as readable code.
The audit migrated the shared small-enemy set (4/32/33) for Cloud aura and Perso-decoy detection,
the existing avatar semantic category (200..203) for the other side of that trap, generic-hazard
feedback (Small/Big shake plus the matching explosion), the type-3 crouch exception, lethal-decor
contact (201..203), level-exit membership (7/21), and the signed conveyor direction of lifts
47/48. VerifyDefinitionRegistries exhaustively checks every ObjectType0..203; existing
interaction tests exercise each consuming mechanism.
The remaining explicit conditions are intentionally retained: door slide, dynamite fuse/blast
sequencing, bridge construction, follower wake/homing, Blupih/Blupit firing, projectile contact,
wasp balloon interaction, large-creature turn-state contact, and mockery-taunt selection are
different state machines or per-type control flow, not constant dispatch. TryPerso and the
normal-exit/treasure/clean-all cheats are likewise single-purpose commands, not parallel runtime
classification lists. No generic handler abstraction is justified after this audit._
2026-07-20 update: the Saw blade render-orientation bug (§4/§5/§8/§9's own old entries) is now
resolved — see §3's own writeup for the full 6-round history. plan.md §7 ("Correctness
Infrastructure & Dual-Renderer — Vision") is new: a non-binding assessment of merged
REMAKE-ANALYSIS.md/renderers.md material, read it before starting anything framed as
"modernization" or "reduce the bug factory."
Galaxy Eggbert is a faithful 3D remake of mobile-eggbert, itself a C++ port of Speedy
Blupi, a 2013 Windows Phone XNA game. The main goal is to reimplement mobile-eggbert's exact game
logic, levels, and assets in 3D (perspective camera, billboard sprites, 3D-rendered tiles)
without inventing new mechanics — every feature must trace to a confirmed mobile-eggbert
behavior (see CLAUDE.md's "Faithful Remake" rule).
Current development phase: GalaxyEggbertCNA is the sole actively-developed build target. It
has reached a genuinely playable state: real terrain, objects, enemies, hazards, HUD, sound,
save system, and menus all work. The single largest remaining gap is that Blupi himself has no
visible 3D model yet (invisible collision point in first-person, a temporary placeholder model in
third-person).
In-game 3D world editor (COMPLETE, see plan.md §6): GamePhase::Editor,
src/GalaxyEggbert/Editor/, a user-requested feature letting each player create, edit, save
and play-test their own .vwr worlds — inspired by free-eggbert's "Own mission" editor. The user
approved a 13-milestone plan (EDITOR-100..112); all 13 are done, and later user-directed
follow-ups through EDITOR-130 are implemented — see plan.md §6 for the full write-up (what's
built, and a real open concern about keyboard input
possibly being a window-focus issue, re-confirmed twice more as of 2026-07-23, still not a code bug,
not yet confirmed fixed on a real desktop).
Note this editor is an explicit, user-approved exception to the faithful-remake rule (see
plan.md §6): it is content-creation tooling, not a mobile-eggbert feature, so inventing editor
UX is fine — inventing new gameplay mechanics is still not.
Important architectural decisions:
- Direct CNA + Easy3D is the locked, sole target (decided 2026-07-05). The retired pre-CNA
implementation was removed from the live tree on 2026-07-25 and remains in git history at
4afd53e; do not resurrect its dependency path. - Easy3D is a small helper library beside CNA (cameras, texture atlas, batching) — it must not hide CNA; game code calls CNA directly.
../mobile-eggbertis read-only, never modified, even temporarily. Assets (PNGs, sounds, world files) are freely reused by direct path; code/data (tables, enum values, byte layouts) are reference-only and require explicit user approval to transcribe.- No
#ifdefguards for hypothetical engine differences — the game speaks CNA directly. - Worlds are hand-authored via
tools/GenerateSampleWorld3D.cpp(writes all 79worlds3d/*.vwrfiles, includingworld999.vwr, this engine's own quarantined mechanics-showcase/test world); there is no automatic 2D→3D world converter and none is planned. SaveData/InputPad/InteractionSystemare deliberately not byte-compatible with real mobile-eggbert's own formats — real behavior is ported faithfully, byte layout is not.
build-cna builds cleanly and was re-verified after every editor milestone (2026-07-18).
build-cna-vulkan was rebuilt and re-tested 2026-07-19 (through the skate/tank animation-wiring
work) — 76/76 (100%) tests pass, cleaner than build-cna's own 78/79 (the unrelated easy-gl
smoke-test failure doesn't reproduce there, presumably a different target set). The editor's own
code is confirmed backend-agnostic through this.
build-cna/— EasyGL family (CNA_GRAPHICS_RENDERER=OPENGLES3, the default — switched back from Vulkan 2026-07-18, see §3; still fully overridable at configure time).build-cna-vulkan/— Vulkan renderer (CNA_GRAPHICS_RENDERER=VULKAN). Has a known, unrelated CNA engine bug:BasicEffectdraws withAlpha < 1don't render at all under this backend (see §5; not independently re-verified this session — status as last checked). The second real bug found the same day (missing NDC Y-flip inSkinnedEffectshaders, making the third-person placeholder model float) — confirmed committed in../cna(checked 2026-07-21,INFRA-009: all 4 originally-reported Vulkanskinned3d*.vert.glslshaders carry thegl_Position.y = -gl_Position.yfix line; this had previously sat uncommitted, seeplan.mdfor the earlier writeup). Note in passing (not otherwise investigated, out ofINFRA-009's bookkeeping scope):pbr3d_skinned.vert.glsldoes not have the same line — unclear if that effect is onAvatarRenderer's actual render path or a separate, unrelated pipeline; flag if a floating-model bug ever resurfaces specifically under a PBR-skinned material.
Galaxy Eggbert depends on 5 sibling repositories, each under independent development (see the
sharp-runtime note above) — the commits below are what this repo was last verified to build/pass
against. Update this list whenever a sibling repo is rebuilt against (a full local rebuild + ctest
pass), not on every unrelated galaxy-eggbert commit.
| Repo | Commit (as of 2026-07-21) | Date |
|---|---|---|
../easy-3d |
e2d1cfa2 |
2026-07-11 |
../cna |
ac3aaaeb |
2026-07-18 |
../easy-gl |
62c0a248 |
2026-07-19 |
../sharp-runtime |
d2cc9cce |
2026-07-16 |
../meta-gl |
d51fcd7f |
2026-07-18 |
../meta-gl was added to this table 2026-07-23, found missing while setting up BUILD-009's CI
(../easy-gl/CMakeLists.txt unconditionally add_subdirectory(../meta-gl) — a genuine transitive
build dependency of ../easy-gl, not previously tracked here even though it's always been required
locally too).
../mobile-eggbert is deliberately NOT in the table above — it's a test-data, not build,
dependency: VerifyMoveObjectTypesCna/VerifyBigDecorParsingCna read real
../mobile-eggbert/worlds/*.txt files directly (found missing the same way, 2026-07-23, when
BUILD-009's CI ran ctest for the first time against a fresh checkout with no ../mobile-eggbert
at all). cna-ci.yml sparse-checks-out only worlds/ (a few MB, not the full ~2.3 GB repo —
Content/ is irrelevant to these 2 tests), pinned to 07e0a673, 2026-07-16.
Known upstream failures, quarantined (not Galaxy Eggbert regressions if they reproduce again):
easy-gl-resource-smoke-tests(ctest) — root-caused entirely in../easy-gl(test_texture_upload_sets_unpack_alignment_wrap_and_unit0_binding, see §5 for the full root-cause writeup). Every fullctestrun this session shows exactly this one failure and no others — if a future run shows a different failure count/name here, treat that as a real signal worth investigating, not more of the same known issue.build-cna-vulkan'sBasicEffectAlpha < 1non-render bug (see §5/above) — CNA engine bug, not galaxy-eggbert. Still open as far as this repo's records show; worth a fresh live check next timebuild-cna-vulkanis touched for an unrelated reason, since../cna's blend-state handling has changed since this was first found (e.g.ddef5e8f, a Vulkan blend-function-mapping fix already merged before this bug was even reported) — plausible but not confirmed to be related.
GalaxyEggbertCNA also builds and runs in-browser via Emscripten — see plan.md BUILD-003 for
the full writeup. Configure/build:
source <path-to-emsdk>/emsdk_env.sh
cmake -S . -B build-web \
-DCMAKE_TOOLCHAIN_FILE=<path-to-emsdk>/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
-DCMAKE_BUILD_TYPE=Release -DGALAXY_EGGBERT_BUILD_CNA=ON
cmake --build build-web --target GalaxyEggbertCNA -j2Produces build-web/GalaxyEggbertCNA.{html,js,wasm,data} — serve that directory over plain HTTP
(not file://, browsers block .wasm/.data fetches from local files) and open the .html.
CNA_ENABLE_NET must stay ON (the default) even for the web build — GalaxyEggbertGame.cpp
unconditionally references AvatarRenderer symbols from CNA_GamerServices, which is gated behind
it. Verified working via a real headless-Chrome/WebGL2 run (see BUILD-003); it is not wired into
a hosting pipeline, so publishing the built files remains a manual step.
Re-verified 2026-07-19 (user request: rebuild for their own site) — build-web was stale
(last built 2026-07-17, before EDITOR-100..109 landed 2026-07-18); the editor sources were already
wired into the EMSCRIPTEN branch of the GalaxyEggbertCNA target in CMakeLists.txt, so this
was a plain reconfigure + rebuild, no CMake changes needed. Rebuilt cleanly (-j2, only
pre-existing harmless warnings), produced a fresh ~24MB .data + .html/.js/.wasm, and
re-verified with a real headless-Chrome/WebGL2 run driven over the DevTools protocol (served from
build-web/ over local HTTP): console showed a real WebGL2 context, world001.vwr loading,
terrain/background/sound assets loading (93/93 sound channels), a terrain mesh uploading, and a
screenshot confirmed the real Init/gamer-select "Speedy Blupi" screen rendering correctly. (A
headless run using Chrome's --virtual-time-budget flag instead produced a black canvas — a
headless-testing-harness artifact from virtual time desyncing with the app's real async asset
loads, not a build regression; the DevTools-protocol approach with a real wall-clock wait, same as
the original BUILD-003 verification, is the reliable way to test this.)
Note observed this session: a sharp-runtime (external sibling dependency, ../sharp-runtime)
build once failed with a duplicate Environment::SetEnvironmentVariable declaration/definition
conflict, then succeeded on an immediate retry with no changes on this side — that repository
appears to be under active, independent development and the failure was transient. If it recurs,
it is not caused by anything in this repository; check ../sharp-runtime's own git log first.
Last full run (2026-07-21, build-cna only, after INFRA-004):
build-cna: 81 tests, 80 pass (99%) — the only failure iseasy-gl-resource-smoke-tests, a pre-existing, unrelated failure in theeasy-gldependency, root-caused entirely to that repository (see §5). It has been the same single failure across every verification pass this session.GalaxyEggbertWorldsTests(gtest, engine-agnosticWorld/Chunk/MoveObjectRecordmodel): 66/66 pass, included in the total above.VerifyGEWorldEditor: 189PASS:assertions, all passing (165 through EDITOR-109, +24 for EDITOR-110's select/edit/remove tool).build-cna-vulkan: not re-run since the editor work began — last known result was 73/73, but that predates EDITOR-100..110. Re-running it is part of EDITOR-112 (§8).
GalaxyEggbertCNA— the main game executable.GenerateSampleWorld3D— writes all 79worlds3d/*.vwrfiles: the real 78-world mobile-eggbert structure (global hub, 12 world hubs, 64 sublevels, world199) plusworld999.vwr, this engine's own hand-authored mechanics-showcase/test world (found/split out 2026-07-17).- Scripted verification tools (each an
add_test()-registered ctest case):VerifyBlupiMovement,VerifyInteractionSystem(largest suite — pickups, hazards, cheats, secret powers, death/respawn timing),VerifyGEInputPad,VerifyGESaveData,VerifyMoveObjectTypesCna,VerifyBigDecorParsingCna,VerifyTerrainAnimDivisor,VerifyTileUvBounds,VerifyCameraShake,VerifyGEWorldEditor(the whole editor's pure logic: camera math, voxel raycast, box region math, undo/redo, palette data + click hit-testing, custom-world storage, browser screen, object placement),VerifyGetObjIcon(INFRA-003—GetObjIcon()data-integrity regression test), andVerifyUnverifiedRenderMapping(INFRA-004— locks the ~131-icon "unverified render-mode guess" table's shape).
EDITOR-100..110 are complete, verified and pushed. What works today, end to end:
- Enter the editor from the real Init/main menu ("Editor" button), scoped to the selected gamer
slot. A browser screen lists that slot's worlds under
customworlds/gamer<N>/, with create / open / two-tap-delete. - Free-fly camera: WASD + Space/Ctrl, hold RMB for mouse-look, scroll adjusts fly speed.
- Voxel raycast crosshair (Amanatides–Woo DDA) with a live highlight of the aimed-at cell.
- Left click places the selected block on the aimed-at face; middle click removes a block.
F(or the palette's BoxFill button) starts a box-fill: first press/click marks corner A, the highlight tracks a live cuboid, a second press/click fills it as ONE undo command.Escapecancels.- Undo/redo (
U/Ror toolbar), 200-command depth cap. - Palette UI: a single vertical column of solid-green buttons at the left edge (2026-07-19 redesign
to match free-eggbert's own editor palette, a user-supplied reference screenshot — supersedes the
earlier left-toolbar + right-grid layout) — 8 fixed action buttons paired 2-per-row (Undo/Redo,
Save/Back, Play-Test/mode-toggle, BoxFill/Confirmed-All-tab-toggle), a paging row, then the
current page's block/object icons one per row. Both Blocks and Objects mode draw the real
per-type sprite (object-m.png/element.png/explo.png/blupi.png/blupi1.png, whichever
ObjectIconssays the type actually lives on) on a solid green backing — see §3. - Objects mode places real
MoveObjectRecords (enemies, pickups, lifts) that appear immediately as billboards — no save/reload round-trip. Gselects whichever already-placedMoveObjectRecord's billboard projects nearest screen center (reusesHud::ProjectWorldToHudSpace(), no new projection code), shown with a magenta highlight cube.Tsets itsposEndto the current aim point, giving it a real patrol path;Tabcycles which of its 5 fields (speed + the 4 patrol-timing fields)OemPlus/OemMinusnudge;Deleteremoves it. All keyboard-only (no new toolbar button), matching the box-fill tool's own keyboard-only precedent; each edit is a realMoveObjectEditundo command (U/Rundo/redo them like any other edit).- Save (
Enteror toolbar) and a full Play-Test loop: saves, launches a real gameplay session against the custom world, and returns to the editor on win/loss/pause-back without touching real save progress.
Two real bugs found and fixed via this work (both pre-existing, neither editor-specific):
TerrainRendererbuilt and drew an empty static mesh for an all-air world, throwingArgumentOutOfRangeException(primitiveCountmust be positive). Only reachable once "New World" could create a genuinely blank world. Fixed by guarding construction, matching sibling renderers.- The palette fired on mouse release but world edits are edge-triggered on press, so every palette icon/toolbar click also placed a block at the crosshair behind the palette. Present since EDITOR-106; fixed by claiming the mouse per-press, with a regression guard confirmed to fail without the fix.
Two related bug families found and fixed 12 times total that session, both from the same root
cause: real per-mechanic gate clauses that were correct when first ported but never retrofitted
once a LATER feature (vehicles, secret powers) shipped and should have applied to them too. Each
verified directly against Decor.cpp, not guessed.
Family 1 — vehicle-mode gates (11 fixes): Helicopter/Overcraft/Jeep/Tank/Skateboard exclusion/ immunity clauses that predate Phase 17's vehicle implementation. The 4 most recent, on top of the 7 below:
- Springs now forcibly dismount any vehicle first (unless Shield/Hide active) before applying the
bounce, matching real behavior — new shared
DismountAndDepositVehicle()helper. - Most significant fix:
TriggerDeathLock()(every real death cause) now also unconditionally clears vehicle mount/Balloon/Ecrase/every secret power/Invert/Nage/Surf/Suspend/Ghost, matching realBlupiDead()exactly — previously ALL of this state silently survived death/respawn. - Vehicles/Balloon/Ecrase now correctly skip water Surf/Nage detection entirely (not a "forced dismount" as a stale comment claimed — a vehicle just drives over/through water).
- Corrected 2 more stale "vehicles aren't modeled" comments found during this audit.
The original 7:
- Sucette(26)/Drink(30)/Charge(31) pickups now correctly exclude every vehicle mode + Balloon/ Ecrase (Shield/Invert confirmed to have NO such clause, unlike what an earlier note claimed).
TriggerTeleport()now also excludes vehicle mode (previously only checked Balloon/Ecrase).- Ground jump now excludes Jeep/Tank/Helicopter/Overcraft/Balloon entirely; Skateboard gets its own real distinct jump velocity instead of the ordinary headroom-modulated one.
- Spike/Drip/Saw hazards now grant real immunity while riding Overcraft/Jeep/Tank specifically (not Helicopter/Skateboard; Lava/Blitz/Crusher deliberately have no such clause).
- Switch activation now excludes Overcraft/Jeep/Tank/Skateboard/Balloon (Helicopter IS exempt here — real source allows it, unlike every other gate above).
- Dynamite/Perso placement now excludes every vehicle mode + Balloon/Ecrase (Perso's separate pickup-an-already-placed-decoy path deliberately does NOT get this gate — real source has none there either).
TriggerMount()(mounting a NEW vehicle) now also excludes Balloon/Ecrase (its own header comment wrongly claimed this was already handled elsewhere).- Crate push now excludes every vehicle mode + Balloon/Ecrase.
Audit scope note: also checked platform-lift boarding/riding (Decor.cpp:3013-3018) and egg/
door-key pickups directly — confirmed these genuinely have NO vehicle-mode clause in real source,
so no fix was needed there. Not exhaustively re-verified against all ~56 vehicle-exclusion-pattern
occurrences in Decor.cpp (only the ones reachable from already-implemented mechanics), so a
residual few may still exist if new mechanics get implemented later — re-check any NEW
mechanic against this same pattern before assuming it's complete.
Family 2 — Shield/Hide immunity (1 code fix + 3 doc corrections): SecretPower::Shield/Hide
grant real hazard immunity (blupi_.IsInvincible()) — already correctly wired at all 8 real hazard
contact sites (Lava/Spike/Drip/Blitz/Crusher/Saw/Fan/Drown) plus fired-projectiles/dynamite-blast/
wasp/large-creature (verified directly, all consistent). The one genuine gap: the water breath
gauge decremented unconditionally regardless of Shield/Hide, now fixed. 3 plan.md notes still
claimed this immunity "wasn't modeled" for fired-projectiles/Spikes/Fan — stale since 170 shipped
2026-07-12, corrected.
Family 3 — 3 genuinely new gameplay features (not a gate retrofit): ObjectType201-203 lethal
contact (PICKUP-069), the Perso-decoy/lethal-decor enemy trap mechanic, and secret-exit
(ObjectType21) contact (PICKUP-009/083) — real, previously-undiscovered/unimplemented
mechanics found via direct Decor.cpp reads, not vehicle/immunity retrofits like Families 1-2. The
trap mechanic resolves a "what does placing a decoy actually DO" mystery flagged back on
2026-07-13.
Also this session: 2 more hallucinated-feature cancellations (PICKUP-086/087, "shield trail
sound") and 2 clarified-not-fixable notes (PICKUP-027/035, both genuinely blocked on a bigger
prerequisite — AscenseurVertigo's render decision and crate fall-physics respectively — not
independent gaps as previously stated).
(Prior session, 2026-07-13/14): real deferred "Voyage" pickup-reward timing, Clear2/3/4 death VFX, the death-lock + life-loss-Voyage system, Sucette/Drink/Charge's 2-stage pickup delay.
worlds3d/world999.vwr, this engine's own quarantined mechanics-showcase/test world (split out of
world001.vwr 2026-07-17 — see plan.md SCORE-013's writeup — when world001.vwr became the
real, lean global hub). Reachable in-game from the global hub (mission 1) via its own DemoPortal
marker, or directly via GE_DEBUG-style tooling/tests. Playable with first-/third-person camera
toggle (C key), tank-control movement, and the full interactive-object system (pickups, hazards,
enemies, doors, lifts, crates). A second wasp (ObjectType44) sits 5 tiles east of spawn on the
flat corridor floor (added 2026-07-18) specifically so the Balloon status can be triggered and
observed within seconds of loading the world, with zero platforming — the original wasp is on a
north-hill plateau reachable only via a staircase + terraced ascent. Both wasps (and the
object-exhibition grid's bird/wasp specimens) now have a real patrol range and actually
move/animate (fixed 2026-07-20, see §3) — they were silently stationary before.
- No visible 3D Blupi model — invisible collision point in first-person; a placeholder model in third-person. Blocked on the user providing a real model/rig.
World editor: the hardening pass (EDITOR-112) is the one remaining milestone— done 2026-07-23. All 13 approved editor milestones (EDITOR-100..112) are now complete.- The editor has no text rendering — toolbar buttons are distinguished by position and color only (object palette cells now draw real per-type sprites, not flat colors, since the 2026-07-19 redesign — see §3). A documented, deliberate simplification, not an oversight (see §5).
- Several real HUD buttons render but are intentionally inert (no desktop equivalent exists yet):
SetupJump/SetupZoom/SetupAccel,PauseBack,InitRanking/InitBuy. - Idle "fidget" periodic sounds — blocked on
AnimStatevalues this engine doesn't have (same prerequisite as the 3D Blupi model).
Most recent first. Full history: git log.
feat: BUILD-009 — GitHub Actions CI for GalaxyEggbertCNA, confirmed green on a real run (2026-07-23)
New .github/workflows/cna-ci.yml, closing the last open item in plan.md's BUILD-00x list
besides Windows cross-compile/packaging. Checks out galaxy-eggbert plus 5 pinned sibling repos
(cna/easy-3d/easy-gl/sharp-runtime/meta-gl, same commits as this file's "Sibling-repo
commit pins" table above) as directory siblings, plus a sparse (worlds/-only) checkout of
../mobile-eggbert (test data, not a build dependency), reuses ../cna's own already-proven
devices-tests.yml apt package list + vendored-SDL cache pattern, adds a ccache cache on top,
configures -DGALAXY_EGGBERT_BUILD_CNA=ON, builds, then runs
ctest --test-dir build-cna -E 'easy-gl-resource-smoke-tests' (excluding the one known,
pre-existing, unrelated upstream failure documented above).
Scoped out by explicit user decision: no golden-frame/golden-trace steps in CI (no xvfb-run) —
those stay a manual local step, see the entry directly below and §7's commands.
Actually triggered 4 live pushes to develop and let each real failure drive the next fix,
rather than stopping at "looks right by inspection" — 3 real gaps found this way, invisible from
this environment where every sibling repo already happens to be checked out locally:
- Abbreviated 8-char sibling SHAs (
ac3aaaebetc.) failed —actions/checkoutneeds the full 40-char commit hash for an arbitrary-commit fetch. Expanded all 4 original pins. - Configure failed:
../easy-glunconditionally needs../meta-gltoo (a real transitive build dependency, missing from this file's own pin table until now — added above, pinnedd51fcd7f). ctestfailed 2/81 —VerifyMoveObjectTypesCna/VerifyBigDecorParsingCnaread real../mobile-eggbert/worlds/*.txtoff disk, which nothing had checked out. Added a sparseworlds/-only checkout (a few MB, not the full ~2.3 GB repo), pinned07e0a673.
Final result, run 29944268525: fully green, ctest 79/81 (2 correctly excluded), ~6.5 minutes
cold-cache end-to-end. The earlier local-only check (DISPLAY unset, no xvfb-run, 81/82 in
this container) correctly predicted the easy-gl-resource-smoke-tests exclusion but could not
have caught the meta-gl/mobile-eggbert gaps — both are workspace-layout issues only a genuine
clean-checkout run surfaces. Full plan.md BUILD-009 entry has the complete writeup.
A prior conversational claim (not written into this file) said golden capture "couldn't be
completed here" due to a missing SDL/X video device. Re-checked directly rather than trusting that:
xvfb-run/Xvfb ARE installed in this container, and xvfb-run -a ./GalaxyEggbertCNA --golden-capture (using the existing build-cna binary) succeeds cleanly — no display error at
all. Both wrapper scripts confirmed green the same way: xvfb-run -a tools/verify_golden_frames.sh build-cna → ALL CHECKS PASSED (all 3 frames byte-match); xvfb-run -a tools/verify_golden_trace.sh build-cna → ALL CHECKS PASSED. This file's own §7 commands were already correct (they specify
xvfb-run) — nothing here was actually wrong in the docs, the earlier claim was just an
unverified/incorrect statement made in conversation, not a doc bug. Noted here so it isn't
re-asserted as an open blocker again without re-checking.
One more fresh-angle audit round, this time targeted rather than a generic sweep: the world
editor's EditCommandStack/WorldEditor dirty-tracking/undo-redo state machine, which reached
feature completeness this session (EDITOR-100..112) but had never had this specific lens applied.
Checked, by direct code reading (not speculation): every EditCommand::Kind variant's Undo()/
Redo() correctly inverts what its matching Push() site populated (no stale/default field found
in any variant); Push() unconditionally clears the redo stack so a post-undo edit can never let
redo resurrect a stale command (already tested); undo/redo on an empty stack are no-op false
returns (already tested); dirty_'s sticky-until-Save behavior (even across Undo) is a deliberate,
documented design choice, not a bug, since an undo doesn't guarantee the on-disk file matches;
backConfirmArmed_ is correctly reset by every path that should reset it, including both Undo and
Redo; every EditCommand field is a plain value type (Worlds::Block, MoveObjectRecord,
scalars) with no pointers/references, so no dangling-reference risk exists.
Result: no genuine bug found — nothing changed, nothing committed. One coverage-only note (not
a defect): RefreshSelectedObjectAfterHistoryChange() (WorldEditor.cpp:644-659), which re-syncs
hasSelectedObject_/selectedObject_ after every undo/redo, is verifiably correct by inspection
but has no test at the WorldEditor::Update() level (no public accessor exists to inspect
selection state from a test) — left as a documented gap, not invented a test around a nonexistent
accessor just to have one. Together with the file-I/O trust-boundary audit above (which DID find a
real bug) and the immediately-prior public-API sweep, this confirms the code-quality/edge-case audit
category has now genuinely reached diminishing returns rather than just "narrowing" — see §10.
A follow-up file-I/O trust-boundary audit (a different angle from the public-API sweep below —
specifically looking for external/parsed data flowing unvalidated into array indices or numeric
conversions, the same shape as the SaveData bug) checked every disk-reading path in
src/GalaxyEggbertCNA/ and src/GalaxyEggbert/. The core .vwr/.vch binary loaders
(World::loadFromFile(), Chunk::read()) were already exhaustively guarded at every parsed field.
One real gap found: WorldRuntime::LoadFromMobileEggbertFile()'s Decor:/BigDecor: cell
parser called std::stoi() directly with no try/catch anywhere in the function or its callers — a
non-numeric or over-long cell throws, uncaught, terminating the process. Confirmed as an actual
crash via deliberate bug injection (removing the fix's own try/catch): terminate() from an
uncaught std::invalid_argument.
Not reachable from any real gameplay path today (this function's only actual callers are 4 test
tools, all reading trusted ../mobile-eggbert reference files — confirmed by grepping every call
site) — lower severity than the SaveData crash, which was reachable through live save-file
loading. Fixed anyway since it's real defect in a genuine public API this project deliberately keeps
callable (not dead code). Fix: a SafeStoi() wrapper returning 0 on parse failure, which the
existing if (tileId > 0) gate at both call sites already treats as "nothing here" — no separate
error path needed. New regression test (a synthetic malformed file with a non-numeric cell,
confirming the loader survives it) in tools/VerifyBigDecorParsingCna.cpp; verified real teeth via
bug injection (reproduced the actual crash), reverted. Full regression clean on all 3 native
backends. 2 other minor hardening opportunities were found and consciously left alone (an unbounded
chunksPerAxis resource-exhaustion shape in World.cpp, and a defense-in-depth-only gap in
BlockTypes::fromMobileIconId()) — see plan.md's own note for the reasoning.
A follow-up audit systematically grepped every public method in BlupiController.hpp/
ObjectIcons.hpp/InputPad.hpp/WorldRuntime.hpp against its own test file, treating zero
hits (after ruling out coverage split across a sibling test file) as a real finding. InputPad
and WorldRuntime came back fully covered; 3 real gaps closed:
ObjectIcons's 5 texture-atlas-selection predicates (IsUniformCubeObject/IsObjectMPngSourced/IsExploPngSourced/IsBlupiPngSourced/UsesBlupi1Texture) had zero coverage anywhere — these gate which of 5 real sprite sheets anObjectType's billboard samples from; a type moved between lists would render with a completely wrong/garbage texture, silently, with no assertion catching it. Added a regression lock on today's exact membership plus 2 exhaustive invariant checks across every realObjectType(0-255): the 4 predicates are mutually exclusive, andUsesBlupi1Textureis always a subset ofIsBlupiPngSourced.BlupiController::TriggerBye()(real "Bye" farewell freeze) had zero coverage — added the same "freeze, count down, auto-resume" test shape already used forTriggerOneShotAnim()/TriggerTeleport(), plus 2 of its 7 real exclusion-condition checks.BlupiController::TriggerMockery()(real taunt animations) had zero coverage — added success/already-mocking/cooldown/variant-duration checks. One test-design mistake caught and fixed while writing it:TriggerMockery()has no internal balloon/vehicle exclusion check by design (its own comment says the real gate is satisfied by the CALLER only invoking it whileGetAnimState()==Stop) — a first draft wrongly expected an internal check; fixed to instead verify the actual relied-upon invariant (ballooned state never reads asStop).
Verified real teeth via bug injection on all 3 (texture-predicate exclusivity/subset checks, and
TriggerBye()'s exclusion gate), all reverted. Full regression clean on all 3 native backends.
Same code audit that found the SaveData bug also flagged kMaxEggCount (real MAX_EGG_COUNT,
Decor.cpp:96) defined twice in InteractionSystem.cpp — once as float at the egg-touch gate,
once as int at the voyage-completion gate. Harmless while both agree (both correctly 10), but a
future rebalance touching only one site would silently desync the two independently-enforced gates.
Hoisted to one shared constexpr int both sites now use. Behavior-preserving (both were already
10); full regression clean on all 3 native backends.
A fresh code audit found SaveData::Load()'s selectedGamer parser had zero bounds validation,
unlike the sibling gamer.N.field parser right next to it. Every accessor indexing the selected
slot (GetLives()/SetLives()/etc.) reads gamers_[selectedGamer_] with no check of its own, and
gamers_ is a fixed 3-element array — a hand-edited or corrupted save file with an out-of-range
selectedGamer is a real out-of-bounds access, not just a latent bug. Confirmed as an actual
crash: a deliberate bug-injection test (disabling the new guard) segfaulted VerifyGESaveData
outright.
Fixed both Load() (rejects an out-of-range value, same shape as the sibling parser) and the
public SetSelectedGamer(int) setter (now clamps instead of assigning unchecked). 5 new
VerifyGESaveData checks; verified real teeth via 2 separate bug injections (one reproducing the
actual segfault), both reverted. Full regression clean on all 3 native backends. See plan.md's
MENU-058..069 area for the full writeup.
A gap noted but deliberately left open during INFRA-006's basic-pickup migration: Dynamite's "can't
carry a second stick" gate and Egg's MAX_EGG_COUNT=10 cap both had zero test coverage for the
"touch a second one while already at the limit" case (only BulletPack's own cap already had this,
turns out — the old note may have been stale even when written). Added both, matching BulletPack's
own "push a fresh synthetic instance" pattern.
Found and fixed a real test-harness bug (not production) while writing the Egg test: a first draft
reactivated the SAME real egg object in place between touches; the sample world actually has 3 real
eggs, and once the reused object got pruned from the mobile-object vector after enough real
world.Update() ticks, the next "first ObjectType6" search silently matched a different egg
elsewhere, whose position no longer matched the touch position — later iterations silently touched
nothing, which read exactly like a stuck cap until traced with temporary debug prints. Fixed by
switching to fresh synthetic pushes throughout.
Verified real teeth via bug injection on both new tests (Dynamite: disabling its gate caught all 3
new checks; Egg: disabling its touch-time gate caught 1 of 3 — the other 2 stayed green because
ApplyVoyageReward()'s own separate completion-time cap check still blocked the reward, a genuine
defense-in-depth pair in the real code, not a test weakness). Both reverted. Full regression clean
on all 3 native backends. See plan.md's INFRA-006 entry for the full writeup.
With the editor plan complete, swept every galaxy-eggbert-only source file (engine-agnostic
src/GalaxyEggbert/Worlds/, MoveObjectRecord.cpp, plus the handful of CNA-side files with no
Easy3D/CNA/Microsoft::Xna header dependency — most CNA game code couldn't be checked this way
without pulling in sibling-repo headers, out of this session's repo-only scope) for compiler
warnings via g++ -Wall -Wextra -fsyntax-only. Found exactly one, in
BlupiController::IsPointSolid(): blockType < 0 where blockType is world.getBlock(...).type()
(std::uint16_t, unsigned) — always false, dead code, functionally harmless (the upper-bound half
of the same check already covered every actually-reachable case). Removed the dead clause; full
regression clean on all 3 native backends (behavior-preserving by construction, confirmed unchanged
test results).
With the editor plan complete, swept plan.md's "Known open bugs" for stale entries. Found one
(plan.md's pre-## 1 preamble list) referencing a NEXT.md §5/§8 task 4 pointer that no longer
exists in this file, and a prior repro attempt in tools/VerifyBlupiMovement.cpp that only tested
VERTICAL collision (landing on top, already passing) — the literal "walkable-through" wording more
naturally means walking INTO the block horizontally, never tested. Added that missing case: a
single-height grass-topped block (icon 107) correctly climbs via the existing kStepLimit step-up
mechanic (same as any curb-height obstacle — likely what the original report actually saw), while a
3-tall wall of the same icon (too tall to step up) correctly blocks like any other wall. Verified
real teeth via bug injection (disabled the step-up branch, caught this new test plus 1 pre-existing
staircase test, reverted). Two new permanent regression checks in VerifyBlupiMovement.cpp, full
suite clean on all 3 native backends. See plan.md's own updated entry for the full writeup — as
far as this investigation could determine, this was never a reproducible bug.
The last of the 13 approved editor milestones. Three parts:
- Unsaved-changes guard. New
dirty_/backConfirmArmed_state onWorldEditor, centralized through a newMarkMutated()helper (replaces all 10needsPresentationRebuild_ = true;call sites) sodirty_can't drift out of sync. The Back toolbar button is now a real 2-tap when dirty: first press only arms (brightens, reusing the existing mode-toggle "on"-state color — no text labels anywhere in this class), second actually leaves, discarding unsaved changes; a single immediate tap when there's nothing unsaved. Save/PlayTest clear the flag on an actual write to disk; a further edit while armed disarms it (still working, not confirming). Scoped to Back only after confirming it's the ONE real path out of an active editing session in this codebase (the browser's Open/New are only reachable after Back already ran) — "Quit" (a real window-manager close) is a separately known, already-declined-to-fix limitation, unrelated here. - A box-fill test straddling the world's own Z=0/Z=99 bounds — a real
WorldEditor-level integration case (distinct fromBoxRegion::NormalizeAndClamp's own EDITOR-105 unit test), confirming the fill loop covers every cell edge-to-edge with no off-by-one. Getting both corners to land on EXACT edge cells needed real care (hit.z is affine in camera height at fixed pitch/yaw; shifting between corners needed 2Update()calls, not one, to cancel an unwanted height drift from the movement key tied to the pitched-down forward vector) — a first, simpler attempt failed for exactly this reason, root-caused via a standalone scratch diagnostic. - Consolidated
VerifyGEWorldEditorinto clearly-named sections — found the file already consistently used clear section markers throughout; completed the top-of-file index with accurate EDITOR-111/112 entries rather than a risky wholesale physical reorder of ~1500 lines of already-passing test code for purely cosmetic adjacency benefit.
Verified real teeth on both new test additions via deliberate bug injection (disabled the guard's
arm-on-first-press branch: caught 4 failures; introduced an off-by-one in the real fill loop: caught
3 failures including 2 pre-existing sibling tests), both reverted. Live verification of the Back
button's brightened-color rendering was attempted (same Xvfb+xdotool technique that worked for
EDITOR-111's own live check earlier this session) but blocked by the SAME known keyboard/window-focus
issue recurring — neither keyboard nor mouse input reached the game at all this time, despite
working fine earlier the same session with no code change in between. A further, independent data
point for that already-tracked, out-of-scope issue, not a new blocker — the underlying logic is
still thoroughly verified via the headless automated suite. Full regression clean on all 3 native
backends. See plan.md's EDITOR-112 entry (§6) for the full writeup.
This closes the editor plan — all 13 approved milestones (EDITOR-100 through EDITOR-112) are now done and pushed.
World editor work resumed this session (was paused 2026-07-19) after re-confirming the "Known
problems" keyboard-input/window-focus concern below still holds (fresh re-read of the input
pipeline, same conclusion as before: not a galaxy-eggbert code bug). A stepper cycling
world.skyRegion() 0-31: Left/Right arrow keys (confirmed unused anywhere in WorldEditor.cpp
before this) plus a new palette toolbar-button pair (SkyRegionPrev/SkyRegionNext), wrapping
0<->31 rather than clamping. New Kind::SkyRegionEdit handling in EditCommandStack (declared
since EDITOR-105, unhandled until now) — 2 scalar before/after fields, Undo/Redo call
world.setSkyRegion(). No text readout needed: each step requests a presentation rebuild, so the
real background itself IS the feedback, reusing the exact mechanism a fresh world-load already has
(falls back to a flat clear color for the 4 ids with no real art, confirmed against the actual
copied Content/backgrounds/ directory, not just the reference doc).
Adding the new toolbar-button row shifted every content-cell/paging hit-test coordinate down one
row; fixed 13 tests broken by the shift (updated hardcoded pixel constants, not reverted the new
row). Verified real teeth via bug injection (wraparound formula briefly replaced with unwrapped
subtraction — caught 2 precise failures, reverted). Live end-to-end verification: a temporary
env-var-gated debug scaffold + real xdotool key sends to the live window under Xvfb confirmed 3
real Right-arrow presses visibly swap the background from region 0's art to region 3's completely
different image; scaffold fully reverted (zero diff in GalaxyEggbertGame.cpp afterward). Full
regression clean on all 3 native backends. See plan.md's EDITOR-111 entry (§6) for the full
writeup.
Moved the three ObjectType exceptions at the shared AdvancePatrolStep() arrival junction into
kPatrolArrivalHandlers[]: projectile 23 and water bubble 15 deactivate at posEnd; goo 34
sticks there by collapsing its patrol endpoints. The default remains the normal transition into the
end-dwell phase. Existing direct coverage already checked bubble deletion and goo sticking; added a
synthetic projectile-arrival test. Removing its table row deliberately caused precisely the new
assertion to fail, then was reverted. Full regression passed on all three native backends (81/81,
79/79 Vulkan, 81/81 debug; the sole known unrelated easy-gl test is excluded where present). See
plan.md's INFRA-006 entry for the full detail.
A fresh survey for a 6th family found only one clean candidate, and it's smaller than the 5 prior
handler-table families: the dynamite-blast victim-membership check (27 types) was an inline 27-way
|| chain at its one call site — every member gets identical treatment (crates as a linked group,
everything else a plain deactivate), so it's a pure membership predicate, not per-type divergent
behavior. Extracted verbatim into IsDestructibleByDynamite(ObjectType), matching the file's
existing IsPlatformLift()/IsCrate()/IsGenericHazard() pattern rather than a new table.
Verified real teeth: temporarily removed the crate (ObjectType12) from the predicate, confirmed the
existing dynamite-blast test's crate-destruction check failed with a precise message, reverted. The
other 26 members have no dedicated test either way — same coverage as before, not a regression.
Two other candidates surfaced by the survey were explicitly NOT taken: the pickup-touch-radius gate
(mostly already covered by the 2 migrated pickup tables, marginal DRY benefit on working code) and
the mockery-taunt qualifying list (real per-type divergence, same shape that ruled out the wider
enemy/hazard family). Full regression clean on all 3 native backends. See plan.md's INFRA-006
entry for the full writeup.
feat: INFRA-006 5th family — handler-table dispatch for the 7 patrol enemies' icon selection (2026-07-23)
The other candidate from the survey that found the vehicle mapping (below), taken after explicit
go-ahead given its enemy-adjacent-code caveat. Bulldozer4/fish17/bird20/blupih32/blupit33/wasp44/
creature54's per-direction/turn-transition icon dispatch in GalaxyEggbertGame.cpp's billboard-
render loop — purely cosmetic icon selection, NOT the kill/damage/contact logic the wider enemy/
hazard family was ruled out for. New kPatrolIconHandlers[] (function-pointer table) +
TryGetPatrolIcon() replace the 7-case switch; GetCreatureIcon() alone drops the shared
signature's direction bool, so it gets a thin same-signature wrapper purely to fit the table.
No existing test covers this exact dispatch switch (VerifyInteractionSystem's icon assertions
call each GetXIcon() directly) — verified via direct value-for-value comparison against the old
switch instead. Unlike the vehicle mapping, this code path IS exercised by the golden harnesses
(world999.vwr's wasp/creature/blupih/blupit patrol objects run every tick during golden capture)
— golden_trace.txt stayed byte-identical (only logs Blupi's own state, so an exercise-only check
here); golden-frame screenshots failed all 3, reproduced identically with the change reverted (same
pre-existing flakiness as before, confirmed unrelated). Full regression clean on all 3 native
backends. See plan.md's INFRA-006 entry for the full writeup.
feat: INFRA-006 4th family — handler-table dispatch for the vehicle ObjectType↔VehicleMode mapping (2026-07-23)
Continued the migration after a fresh survey of both god-methods (enemy/hazard family already ruled
out, see the entry below). Picked the cleanest of 2 new candidates: the vehicle mount/dismount
mapping in GalaxyEggbertGame.cpp (Helicopter13/Jeep19/Tank28/Skateboard24/Overcraft46) — 2
separate 5-case switches doing the SAME bijection in opposite directions (mode→type in
DismountAndDepositVehicle(), type→mode in the action-button mount scan), not a per-type behavior
dispatch like the prior 3 families. New kVehicleModeTable[] + VehicleModeToObjectType()/
ObjectTypeToVehicleMode() replace both switches with one shared lookup. Skateboard's real
TakeSkate/DeposeSkate anim (the only real per-vehicle divergence) stays its own explicit check at
each call site, outside the table — unchanged from before.
Stated plainly: this exact dispatch has no existing automated test (VerifyBlupiMovement/
VerifyInteractionSystem both exercise TriggerMount() directly, bypassing this file's mapping
entirely — confirmed by grep). Verified instead by direct value-for-value comparison against the
old switches (both directions, including the default fallback) — a provably pure bijective
refactor with no new combined behavior, a proportionate verification bar unlike INFRA-005's merge.
Full regression clean on all 3 native backends; golden-trace byte-match unchanged (its scripted
input never presses the action button, so this path is provably unreached); golden-frame flakiness
reproduced identically with the change reverted (confirmed unrelated, same pre-existing issue).
See plan.md's INFRA-006 entry for the full writeup.
Closes the last of INFRA-005's corrected deviations (see the 2026-07-22 audit entry below): the
user chose the most ambitious of 3 offered scopes — a real single merged 3D resolve, not just X+Z —
after 2 further conflicts were surfaced and approved individually first: (1) splitting checkSubcell
into separate horizontal/vertical flags (X/Z need fine sub-tile precision for WorldSelect/DemoPortal;
Y needs coarse whole-block precision so Lava/Crusher/Saw/Blitz/Drip still read as solid ground —
one shared flag can't do both in the same march), and (2) computing dy before the merged call
(verified safe except one accepted narrow edge case in HasJumpHeadroom(), step-up + jump same-tick).
Found and fixed a real bug during implementation, not by research: the first draft's march loop
returned the WHOLE result the instant ANY single axis blocked. Since a grounded landing zeroes
m_velocityY and gravity re-accumulates for exactly one frame before the next landing, the per-tick
dy is small but genuinely nonzero almost every tick while standing still — and the per-step Y check
has no ground-clearance epsilon (unlike the final onGround probe's own y - 0.05f), so Y read as
blocked on the FIRST micro-step nearly every tick. The early return threw away ~18/19 of that tick's
dx/dz too, even though X/Z were never blocked — an observed ~19x walking-speed regression, caught
by VerifyBlupiMovement's existing WorldSelect-contact test (the walker never covered enough ground
to reach the marker). Root-caused via a standalone scratch reproduction tracing X per tick, fixed by
letting each axis march to its own full distance independently within the one shared loop (freezing
only itself on its own block, never stopping the other two).
Golden-trace reference (tests/golden/golden_trace.txt) regenerated: a real, intended behavior
change vs. the pre-merge trace — resting height on ground settles ~0.002 units lower, the direct
consequence of checkSubcellVertical=false (coarse) superseding the old fine per-icon-mask resting
height — plus ~1e-6 rounding noise during airborne arcs from the merged step count. Re-verified
byte-identical across all 3 native backends after regenerating. Also found --golden-capture-trace
itself is occasionally non-deterministic under system load (independent of this change — reproduced
identically with the fix fully reverted via git stash), extending the already-known
--golden-capture screenshot flakiness to the trace mode too; not chased further, out of scope here.
Full regression clean on all 3 native backends (VerifyBlupiMovement all-pass, ctest 81/82, only
the pre-existing unrelated easy-gl-resource-smoke-tests failure). See plan.md's INFRA-005
entry for the full writeup.
Second of the 3 gaps the audit surfaced, now closed. P0-2 asked for GetObjIcon() to be
cross-checked against 08-animations.md programmatically — new tools/ VerifyObjIconAgainstReferenceDoc.cpp parses that doc's §3.1/§3.2 tables at runtime (regex, not
hand-copied numbers) and black-box-measures GetObjIcon()'s actual frame count against what the doc
claims, for all 24 rows.
Found and worked through a genuine information-theoretic limit along the way: a first draft using
pure black-box period/segment-counting (zero source knowledge) got 6 of 24 rows "wrong" — several
tables (kBulldozer = {66,66,67,67,66,66,65,65}, etc.) deliberately hold the same icon across
adjacent slots as an authoring choice, and a repeated-value array is output-indistinguishable from
a genuinely shorter one. Resolved by explicit user decision: read just the per-type hold divisor
from the source (a small structural fact, not the expected data) — with that, 23/24 rows match
exactly, and the 24th (ObjectType25/shield) is the same already-known, already-accepted doc
staleness VerifyGetObjIcon.cpp documents, asserted as a named exception rather than a fresh
failure. Verified real teeth via a deliberate array-size mutation (caught, precise message,
reverted). Full regression clean on all 3 native builds. See plan.md's INFRA-003 entry for the
full writeup.
feat: INFRA-002's "behavioral trace" half — --golden-capture-trace + verify_golden_trace.sh (2026-07-22)
Closes the first of the 3 gaps the same-day audit (below) surfaced. REMAKE-ANALYSIS.md's P0-1
asked for TWO harnesses — golden-image (done) and a deterministic per-tick behavioral trace
(never built). While designing this, found the passive --golden-capture mode never fed Blupi
any simulated input at all (he sits at spawn the whole 180-tick window) — so a meaningful
behavioral trace needed its own scripted input, kept in a genuinely separate mode
(EnableGoldenTraceMode()/--golden-capture-trace) so the already-approved golden_frame_*.png
references stay completely untouched.
New mode drives Blupi through a small fixed script (walk west down the same "tested corridor"
VerifyBlupiMovement.cpp depends on staying unchanged, jump once at tick 40-46, keep walking,
150 ticks), recording one line per tick (position/velocity-Y/grounded/anim-state/anim-icon) to
golden_trace.txt. New sibling script tools/verify_golden_trace.sh, same shape as
verify_golden_frames.sh. Determinism confirmed two ways: identical md5sum across 2 independent
runs (same bar INFRA-001 set), and — stronger than the screenshot check got — byte-identical
across all 3 native backends (build-cna EasyGL, cmake-build-debug EasyGL, build-cna-vulkan
Vulkan), confirming the simulation itself is genuinely backend-agnostic. Full regression clean on
all 3 builds. See plan.md's INFRA-002 entry for the full writeup.
docs+fix: external audit of INFRA-001..005 "done" claims — 4 confirmed overclaims corrected (2026-07-22)
An independent audit challenged several plan.md [x] done markers against REMAKE-ANALYSIS.md's
actual requirement text. Rather than accept or dismiss it, independently re-verified every specific
claim by reading the cited file:line locations and the original P0-1/P0-2/P1-1 spec wording directly
— all held up. Confirmed and fixed:
INFRA-002:verify_golden_frames.shran underset -euo pipefailwith no|| trueafter the game-launch line — if the binary can't even start (no display/GPU), the script died there, before reaching its own per-frameFAILreporting, producing zero output instead of the intended "FAIL: golden_frame_NNNN.png was not captured" per frame. Reproduced with a fake binary that exits 1 (old script: silent; fixed script: correct 3FAILlines) — a real bug, now fixed. Separately, P0-1's "behavioral trace" (deterministic per-tick position/velocity/anim-state log, diffed) was never built — only the screenshot half of P0-1 exists. Left open, not silently folded in.INFRA-003: P0-2 asked for a programmatic cross-check of everyGetObjIconentry against08-animations.md. What exists is a regression lock (data copied from the source file itself) plus one manual spot-check — real value, but not what P0-2 describes. Left open.INFRA-004: confirmed viagrepthatUnverifiedRenderMapping.hppis used only by its own test, nowhere in the actual renderer — a documented reference list a human can check against, not automatic enforcement. Already implicit in the task's own "bookkeeping only" line, now stated explicitly.INFRA-005: the biggest one. P1-1 asked for ONE merged-position resolve covering both axes; the actual code is 3 sequential per-axisResolveMove()calls (X, then Z, then Y) — a real, reasoned design choice (preserves slide-along-wall behavior) that was never disclosed as a deviation from spec. The airborne-wall-clip bug this task actually targeted is still genuinely fixed either way — this correction is about algorithm shape, not that fix being wrong.
All 4 corrections written directly into plan.md's own INFRA-002/003/004/005 entries (dated
2026-07-22, not silently edited into the original "done" text) so the history stays honest. No
functional code changed except the one-line script fix.
Live user bug report: "liska... muze vejit skoro cela do zdi" (the fox can walk almost entirely
into a wall) — turned out to be about Blupi's own third-person placeholder model, not an enemy.
Blupi's collision is a single point, stopping exactly flush with a wall's face; the fox mesh has
real nose-to-tail depth with no compensating render offset, so roughly half its body visually
projected past the collision point into any touched wall. Measured the model's real depth from
fox1.verts.bin (same method as the existing kPlaceholderModelScale): local Z span ~154.72
units, by far the largest axis, consistent with a quadruped body-length axis. Fixed with a new
3rd tuning constant, kPlaceholderModelDepthOffset = 0.6f, applied through the yaw rotation so it
always points opposite the model's current facing. Tuned by eye via live screenshot iteration
(same rigor as the existing kPlaceholderModelScale/kPlaceholderModelYOffset) — confirmed fixed
against a wall and unchanged/correct in open floor. Render-only, doesn't touch collision. Full
regression clean on all 3 native builds. See plan.md's 069 entry for the full writeup.
Live user bug report ("teleporty nefunguji a blupi jimi prochazi skrz" — turned out to mean the
hub-navigation "booth" markers, not the actual Teleport1-4 pillars, which were separately verified
working). Root-caused as a real regression from INFRA-005 (unrelated commit, same day): these
markers' real per-icon quarter-cell mask is genuinely thin (same category as Lava/Crusher/Saw), so
INFRA-005's sub-tile-precision collision correctly stopped treating them as solid — which broke an
accidental dependency, the old coarse collision's step-up mechanic used to elevate Blupi onto
them on contact, which is what let GetGroundBlockType() (gated on IsOnGround()) see them at
all. Confirmed via a direct git-worktree A/B test against the pre-INFRA-005 commit. Real
Decor::IsWorld(m_blupiPos) was never a grounded-check to begin with — fixed by swapping the
trigger site to the already-existing GetBlockTypeAt() (built for water detection, same
"not gated on IsOnGround()" shape) instead of GetGroundBlockType().
Verified live end-to-end twice with temporary instrumentation (reverted before commit): standing
already-on-a-marker still works, and — the actual reported bug — walking into one from a few tiles
away now correctly triggers mission 1 → 10. New VerifyBlupiMovement test documents the exact
distinction (318/318, was 311/311). Full regression clean on all 3 native build dirs.
Also found, unrelated: golden-frame capture (INFRA-001/002) is genuinely non-deterministic in
this environment on repeat runs with zero code change — confirmed via git stash that it's
unrelated to this fix. Contradicts the original "confirmed deterministic" claim; flagged for a
fresh look next time golden-frame work is touched, not chased down here. See plan.md's
SCORE-013 entry for the full writeup.
After 2 low-risk families migrated, the user asked to attempt the explicitly-flagged-risky
enemy/hazard/combat family (blupih/blupit/wasp/creature/follower/projectiles) anyway, carefully.
Read the full ~350-line block line by line before writing anything — confirmed concretely (not
just per the earlier survey's guess) that 5 of its 6 distinct behaviors are genuinely different in
KIND: fired-projectile is always-fatal-and-self-destroys, wasp is never-lethal-never-destroys,
creature is lethal only mid-patrol-turn, blupih/blupit aren't contact hazards at all (their firing
is woven into the shared patrol-turn/dwell-frame timing), follower is a wake-then-home state
machine with its own self-destruct. The one uniform sub-part (the 8-type generic-hazard list) is
already exactly as data-driven as it should be via existing IsGenericHazard()/
IsBalloonPoppableHazard() predicates. Forcing a single handler table over the rest would add
indirection without removing real complexity, risking exactly the kind of subtle bug this project
has worked to avoid. No code changed — this is a deliberate "don't migrate" verdict backed by
having actually read the code, not a shortcut. See plan.md's INFRA-006 entry for the full
writeup.
Migrated Treasure(5)/Key1(49)/Key2(50)/Key3(51)/Dynamite(55) — pickups that self-delete on contact
and defer their reward to voyage completion, no gate or one plain bool gate — to
kBasicPickupHandlers[] + TryCollectBasicPickup(), replacing 5 near-identical case bodies.
Egg(6) and BulletPack(29) deliberately stay open-coded: Egg's voyage endpoint depends on live
lifeEggCount_ state at grant time, and BulletPack has an immediate side effect
(bulletCount_ = kBulletCap) before deactivating — neither fits fixed table data without adding
more complexity than migrating one type is worth.
Verification found an honest, pre-existing gap, not introduced here: bug-injection testing
caught a flipped sparkle-burst flag (1 precise failure), but disabling Dynamite's carry-cap gate
entirely was caught by zero tests — VerifyInteractionSystem's dynamite coverage only exercises
the single-pickup-then-place path, never "touch a second stick while already carrying one." The
same gap likely applies to Egg's/BulletPack's own caps. Left undocumented-as-a-task deliberately
(a 3-pickup-wide test-coverage gap, not something to fold into this one scoped migration) —
verified the gate is correct by direct code inspection instead. VerifyInteractionSystem unchanged
at 547/547; full regression clean on build-cna/cmake-build-debug; golden-frame byte-match
unchanged. See plan.md's INFRA-006 entry for the full writeup.
feat: INFRA-006 2nd family — handler-table dispatch for the 17 self-expiring particle types (2026-07-21)
Continued the migration started by the secret-power-pickup pilot. Surveyed both god-methods fresh
(a dedicated scoping pass, matching the project's own "needs a fresh go-ahead per family"
convention) — GalaxyEggbertGame.cpp's own ObjectType handling turned out thin (37 refs,
mostly already-clean lookup tables); nearly all remaining work is in
InteractionSystem.cpp::Update(). Picked the 17 purely-cosmetic self-expiring particle/effect
types (explosion flashes, splash/sparkle/magic-trail/splat/teleporter-arc effects — full
ObjectType list in plan.md's INFRA-006 entry) as the lowest-risk next family: no Blupi
interaction, no kill/hazard logic, confined to one ~190-line region of the switch. New
kExpiringParticleHandlers[] table + TryTickExpiringParticle() replace the 17 near-identical
if blocks with one lookup + one dispatch.
Real wrinkle preserved, not smoothed over: the original code had two genuinely different
shapes — 7 types always continued regardless of expiry state; the other 10 only continued once
expired, otherwise falling through to the shared AdvancePatrolStep() call (load-bearing for 4 of
them with real posStart→posEnd slides). A per-entry alwaysContinueEvenBeforeExpiry bool keeps
both shapes intact — collapsing them into one would have silently broken the slide types.
Verification: VerifyInteractionSystem unchanged at 547/547 (nearly all 17 types already had
direct expiry-boundary test pairs from earlier work). Confirmed the table has teeth: temporarily
shifted one entry's expiry phase by 1, exactly 1 test failed with the precise message, reverted.
Full regression clean on build-cna (80/81, pre-existing unrelated failure only) and
cmake-build-debug; golden-frame byte-match unchanged (pure dispatch-logic reshaping, no rendering
touched). See plan.md's INFRA-006 entry for the full writeup, including which family (core
enemy/hazard/combat logic) was surveyed and explicitly ruled out as a poor near-term fit.
Found while re-verifying INFRA-005/006 on build-web: ctest's own add_test() registrations for
VerifyBlupiMovement/VerifyInteractionSystem/etc. have always been unconditional (no
NOT EMSCRIPTEN guard), but nobody had ever actually run ctest --test-dir build-web before —
prior web verification (BUILD-003) only checked the live app via headless Chrome. Doing so
aborted immediately: only the main GalaxyEggbertCNA target gets --preload-file'd assets, so
these standalone console tools got an empty virtual filesystem.
Fixed with -sNODERAWFS=1 (real host filesystem access, matching native exactly, zero source
changes) for the 4 tools with relative repo/sibling-dir path reads
(VerifyBlupiMovement/VerifyInteractionSystem/VerifyMoveObjectTypesCna/
VerifyBigDecorParsingCna); VerifyGESaveData needed a different fix instead (its /save path is
an absolute IDBFS mount point, not a relative repo path — NODERAWFS would wrongly map it onto the
real host's own /save) — a new minimal cmake/web/pre-test-save-dir.js just creates /save in
that tool's own in-memory MEMFS.
Second, unrelated bug found along the way: VerifyGEWorldEditor's CustomWorldsDir() check
hardcoded the native-only expected path, missing CustomWorldStorage.cpp's own
#if defined(__EMSCRIPTEN__) branch (/save/customworlds) — fixed by mirroring the same branch in
the test. Confirmed pre-existing (not something the NODERAWFS change touched) and re-verified clean
on build-cna/build-cna-vulkan/cmake-build-debug too after the fix (shared test file).
Full ctest --test-dir build-web: 15/15 (100%), up from an immediate abort. See plan.md's
BUILD-011 entry for the full writeup.
chore: INFRA-005/INFRA-006 verified backend-agnostic on build-cna-vulkan and cmake-build-debug (2026-07-21)
Following the same precedent already used for the world editor, rebuilt (incremental, -j2,
already-configured targets per CLAUDE.md's "reuse before rebuilding") and re-tested the two other
native CNA build directories after the collision-resolver rewrite and pickup-handler-table pilot
landed, since both had so far only been built/tested against build-cna (EasyGL):
build-cna-vulkan(Vulkan backend): clean incremental rebuild;VerifyBlupiMovementandVerifyInteractionSystemboth fully pass standalone; fullctest78/78 (100%).cmake-build-debug(EasyGL, the "default" build target per project docs): clean incremental rebuild; both Verify binaries pass standalone; fullctest80/81, the one failure being the same pre-existing, already-quarantinedeasy-gl-resource-smoke-testsupstream bug (see "Known upstream failures, quarantined" below) — not a regression.
Confirms the unified movement/collision resolver (IsPointSolid/ResolveMove/
RecoverFromPenetration) and the secret-power-pickup handler table are genuinely backend-agnostic,
with no graphics-backend-specific assumptions leaking into either.
First ObjectType family migrated off the open-coded if (obj.type == ObjectTypeN) god-methods
(REMAKE-ANALYSIS.md P1-2). Chose the 5 secret-power pickups (Shield/Power/Cloud/Hide/Invert,
ObjectType 25/26/30/31/40) as the pilot — lowest risk (not core kill/hazard logic), freshest
familiarity (just touched in INFRA-007). New kSecretPowerPickupHandlers[] table
(InteractionSystem.cpp's own anonymous namespace) + one TryGrantSecretPowerPickup() member
function replace what used to be 5 near-identical case bodies in Update()'s big switch with a
single 5-label group calling one shared dispatch — "add/fix a 6th secret-power pickup" now touches
only the table, not the god-method's own case list.
Real scope boundary found while investigating (kept deliberately out of this pilot): 2 OTHER dispatch sites in the same file reference these same 5 types — a dynamite-blast destructible-type list (~27 types) and a pickup-touch-radius gate (~13 types) — but both are cross-cutting concepts spanning far more types than just this family. Fully replacing either would need every other family sharing them migrated too; left as open-coded lists for now. This is the honest shape of "migrate one family at a time" — a first pilot can only absorb dispatch logic entirely confined to its own family.
Behavior-neutral by design: VerifyInteractionSystem unchanged at 547/547. Verified the new
dispatch has real teeth — temporarily flipped one handler's requiresActionButton flag, confirmed 3
tests failed with precise messages, reverted. Full regression clean (80/81, pre-existing unrelated
failure only); golden-frame byte-match unchanged. See plan.md's INFRA-006 entry (marked [~],
pilot done, full migration still open — ~65 more ObjectTypes remain across both god-methods) for
the full writeup.
feat: INFRA-005 — unified movement/collision resolver, closes the airborne-wall-clip gap (2026-07-21)
Scoped with the user first (full unified resolver + sub-tile fidelity + BlupiAdjust-style
penetration recovery, the more ambitious options each time), then researched real mobile-eggbert
(Decor::TestPath/BlupiAdjust/DecorDetect) before writing any code. Real TestPath is a
Bresenham pixel march (one rect, X+Y resolved together, rewinds to last clear position on the first
hit) with no grounded/airborne branch anywhere — called unconditionally for every mode including
every vehicle, confirming this engine's old !m_onGround bypass in TryMoveAxis was a real,
confirmed bug, not a simplification. Real solidity (DecorDetect) is a 4x4 (16px) per-icon
quarter-cell mask (Tables::table_decor_quart, 7056 entries), not whole-tile.
New BlupiController API: IsPointSolid() (sub-tile precision via the real quarter-cell mask,
extruded uniformly along this engine's own Z axis since the real mask is inherently 2D),
ResolveMove() (one march per call, mirrors real TestPath, reports which axis blocked +
onGround), RecoverFromPenetration() (real BlupiAdjust equivalent, called unconditionally at
the top of Step(), 3 real distinct attempts — down/right/left — since this engine's Blupi has
always been a point, not the real rect BlupiAdjust pushes). GroundHeightAt/CeilingHeightAt/
TryMoveAxis removed entirely. New src/GalaxyEggbert/Game/DecorQuartTable.hpp holds the
transcribed quarter-cell data (mechanically extracted, cross-checked via a second independent
extraction — byte-identical).
Two real problems found and fixed during implementation (not by the research):
- A coordinate-convention bug this refactor itself introduced, then found and fixed: this
engine's Y axis uses a bottom-anchored grid convention (
[g, g+1), confirmed againstGetGroundBlockType()'s ownlround(m_y) - 1) that's DIFFERENT from X/Z's center-anchored one ([g-0.5, g+0.5), matching the terrain renderer's ownCubeMesh) — a real, pre-existing asymmetry already known and worked around elsewhere in this codebase (GalaxyEggbertGame.cpp'skPlaceholderModelYOffsetcomment literally documents this same 0.5-unit collision-vs-render gap). First draft used the wrong (center-anchored) formula for Y, breaking 59 of 311VerifyBlupiMovementchecks; fixed by usingfloor(y)for Y specifically. - A real design tension, resolved via a
checkSubcellparameter: several hazard tiles Blupi's own hazard-detection depends on physically resting on (Lava/Crusher/Saw/Blitz/Drip) have an all-zero real quarter-cell mask (genuinely thin, independently corroborating15-3d-render-mapping-design.md's own "ThinMechanical" findings for the exact same icons) — applying sub-tile precision to ground detection would make Blupi fall through already-verified hazards. Ground/ceiling resolution now explicitly uses the coarse (non-sub-tile) check; horizontal wall collision keeps the fine one — a deliberate, documented scope boundary.
kStepLimit step-up (already a CNA-only invention, not a real TestPath concept) kept as its own
explicit mechanism (FindColumnGroundY), not folded into ResolveMove itself.
Verification: VerifyBlupiMovement 252→311/311 after both fixes above. One pre-existing test's
own expected value corrected (not a regression): the balloon-ceiling-stop boundary moved from ~2.5
to ~3.0 once ground/ceiling used one consistent convention instead of two subtly different ones (the
old -0.5 was itself a minor, previously-unnoticed inconsistency). New dedicated test for the actual
airborne-wall-clip bug — verified it has real teeth (a first draft passed even with the old bug
deliberately reintroduced since the jump landed before reaching the wall; redesigned to contact the
wall within the airborne window, then reconfirmed FAIL-with-bug/PASS-with-fix before reverting the
temporary re-injection). Full regression clean (80/81, pre-existing unrelated failure only);
golden-frame byte-match unchanged; live smoke run clean. See plan.md's INFRA-005 entry for the
full writeup.
feat: INFRA-007 step 3/3 — Voyage/DeathLock moved to the typed event queue, INFRA-007 fully complete (2026-07-21)
Completes the migration (steps 1/2 below): all 17 of InteractionSystem/BlupiController's
original *ThisFrame() flags now go through a typed event queue. Voyage/DeathLock turned out to
carry a much bigger payload than earlier estimated — Voyage has 11 fields (kind, iconId,
isButtonChannel, worldX/Y/Z, fixedX/Y, worldIsStart, isAscend, ascendOffsetY), DeathLock has 2
(kind, shouldRespawn) — the approved tagged-struct shape absorbed this fine, no design change
needed. New wrinkle this step had to solve: unlike every other Kind, at most ONE
VoyageRequested/DeathLockRequested event can exist per frame (the old flat members gave this
"last request wins" behavior for free via plain assignment) — solved with a new private
InteractionSystem::ReplaceEvent(EventKind, Event) that removes any existing entry of that Kind
before appending, used by RequestVoyage()/RequestClear2Ascend() and all 4 death-lock trigger
sites. Two enum class ... : std::uint8_t; forward declarations (VoyageKind, PendingDeathKind)
let Event's new fields be typed by them ahead of their full definitions, which stay exactly where
they've always lived. Consumer side: GalaxyEggbertGame::ResolvePendingVoyage()/
ResolveDeathLock() rewritten against FindInteractionEvent(), reading payload off the returned
pointer instead of ~9 individual getters each. VerifyInteractionSystem.cpp's 2 helper lambdas
(mirroring the real consumer functions) plus ~10 direct call sites rewritten the same way; all
stale doc comments updated; zero references to any old getter/member name remain anywhere in
src/include/tools (confirmed via grep). Full regression clean (80/81, only the pre-existing
unrelated easy-gl-resource-smoke-tests failure); VerifyInteractionSystem: still 547 checks, all
passing (same as step 2 — no coverage lost). Live smoke check (--golden-capture + golden-frame
byte-compare) clean. Honest gap: the real camera-projection consumer path
(ResolvePendingVoyage()'s Hud::ProjectWorldToHudSpace()/BeginVoyage() call) has no dedicated
test coverage (predates this refactor — needs a full GalaxyEggbertGame with a real graphics
device) and wasn't separately live-instrumented this step, since every line touched there is a
mechanical getter-to-pointer-member substitution with no logic change, on an unmodified projection
call chain. See plan.md's INFRA-007 entry for the full writeup.
feat: INFRA-007 step 2/3 — InteractionSystem's 11 remaining flags moved to the typed event queue (2026-07-21)
Continues step 1 (below). Migrated InteractionSystem's 11 flags (everything except
CrateBeingPushedThisFrame — stays a bool, see step 1 — and the Voyage/DeathLock pair, step 3)
to EventKind/Event/EventsThisFrame(). Re-verifying the exact payload shape while implementing
this caught a real correction to step 1's own design summary: 3 of the 11
(PowerGranted/CloudGranted/HideGranted) actually carry a payload — the pickup's world
position, previously 9 separate float members (Power/Cloud/HidePickupX/Y/Z()) — only 8 of the 11
are truly payload-free. The already-approved tagged-struct shape absorbed this without needing a
design change. DiedThisFrame()'s "tested, no live-game-loop consumer" status carried over
unchanged, as decided in step 1. New GalaxyEggbertGame::FindInteractionEvent(EventKind)
private helper (const Event*, nullptr if absent) lets payload-carrying kinds read their pickup
position at the same call site. All 8 scattered consumption sites in GalaxyEggbertGame.cpp
rewritten in place — same order/interleaving, only the storage mechanism changed.
tools/VerifyInteractionSystem.cpp (~64 references — this task's largest test-file impact)
rewritten via two small local helpers (hasEvent/findEvent). Confirmed via grep: zero
references to any of the 11 old getter/member names remain anywhere in src//include//tools/.
Full regression clean (80/81, only the pre-existing unrelated easy-gl-resource-smoke-tests
failure); VerifyInteractionSystem itself: 547 checks, all passing. Live smoke check:
--golden-capture against worlds3d/world999.vwr (exercises InteractionSystem::Update() every
frame) completes cleanly, and all 3 golden reference frames still byte-match exactly — the render
pipeline is untouched. See plan.md's INFRA-007 entry for the full writeup. Step 3
(Voyage/DeathLock, the largest payloads, explicitly saved for last) not started yet.
feat: INFRA-007 step 1/3 — BlupiController's 3 sound-cue flags moved to a typed event queue (2026-07-21)
Design proposed to and approved by the user before any code changed (this task's own
precondition, since it touches already-working signal plumbing). Surveyed all 17 *ThisFrame()
flags first (14 in InteractionSystem, 3 in BlupiController) and found 3 different real
shapes hiding under one name: 14 simple no-payload one-shots, 2 that carry extra payload fields
and can co-occur in the same frame (Voyage/DeathLock), and 1 (CrateBeingPushedThisFrame()) that
isn't a one-shot event at all — it's documented as continuous per-frame state with the caller
doing its own edge detection, so it's explicitly excluded from the queue. Also found
DiedThisFrame() is tested but has no real consumer in the shipped game — decided to carry it
into the new queue unchanged rather than use this transport-only refactor as cover to also drop
it. Approved shape: two independent event queues (one per emitting class, preserving their
existing decoupling), each a tagged struct (not std::variant — no precedent for it in this
codebase). Migration ordered smallest-risk first. Step 1 (this entry):
BlupiController::DownEntrySoundFiredThisFrame()/UpEntrySoundFiredThisFrame()/
DownReleaseSoundFiredThisFrame() replaced by EventKind/Event/EventsThisFrame()
(std::vector<Event>, cleared/refilled every Step()); the 3 if checks in
GalaxyEggbertGame.cpp became one loop + switch; VerifyBlupiMovement.cpp's 10 assertions
rewritten against a small hasEvent() helper, same coverage. Clean 4-file diff, no old
getters/members left behind. Full regression clean (80/81, only the pre-existing unrelated
easy-gl-resource-smoke-tests failure). See plan.md INFRA-007 for the full design writeup.
Steps 2/3 (InteractionSystem's 13 remaining flags, including the much larger
VerifyInteractionSystem.cpp test-file impact) not started yet.
feat: INFRA-004 — mark ~131 unverified 2D→3D render-mapping icons as an explicit, queryable set (2026-07-21)
Fourth item from plan.md §7's task breakdown. Bookkeeping only, no rendering code touched. New
include/GalaxyEggbert/UnverifiedRenderMapping.hpp (header-only, engine-agnostic): a table of every
icon ID whose 3D render-mode recommendation (mobile-eggbert-reference/ 15-3d-render-mapping-design.md §10.2-§10.5, as applied per-icon in 02-tiles.md's Category column)
is still only a first-pass agent visual guess, never confirmed by direct user identification the
way §11's other 34 icons were — 131 icons (generated straight from 02-tiles.md's actual
"(§10.N)"-cited rows, the current applied ground truth, not the design doc's older ~137 prose
estimate; the gap is explained by icons already resolved by direct identification — 61/62/65/67 and
Saw/SawStopped 378/379 — correctly excluded). New tools/VerifyUnverifiedRenderMapping.cpp, wired
into ctest (pure data table, no display needed): asserts the exact count, no duplicate/
out-of-range IDs, and spot-checks IsUnverified() both ways. Verified the test has teeth:
deliberately duplicated an entry, confirmed both the count and duplicate checks failed with precise
messages, reverted, confirmed a clean re-run. Full regression clean (80/81, only the pre-existing
unrelated easy-gl-resource-smoke-tests failure).
Third item from plan.md §7's task breakdown. New tools/VerifyGetObjIcon.cpp (headless,
engine-agnostic, registered with ctest — unlike INFRA-001/002 this one needs no display, so
it runs in the normal suite): 232 checks covering every real ObjectType case in
ObjectIcons::GetObjIcon()'s switch, not just the 34 with a historical Fixed 202... note —
exact per-tick sequence for table-driven types, exact formula for ascending/descending/
triangle-wave types, fixed icon for static types, transcribed directly from the production code's
own already-committed values. Cross-checked frame-array length against
mobile-eggbert-reference/08-animations.md §3.1/§3.2/§4 wherever documented, surfacing one real
discrepancy: the doc's own ObjectType25 row still says "8 frames" but flags itself as stale in
its own text (real table_shield has 16 — matches the current, already-fixed code); the test
asserts 16. Verified the test actually catches regressions, not just trivially passing:
deliberately mutated a divisor in ObjectIcons.cpp, confirmed a precise FAIL message, reverted
(clean git diff). Full regression clean (79/80 counting the new test, only the pre-existing
unrelated easy-gl-resource-smoke-tests failure).
Second item from plan.md §7's task breakdown, straight after INFRA-001. tests/golden/ golden_frame_0060/0120/0180.png are the approved reference frames (captured the same
--golden-capture way INFRA-001 proved reproducible). New tools/verify_golden_frames.sh <build-dir>: runs the capture, then exact-byte-compares (cmp) each fresh frame against its
reference, printing PASS/FAIL per frame and an ALL CHECKS PASSED/SOME CHECKS FAILED
summary with a matching exit code — same convention as the VerifyXxx C++ tools, just a shell
script since this one launches a subprocess and diffs files rather than exercising game logic
directly. Verified both directions live: passes against the real references, and correctly reports
FAIL against a deliberately corrupted reference copy (restored afterward, confirmed via git status showing no unintended changes). Not wired into the default ctest run, same reason as
INFRA-001 (needs a real display/GL context) — see §7's own new command.
First implemented item from plan.md §7's correctness-infrastructure task breakdown. A real,
committed --golden-capture CLI flag (main.cpp, parsed and passed to a new
GalaxyEggbertGame::EnableGoldenCaptureMode()) — not another throwaway env-var hack. Skips to
Play, loads the fixed worlds3d/world999.vwr demo world, lets the game's existing 60Hz fixed
timestep run, writes golden_frame_0060/0120/0180.png at 3 fixed tick indices, then self-terminates
via Exit() (confirmed: process exits 0 on its own, not killed by an external timeout).
Reproducibility verified empirically, not assumed: 2 independent runs under xvfb-run produced
byte-identical PNGs (md5sum match on all 3 files) — the actual bar INFRA-001 set for itself
("proves the capture itself is stable/reproducible"). Deliberately not wired into the default
ctest (needs a real display/GL context, same reason the existing live-headless-check step has
always been separate from ctest in this project) — documented as its own command in §7 above.
INFRA-002 (golden-image diffing on top of this) is the natural next step, not done yet. Full
regression clean (78/79, only the pre-existing unrelated easy-gl-resource-smoke-tests failure).
docs: merge REMAKE-ANALYSIS.md/renderers.md from a separate branch; record vision in plan.md §7 (2026-07-20)
User asked to merge origin/claude/galaxy-eggbert-3d-remake-n0ga3y into develop (a separate
session's exploratory work) and review the result. Merged cleanly (purely additive — 3 new docs
plus one new, currently-unwired header, no conflicts with this session's own Saw/wasp work):
REMAKE-ANALYSIS.md (root-cause analysis of the recurring bug pattern — correctness is verified by
a human looking at a screenshot, not automated, plus 6 other root causes), renderers.md +
renderers-next-steps.md (a dual-renderer architecture proposal, path A = same CNA/Easy3D base),
and src/GalaxyEggbert/Game/SceneFrame.hpp (the first data-contract artifact of that proposal
— unwired, nothing includes it yet).
Independently spot-checked the analysis' headline numbers against this codebase before writing
anything up (34 Fixed notes in ObjectIcons.cpp, Update()/Draw() line counts) — all
confirmed accurate, not exaggerated. Recorded the assessment as plan.md §7 ("Correctness
Infrastructure & Dual-Renderer — Vision"): the diagnosis is accurate (the same-day Saw-blade saga
below is a live instance of it), a small P0 slice (permanent golden-screenshot harness +
GetObjIcon() data-integrity checks) is cheap and worth doing when there's appetite, but the larger
items (a shared collision resolver, an ObjectType handler-table refactor) and the separate
dual-renderer direction change each need their own scoped task and explicit user sign-off — not to
be bundled into one "modernization" effort. Written as direction, not a task queue. The foreign
branch was deleted from the remote after merging (explicit user request).
Follow-up to the wasp-patrol fix below, same session, user-requested: the object-exhibition grid's
bird (ObjectType20) and wasp (ObjectType44) specimens — built via a loop that places every
confirmed ObjectType as a static catalog piece (posEnd==posStart by explicit design, "nothing
patrols") — now get a small real ±1-unit patrol range instead, so their real per-direction/
turn-transition icon animation (GetBirdIcon()/GetWaspIcon(), both patrolStep/patrolTime
driven) actually plays. Kept small enough to stay inside each specimen's own 3-unit-spaced grid
cell. Verified live (Xvfb): both now show patrolStep/patrolTime advancing exactly like the
already-fixed gameplay wasps. The rest of the ~40-type exhibition grid is unchanged (still static
by design).
User report: "wasp and bird are frozen." Root cause found: both ObjectType44 (wasp) placements in
the demo world (tools/GenerateSampleWorld3D.cpp) were built via the place() helper, which always
sets posStart==posEnd. That's a real, faithful no-op for InteractionSystem's shared
patrol-turn mechanic (AdvancePatrolStep()'s own "no-op if posStart==posEnd" guard, already
verified against the real source's own equivalent guard in an earlier session) — but the wasp is a
confirmed real "patrol walker enemy" (mobile-eggbert-reference/03-objects.md) that's never
stationary in mobile-eggbert, so the zero-range placement also froze its per-direction/
turn-transition icon animation (GetWaspIcon() reads patrolStep/patrolTime, both permanently
stuck at their initial values without a real patrol tick), not just its movement. Same bug already
found and fixed for the platform lift (2026-07-10) — both wasps now built directly via
PlaceMoveObject with a real posStart != posEnd, matching that precedent, instead of place().
Verified live (Xvfb): both wasps now show patrolStep advancing 1→2→3→4 and position interpolating
smoothly over their real patrol range. (No bird is placed anywhere via a named ObjectType20 token
in this generator — the "bird" half of the report turned out to be the object-exhibition grid's own
dynamically-typed specimen, see the follow-up entry above.)
fix: Saw/SawStopped (icon 378/379) render orientation — 6-round live-feedback saga, resolved (2026-07-20)
The single most-argued-over item in the whole session. Starting state: 2 prior rounds (2026-07-11)
had only ever adjusted the VERTICAL position of a vertical InnerFlatPlate (bottom-anchored, then
top-anchored-hanging-down); this session picked it up again after a live screenshot showed it as a
thin diagonal wedge jammed into a corridor's side wall.
Round-by-round (each against a fresh live Xvfb screenshot, most reverted before the next):
- Read at the time as "should lie FLAT on the floor" (
PlateAxis::Y) — implemented, but as an EXCLUSIVEInnerFlatPlate(hollow block, no solid cube), which read as a hole/pit in the floor. - Fixed additively instead (same idiom as the existing grass-top overlay): a normal, fully opaque
RockPile-textured floor cube (NOT the confusingly-named
Ground/icon 10, actually a machine-piece graphic perBlockTypes.hpp's own NOTE) PLUS the blade as a horizontal overlay plate. This is when the icon's real, previously-unnoticed transparency was found: the top half of every 378-383 animation frame is fully transparent (confirmed via direct alpha-channel inspection), blade only in the bottom half. - User's final word, against this round's own screenshot: the blade should be upright after
all (round 1's diagonal-wedge problem was the vertical PLATE'S position/hole, not the vertical
axis itself) — reverted back to vertical, rooted at the floor's walkable surface, extending
upward, still additive on the round-2 solid floor cube. This is the confirmed-stable
orientation committed as
d4cc7f5. - A further live-only investigation chased an exact-180-degree in-plane texture rotation, a
precise centering position, and a size increase, each shown live and each reverted at the user's
explicit instruction — none of that survived to the commit. Two genuine, reusable findings from
that investigation, kept only as code comments (not applied): (a) rotating a
PlateItem's UV by 180° viaU0<->U1/V0<->V1swap or negative Width/Height both silently vanish the whole plate with THIS icon specifically — isolated to atlas mip-level bleeding from its mostly-transparent tile (confirmed via a decisive test: the identical rotation renders fine on an opaque tile, and fine on this same tile cropped to just its own opaque bounding box); (b) the wasp/bird patrol fix above was actually found as a side effect of this same live-testing session.
Also moved Saw/SawStopped out of InnerFlatPlateTiles's shared icon list into their own
IsGroundAnchoredPlateIcon() handling directly in TerrainRenderer.cpp's RebuildAnimatedRenderer()
(not the constructor's main Build() loop, since both icons are always animated and routed there
first) — the only 2 icons needing the additive solid-cube-plus-overlay treatment. Committed as
d4cc7f5. Full regression clean throughout (78/79, only the pre-existing unrelated
easy-gl-resource-smoke-tests failure). See TerrainRenderer.cpp's own
IsGroundAnchoredPlateIcon comment block for the complete citation trail if this needs revisiting.
feat: wired JumpSkate/AirSkate, TakeSkate/DeposeSkate, FireTank anim icons; fixed a Down frame-count bug (2026-07-19)
Follow-up to the animation-icon-sheet fix below, same session, done autonomously overnight per the
user's explicit request to continue implementing everything possible from
mobile-eggbert-reference/08-animations.md §8's "not wired" list. Extracted and cross-validated
the real table_blupi frame arrays (a from-scratch parser, validated byte-for-byte against 37
already-approved arrays before trusting any new one) for the 5 items research forks confirmed were
genuinely wireable (see plan.md BLUPI-053/054/055/058 and their BLUPI-047 follow-up note):
- JumpSkate/AirSkate (
table_blupi40/41) — Skateboard is the only vehicle mode with its own real airborne icon pair;vehicleAnimState()'sSkateboardcase now splits bym_velocityYsign, same as the baseJump/Airsplit. - TakeSkate/DeposeSkate (42/43) — wired via
TriggerOneShotAnim()at the real Skateboard mount/dismount hook points inGalaxyEggbertGame.cpp(confirmed the only vehicle with a dedicated mount/dismount pose). - FireTank (53) — new
InteractionSystem::TankFiredThisFrame()per-frame signal (mirrorsCrateBeingPushedThisFrame(), since that class has noBlupiControlleraccess), true only the frame a bullet actually launches, not the empty-clip click. - Bonus fix:
kDownFrameswas{33}(1 frame) but the real table (and the reference doc) both say Down(6) has 3 frames (33,34,35) — found while cross-validating the parser, fixed independently.
Also re-confirmed (via 2 research forks, not implemented — deliberately deferred/dead):
Turn variants' real trigger condition (Decor::BlupiStep()'s m_blupiSpeedX-sign-vs-m_blupiDir
mismatch, a genuine edge-detection problem) documented for a future pass; Clear5-Clear8 reconfirmed
as real dead code (Decor::BlupiDead() never assigns them). See
mobile-eggbert-reference/08-animations.md §8 for the updated full status (46/84 wired).
New test coverage: VerifyBlupiMovement (Down cycling, JumpSkate/AirSkate state+icon+apex/landing,
TakeSkate/DeposeSkate one-shot trigger+resume, FireTank one-shot trigger+resume) and
VerifyInteractionSystem (TankFiredThisFrame() true/false across the not-in-tank/cooldown/
empty-clip cases). Full regression clean (only the pre-existing unrelated
easy-gl-resource-smoke-tests failure), built/tested at -j2 throughout per the user's standing
CPU-core-limit instruction.
User request: "make sure the bottom-right animation icon actually corresponds to what should be
animated" (the interim debug indicator, Hud.hpp's own class comment, not part of the real
mobile-eggbert HUD). Cross-checked against mobile-eggbert-reference/08-animations.md §2's own
"Channel selection" note (verified there directly against Decor.cpp's real BlupiSearchIcon()):
only Clear1/Clear2/Clear3/Glu/Electro use element.png, every other real BlupiAction
uses blupi.png. Hud's indicator always sampled blupi.png regardless — since Clear1/
Clear2/Clear3/Glu are 4 of this engine's real, already-wired DeathLocked causes, all 4 were
showing whatever unrelated blupi.png pixels happened to sit at that numeric icon index (Electro
has no modeled mechanic here, so it never came up). Fixed with new
BlupiController::AnimIconUsesElementSheet() (true only for those 4 causes) threaded through a
new Hud::Draw() parameter, selecting the already-loaded element.png batch instead of
blupi.png for just this one HUD element when it applies — Hud already draws other element.png
icons elsewhere (keys/bullets/dynamite/Voyage), so no new asset load or render path was needed.
8 new VerifyBlupiMovement assertions (a Stop-state baseline plus the sheet flag for all 6
DeathCause values, including 2 that had no dedicated test before, Clear3/Glu). Live-verified
under xvfb-run with temporary instrumentation (forced a Clear1 death-lock, screenshot, fully
reverted before commit): the icon now shows a coherent Blupi silhouette instead of a mismatched
crop. Full regression clean (only the pre-existing unrelated easy-gl-resource-smoke-tests
failure). See plan.md 064's own entry for the full writeup.
Follow-up to the per-type-icons redesign below, same session: the user clarified (with the same
freeeggbert_editor.jpg reference) that the layout also needed to match free-eggbert, not just
the icon style — one vertical column of solid-green buttons at the left edge, no separate grid
elsewhere on screen, plus a real toolbar button for box-fill ("beyond free-eggbert scope").
EditorPalette was rewritten from left-toolbar-plus-right-8x4-grid to a single left column:
- 8 fixed action buttons, paired 2-per-row (
ToolbarButtonRect(index):index/2= row,index%2= left/right half) — Undo/Redo, Save/Back, Play-Test/mode-toggle, BoxFill/Confirmed-All-tab — then a paging row, then content cells one per row. Pairing was necessary, not cosmetic: 9 full-width rows (the original one-row-per-action draft) need 514px of vertical space alone, more than this engine's actual default 800x480 window has room for at all; 5 paired rows leaves real space for content. PageCount()/a newItemsPerPage()now take the realviewportHeightand compute how many content rows fit below the fixed header, replacing the old compile-timekIconsPerPage=32constant — a single column has much less room per page than an 8x4 grid did.- New
ToolbarAction::BoxFill, wired intoWorldEditor::Update()'s existing box-fill state machine via a simple||alongside the F-key condition — clicking the button behaves exactly like pressing F (first click marks corner A, second click fills), no new state needed. - Solid green backing (not translucent) for every button, matching the reference's opaque style;
the mode-toggle and Confirmed/All-tab buttons brighten to a lighter green when "on" (position
alone can't convey that state); the selected content icon gets a gold highlight instead of the
old enlarged-white-square treatment, so it stands out clearly against the now-green background.
5 new
VerifyGEWorldEditorassertions (the BoxFill button drives the identical fill/undo behavior the F key already had, verified against the same expected-region math). All existing palette-click tests' hardcoded pixel coordinates were recomputed for the new geometry (the old ones silently clicked the wrong control after the layout changed, until a full rebuild+rerun caught it as a wall of new failures — fixed by walking every hardcoded coordinate in the test file, not by guessing). Live-verified underxvfb-runwith temporary env-var-gated instrumentation (fully reverted before commit): both Blocks and Objects mode screenshots confirmed the single green column, real icons, and the mode-toggle's brighter-green "on" state.
User-requested redesign (unprompted user feedback, screenshot-driven, referencing free-eggbert's
own editor palette as the target look): the Objects-mode palette previously drew a flat
category-colored square per cell (EDITOR-106/109's own documented simplification, judged
"disproportionate" to build the real multi-atlas icon plumbing at the time). Now every cell draws
the real per-type sprite via ObjectIcons::GetObjIcon() plus its atlas-selection predicates
(IsUniformCubeObject/IsObjectMPngSourced/IsExploPngSourced/IsBlupiPngSourcedAtPhase/
UsesBlupi1Texture) — the SAME lookup already used to render real MoveObject billboards during
gameplay, so no new icon data or mapping was invented. Icons are batched per atlas (object-m.png/
element.png/explo.png/blupi.png/blupi1.png) so a page still costs at most 5 draw calls, same shape
as the pre-existing single-atlas Blocks-mode batch. EditorPalette::Draw() now takes 4 more
Texture2D& parameters (the game's own already-loaded objectTexture_/exploTexture_/
blupiObjectTexture_/blupi1ObjectTexture_, no new asset loads) threaded through
WorldEditor::Draw() from GalaxyEggbertGame; CMakeLists.txt's VerifyGEWorldEditor target
gained ObjectIcons.cpp (a link-time dependency, engine-agnostic, same precedent as
GenerateSampleWorld3D already linking it). EditorPalette::CategoryColor() and its
objectCategoryIndex_ map were removed (no longer used, not referenced by any test).
Every Objects-mode cell also gained a translucent white backing square (same technique/color as the toolbar buttons, which the user explicitly asked to keep) so a real sprite's transparent margins still read clearly against the animated 3D scene behind the palette — this was the SAME flat-quad mechanism already used for the toolbar/selection-highlight, just now applied to every cell in Objects mode (Blocks mode is untouched, its icons are already opaque). Superseded within the same session by the layout entry above: the backing is now solid green (matching the reference's opaque button style, not translucent), applied uniformly to Blocks mode too, once the user clarified the reference meant the whole layout, not just per-cell icons.
No VerifyGEWorldEditor test changes were needed (Draw() is not exercised by that suite — it's
the one method needing a real GraphicsDevice, per this class's own established convention).
Live-verified under xvfb-run with temporary env-var-gated (GE_EDITOR_AUTOTEST_PALETTE)
instrumentation (jump straight to a fresh Editor world, synthetic click on the mode-toggle button,
screenshot, fully reverted before commit): confirmed real, distinct per-type icons (platform lift,
several enemies, explosions, a bee, key, egg, lollipop, balloons, a bomb, etc.) each on a legible
translucent backing, replacing the flat-colored placeholder.
Adds a select-and-edit tool for already-placed MoveObjectRecords, keyboard-only (no new toolbar
button, matching the box-fill tool's own keyboard-only precedent):
Gselects whichever placed object's billboard projects nearest screen center — nearest-billboard picking reusesHud::ProjectWorldToHudSpace()(no new projection code was written), so this section linkssrc/GalaxyEggbert/Game/Hud.cppintoVerifyGEWorldEditorfor the first time (CMakeLists.txt). Shown with a new magenta selection-highlight cube —EditorHighlightRenderergained a second public method,ShowSelectedObject(), andWorldEditorowns a second instance of that class so the selection highlight can stay visible alongside the existing aim-crosshair/ box-fill one (each instance only tracks one set of bounds).Tsets the selected object'sposEndto the current raycast aim cell, giving it a real patrol path (posEnd != posStart).Tabcycles which of the object's 5 numeric fields (speed, then the 4 real patrol-timing fields)OemPlus/OemMinusnudge by a fixed editor-UX step (not a transcribed real constant).Deleteremoves the selected object entirely.
Each action is a real EditCommand::Kind::MoveObjectEdit pushed onto the existing undo stack
(U/R already undo/redo these correctly — no undo/redo code changes were needed, confirming the
EDITOR-109-era design note that anticipated this). New WorldEditor::RefreshSelectedObjectAfterHistoryChange()
re-syncs the cached selectedObject_ from the world after every Undo/Redo, so a later edit's "before"
state can't go stale relative to what Undo/Redo just wrote (a real bug caught and fixed during this
session's own testing, before it ever reached the live build — see below).
A real test bug found and fixed while writing VerifyGEWorldEditor's new section: the first
draft pressed the same edge-triggered key (U) four times in a row across separate Update() calls
expecting each to undo one more step — but WorldEditor::Update()'s edge-triggering (matching
every other tool key in this class) only fires on a press following a release, so only the FIRST
of the four consecutive presses actually did anything; the other three were silent no-ops. Fixed by
adding a pressKey() test helper that presses then releases within two Update() calls, same
requirement a real keyboard has. Not a bug in WorldEditor itself — an easy trap to fall into
when scripting multiple presses of the same key in a test, worth remembering for any future section
that does this.
24 new VerifyGEWorldEditor assertions (189 total): place → G-select → T (patrol path) → OemPlus/
OemMinus (speed, then Tab + stepAdvanceTicks) → 4×undo → 4×redo → Delete → undo-the-delete, all
checked against CollectMoveObjects, plus a no-op sanity check (G with no objects anywhere in the
world selects nothing, and a Delete right after is a safe no-op).
Live-verified end-to-end under xvfb-run with temporary env-var-gated (GE_EDITOR_AUTOTEST_110)
instrumentation (fully reverted before commit, confirmed via git diff): a scripted place → G-select
→ fly → T → Play-Test sequence, driven through synthetic KeyboardState/MouseState fed into the
SAME real WorldEditor::Update() call site real player input uses. Confirmed the play-tested
world's MoveObjectRecord carried the real edited patrol path (posStart=(50,4,33) posEnd=(50,3,17)), and — since the two screenshots taken during play-test looked visually
near-identical at that camera angle/distance — added a temporary numeric trace of the live
MobileObjSpec::current{X,Y,Z} during Play phase to settle it conclusively: currentZ advanced
continuously frame-by-frame from render-space -17 toward -33 (raw grid 33→17), proving the lift
genuinely patrols at runtime; the screenshots just weren't a legible way to see a mostly-depthwise
motion at that angle. No code bugs found in the live pass — instrumentation fully reverted afterward.
New tree src/GalaxyEggbert/Editor/:
WorldEditor.hpp/.cpp— owns camera/tool state/undo stack, drives everything else.VoxelRaycast.hpp/.cpp— Amanatides–Woo DDA againstWorlds::World.BoxRegion.hpp/.cpp— corner pair → normalized, world-clamped box.EditCommandStack.hpp/.cpp— taggedEditCommandundo/redo stack.PaletteCategories.hpp/.cpp— curated + exhaustive numeric palette data (blocks and objects).EditorPalette.hpp/.cpp— toolbar + paged icon grid, Blocks/Objects modes.EditorHighlightRenderer.hpp/.cpp— cell/box selection overlay.EditorBrowserScreen.hpp/.cpp— per-gamer world list/create/open/delete UI.CustomWorldStorage.hpp/.cpp—customworlds/gamer<N>/custom_NNN.vwrpath helpers.
Also added: src/GalaxyEggbert/Game/QuadBatch.hpp/.cpp, extracted from InputPad so the
editor UI reuses the exact same 2D quad primitives (InputPad's public API unchanged;
VerifyGEInputPad passes unmodified as the regression guard), and tools/VerifyGEWorldEditor.cpp
(165 assertions).
Modified: GamePhase.hpp (new Editor value); Worlds/World.hpp/.cpp
(removeBlockExtraMetadata); MoveObjectRecord.hpp/.cpp (RemoveMoveObject);
WorldRuntime.hpp/.cpp (new public ResyncFromWorld(), refactored out of LoadFromVwrFile()'s
tail); InputPad.hpp/.cpp (Init-screen Editor button + QuadBatch extraction);
GalaxyEggbertGame.hpp/.cpp (phase dispatch, worldEditor_, LoadCustomWorldForEditing/
ForPlayTest, play-test routing, ResyncFromWorld() call in RebuildWorldPresentation());
CMakeLists.txt; tests/GalaxyEggbert/MoveObjectRecordTests.cpp (+2 gtest cases, 66 total).
Bugs fixed: the empty-world TerrainRenderer crash and the palette press/release click bug —
both described in §2's "Recently implemented".
Verification method used throughout, worth reusing: pure logic (raycast math, box math, undo/redo,
palette hit-testing, storage, placement) goes into VerifyGEWorldEditor with synthetic
KeyboardState/MouseState values and no GraphicsDevice; anything visual is checked live with
temporary env-var-gated (GE_EDITOR_AUTOTEST_*) instrumentation under Xvfb with screenshot capture,
always fully reverted before commit (confirm with git diff — a whole-file git checkout is
risky here, it can also revert real uncommitted work; prefer targeted edits).
-
fix: Balloon's last 2 documented gaps closed — horizontal drift + ceiling stop (plan.md
E3D-MIG-135). User asked to close both limitations left after the rise fix below. (1) Horizontal drift: the SAME realDecor.cpp:4059-4106block gives Blupi momentum-based left/right drift while ballooned (ramps toward a real terminal speed, decelerates to exactly 0 on release) instead of ordinary walking speed — ported asm_balloonHorizontalSpeed(3.125 units/s terminal).TriggerBalloon()now also force-exits any mounted vehicle (realByeByeHelico()), required so this takes priority immediately. Real wall-stop while drifting NOT ported — this engine has no horizontal-wall-collision for ANY airborne movement today, a shared limitation beyond scope here. (2) Ceiling stop: corrected an earlier WRONG claim that the rise clipped through ceilings like Helicopter/Overcraft — real source's general swept collision (Decor::TestPath()) stops it same as walking into a wall. NewCeilingHeightAt()implements this. Found and fixed a real integration bug while verifying: the pre-existingGroundHeightAt()misidentified the same ceiling block as ground to land ON TOP of once a rising Blupi got close enough — confirmed live via a standalone debug harness (Blupi teleported from y≈2.0 straight to y≈4.0, well before the real 2.5 contact height). Fixed by suppressing the ground-check for every frame a ceiling is within reach, not just the frame the clamp fires. Re-verified: Blupi now rises smoothly and holds exactly at the real ceiling height indefinitely. 4 new test assertions, full regression clean. -
fix: ballooned Blupi now actually RISES — the real behaviour, found at last (plan.md
E3D-MIG-135). Reported ~15 times. Every previous attempt asked only "does gravity still pull him down?" and stopped there — first modelling a 20%-gravity slow fall, then a fixed-height freeze, then papering over the invisibility with an invented cosmetic bob. All wrong. Gravity is not merely switched off: Blupi actively drifts upward for the full 10s. The real code is a dedicated block atDecor.cpp:4039-4058that no earlier pass ever opened — they reasoned only from the!m_blupiBalloon-gated fall trigger atDecor.cpp:2823. It accelerates him upward everyScaleTime(6)ticks to a-3.0px/tick terminal rise,-5.0with Jump/Up held, or decelerating to a hover with Down held (never a descent). Ported with real unit conversion (px/tick @20fps, 64px/block → 0.9375 / 1.5625 units/s, 1.0417 units/s²) and the invented bob deleted. Verified three ways: 6 new assertions pinning each real terminal speed to within 0.02 units/s, a live realistic-input run measuring exactly 0.9375 units/s of climb, and third-person screenshots showing the level drop away beneath him. Documented limitations: the real floaty horizontal drift isn't modelled, and the rise isn't ceiling-clamped (matching the existing Helicopter/Overcraft modes). -
fix: Balloon float effect was too subtle to notice during real gameplay (plan.md
E3D-MIG-135). User re-reported "still no change" after directly confirming (via targeted follow-up questions) they'd done a genuinely fresh CLion rebuild and watched the wasp sting in third-person view. Time-spaced screenshots (correcting two methodology gaps from the first verification pass: consecutive-frame shots can't show sine motion, and chase-camera damping was confounding an early retry) proved the original +/-0.08-unit bob WAS rendering exactly per the math — just too subtle at normal camera distance to read as "floating" rather than noise, since half the cycle dipped below normal standing height. Redesigned as a sustained upward hover (~0.12-0.32 units above normal stance the entire time, never dropping back down) with a gentle bob on top — confirmed via fresh screenshots to be an unmistakable, continuous gap between Blupi's feet and the ground. Physics untouched (still a real, verified zero-gravity freeze). -
feat: trivially-reachable wasp for manual Balloon testing + conclusive realistic-input verification (plan.md
E3D-MIG-135). User reported "wasp sting still doesn't make Blupi float, no change at all" for the ~7th time despite two independent prior fixes (the physics freeze and the visual bob) both already re-verified live and correct. Root suspicion: the ONLY existing wasp sits on a north-hill plateau requiring a staircase + terraced-ascent platforming section to reach — real friction for manual testing that may explain why the fix was never actually being exercised. Added a second wasp directly on the flat spawn corridor ofworlds3d/world999.vwr(5 tiles from spawn, zero platforming), and — critically — re-verified the ENTIRE chain using fully realistic simulated player input (held-forwardmoveInput, driven through the realUpdate()pipeline) instead of a position teleport like every earlier check this session: contact triggersIsBallooned()naturally at frame 46 of ordinary walking, with height correctly frozen while horizontal movement continues. This is the most rigorous verification of this mechanic yet — reachable viaworlds3d/world001.vwr's existingDemoPortala few tiles from the global hub's own spawn point. -
fix: main-menu pillarbox color + panel transparency (plan.md
MENU-006/HUD-004). Two user-reported main-menu visual bugs, both real. (1) The pillarbox margins on the sides of the Init/Wait screens were the generic default XNA/MonoGame "CornflowerBlue" template clear color, never customized to match the actual dark-navy menu background art — fixed by samplingContent/backgrounds/init.png's own corner pixel (RGB(0,35,98)) and using that exact color fordevice.Clear(), eliminating the visible seam. (2) Every button/panel on the main menu (gamer slots, Play, Settings gear) rendered as fully opaque white instead of translucent — root cause: bothHud'skPanelOpacityandInputPad::DrawInit()'s equivalent had been forced to 1.0 since 2026-07-10 specifically to work around a genuine CNA/Vulkan bug (BasicEffectwith Alpha<1 doesn't render at all under Vulkan) -- since Vulkan was the default backend at the time. Restored both to the real 0.6 value now that EasyGL (which renders this correctly) is the default. Building with-DCNA_GRAPHICS_BACKEND=VULKANwill still lose these panels -- that CNA bug itself remains unfixed, out of scope for today. Also investigated a third reported issue (a "different shade" outline around the SPEEDY BLUPI logo/Blupi image, suspectedcnabug) and confirmed via direct pixel math it's the correct, expected alpha-blend result of the real asset's own soft edge over the (now-corrected) dark background -- not a bug. Full regression clean. -
fix: Balloon visual legibility (no visible change on flat ground) + default graphics backend switched to EasyGL (plan.md
E3D-MIG-069/135). User re-reported "Blupi still doesn't float when the wasp stings him" after the physics fix below, ~5th time reporting it — frustrated, rightly so. Rigorous live re-investigation (temporary debug instrumentation: teleport Blupi onto the real wasp inworlds3d/world999.vwr, log every frame, then force him over a genuinely floorless column while already ballooned) proved the physics are 100% correct —GetY()holds EXACTLY constant for 500+ frames over open air. The real gap: the placeholder model's Balloon animation state maps to the SAME idle pose as normal standing, so on flat ground (where the wasp is placed) "frozen height" looks identical to just standing there — no visible cue at all. Fixed with a small cosmetic vertical sine bob applied only to the 3D model's render position while ballooned (does not touch physics). Also switchedCMakeLists.txt's defaultCNA_GRAPHICS_BACKENDfromVULKANback toEASYGL(user request, given the Vulkan shader bug found the same day) — still fully overridable, other backends remain buildable.cmake-build-debugexplicitly reconfigured to pick up the new default (an existing cache doesn't change on its own). Full regression clean. -
fix: Vulkan-only SkinnedEffect Y-flip bug in
../cna(sibling repo) — the placeholder Fox was STILL floating on the user's real desktop builds (plan.mdE3D-MIG-069). User retested the fix below with a live screenshot showing the Fox still floating, well beyond the earlier 0.5-unit fix's scope. Found the user's actual desktop builds (cmake-build-debug,build-cna-vulkan) use the Vulkan backend, never tested in the earlier fix (only EasyGL). Side-by-side screenshots of the identical committed code, differing only in backend, proved a second, separate, Vulkan- only bug: EasyGL renders the Fox correctly grounded, Vulkan renders it hovering high in the sky. Root cause is in../cna(not galaxy-eggbert): every OTHER Vulkan 3D vertex shader applies a manualpos.y = -pos.yto compensate for Vulkan's inverted NDC Y axis vs. OpenGL, but all 4SkinnedEffectVulkan shaders (used byAvatarRenderer, i.e. the third-person model) were missing this exact line — no Vulkan golden-image test exists forSkinnedEffectincna's own suite (only EasyGL has golden PNGs), so this was a real, previously-undetected upstream gap, not something wrong in galaxy-eggbert's own code. Fixed by adding the missing line to all 4 shaders and recompiling viacna's owncompile_shaders.py; re-verified via screenshot that Vulkan now matches EasyGL. This fix currently sits uncommitted in../cna— a genuine upstream engine fix with no possible galaxy-eggbert-side workaround, needs the user's own call on committing it there. All 3 native CNA build dirs (build-cna/cmake-build-debug/build-cna-vulkan) rebuilt and reconfirmed working. Full galaxy-eggbert regression clean (only the 1 known pre-existing failure). Update (2026-07-21,INFRA-009): confirmed committed in../cna— see §2's new "Sibling-repo commit pins" subsection. -
fix: placeholder Fox model floating + wrong scale, real zero-gravity Balloon freeze (plan.md
E3D-MIG-069/135). Three user-reported bugs. (1) The third-person placeholder Fox model visibly hovered above the terrain. Root cause independently confirmed via 3 cross-checked sources (TerrainRenderer.cpp's block-center formula,easy-3d/CubeMesh.cpp's half-extent formula, and the existing first-personkEyeHeightusage):BlupiController::GetY()sits a constant 0.5 world units above the terrain's own rendered surface (an existing, previously- invisible convention — nothing rendered a body against the terrain to reveal it before now). Fixed with a render-onlykPlaceholderModelYOffset = -0.5flocal to this specific placeholder mesh's translation, deliberately NOT by changingGetY()/GroundHeightAt()itself (would ripple into jump physics,kFallDeathY, teleporter/water thresholds, and every already-tuned camera constant assuming the existing convention). (2) The model was ~160% of block height instead of the user-reported real 71.875%. The Fox mesh's local Y-span was measured directly fromfox1.verts.bin(~79.03 units tall) and rescaled against that 71.875% figure (bracketed by realDecor::BlupiRect()'s default collision box,Decor.cpp:2519-2520), givingkPlaceholderModelScale ≈ 0.009095(was an unvalidated0.02). (3) Wasp-sting "Balloon" status used a 20%-reduced-gravity slow fall, an approximation its own comment admitted was unverified. Direct re-check ofDecor.cpp:2823/5834-5849confirms real Blupi genuinely freezes at a fixed height (the real fall-trigger check is itself gated on!m_blupiBalloon) — fixed by skipping gravity integration entirely while ballooned;kBalloonGravityMultiplierremoved as dead code. All three fixes live-verified via temporary env-var-gated debug instrumentation (forced third-person camera, menu-skip, and a delayed screenshot capture showing the model correctly grounded/scaled before-vs-after) and a tightenedVerifyBlupiMovement.cppassertion (ballooned height provably unchanged over time, not just "slower"), all reverted/kept minimal before commit. Full regression clean (78/78 minus the 1 known pre-existing unrelated failure). -
feat: real Mockery (Blupi taunts nearby enemies) + real Stop idle-fidget cycle (plan.md
BLUPI-067/BLUPI-023). User asked two pointed questions: does Blupi stick his tongue out at a nearby enemy, and does he get bored and tap his foot after standing still a while? Both verified directly againstDecor.cppand both were real, unmodeled gaps — plus one documentation error found and fixed (an earlier note this session had Mockery's direction backwards: it's Blupi taunting a nearby enemy, not the reverse). PortedDecor::MockeryDetect()(Decor.cpp:9518-9601) as a new proximity scan inGalaxyEggbertGame.cpp(a bounding-box check, NOT contact/collision, against 11 real enemy ObjectTypes already placed in this engine), gated on Blupi being idle and a newBlupiController::TriggerMockery()/15s-cooldown mechanism — deliberately NOT a freeze (real source never drops focus for this, so movement cancels it immediately, no explicit cancel needed).ObjectType54always gets the distinct Mockeryp variant; every other qualifying type picks Mockery/Mockeryi by facing direction, withObjectType2's own real asymmetry (never the "ahead" variant) ported as-is. Also replacedkStopFrames[]={0}(a Simple3D-era single-frame placeholder) with the REAL 330-frame Stop table — confirmed this isn't a separate "boredom timer", just a long, naturally-cycling idle animation with fidget frames baked directly into it. Live-verified via a temporary debug harness (reverted before commit): standing near world999.vwr's own wasp correctly triggersMockerywith the exact real icon cycle, and standing still long enough genuinely reaches the real "twitch" frame. Full regression clean. -
feat: extract mobile-eggbert's full real Blupi animation table, wire ~24 new states into the HUD icon (plan.md
BLUPI-036/037/038/040/041/043/044/047/051/053/058/069/073/076). User- reported: the HUD animation icon only shows a limited set of poses, unlike real mobile-eggbert. Wrote a small parser mirroringDecor.cpp:2393-2400's own{actionId, frameCount, holdFrame, icon0..iconN}record-scan exactly, ran it against the REALTables::table_blupi[2911]literal (not hand-transcribed), cross-validated by reproducing the already-approvedkChargeFrames/kTeleportingFramesbyte-for-byte before trusting it for anything new — recovered all 84 namedBlupiActionrecords that actually exist (3 of the real 87 — Set/Recedeq/Advanceq — have no table_blupi record at all, genuinely dead in the original game too). Wired real Stop/March icon pairs for all 5 vehicle modes (Helicopter/Jeep/Tank/Skateboard/Overcraft), Swim/Surf, Hide, Push (crate-pushing), and 2 one-shot actions (Switch, PutDynamite via a newTriggerOneShotAnim()/IsOneShotAnimPlaying()mechanism, same freeze shape as the existing Bye/pickup-freeze triggers) — selected by the SAME flags that already drive these already- functional mechanics (m_vehicleMode/m_nage/m_surf/SecretPower::Hide/a newpushingCrateStep()parameter). Real Turn variants (for every vehicle + base humanoid) and Skateboard's own airborne icons are NOT modeled — precise turn-trigger detection needs its own research pass, same gap as the never-implemented baseTurnaction. ~30 more real states have their data extracted and ready but no live trigger yet (no matching mechanic/edge-event exists for them currently) -- seeplan.md'sBLUPI-047/062-067/070/074/075entries for exactly why each one is deferred. Live-verified via a temporary debug harness (reverted before commit): mounting a Jeep correctly selectsStopJeepand cycles the real111,110,111,112icon sequence. Full regression clean (only the pre-existing unrelatedeasy-gl-resource-smoke-testsfailure). -
feat: real "Bye" farewell freeze for hub world-select portals (plan.md
BLUPI-049/SOUND-042). User-reported: stepping onto a hub portal did nothing visually, unlike real mobile-eggbert's ~1.5s "turn and wave". Verified directly againstDecor.cpp:6436(Config::ScaleTime(30)= 1.5s exactly) andDecor.cpp:5476-5488(real trigger is narrow — world-select contact only, never exit-reached/PauseBack/PauseRestart, which stay instant in real source too). NewBlupiController::TriggerBye()/IsBye()freezes Blupi the same wayTriggerTeleport()does; a newAnimState::Byefalls through to the existing Stop pose (no dedicated wave sprite exists in real source). The world-select touch handler now callsSetYaw()once to face the camera and plays the real entry sound (ch32);LoadMission()itself moved to awasBye/IsBye()before/after-Step()completion check, same shape as teleport-transit completion. Live-verified via a temporary debug harness (reverted before commit): freeze holds ~1.5s, yaw updates correctly,LoadMission()fires exactly once with the right target. Full regression clean. -
fix: north-hill platform lift visibly "clipped through" the crow's-nest floor near the wasp (plan.md
PICKUP-020). User-reported with a screenshot. The lift's patrol math was actually correct (verified twice against the real cube-rendering convention — flush at both ends, zero overlap) — the real problem was the crow's-nest floor's single 1-cell carved shaft opening being surrounded on all sides by solid rock, reading as "vanishes into stone" from nearly any angle. Widening the hole into a full trench still looked wrong once live-tested (a 1-cell-deep gap gets visually filled back in by neighbouring rows off-axis). Real fix, matching the exact conclusionliftA/liftBalready reached for this same problem: removed the enclosing floor slab entirely — the shaft is now open air all the way up, with the chest/exit-goal perched on two small, separate stepping-stones instead of one enclosing floor. Live-verified: the platform now floats clearly in open space at every patrol height. Full regression clean. -
fix: web build showed only a black screen on the real, unpatched shell (plan.md
BUILD-003). The prior entry's headless verification had patchedModule.noInitialRundirectly in a throwaway test copy, bypassingshell-cna.html's own "Click or tap to begin." gate — masking a real bug the user then hit immediately. Root cause (found by reading the compiled.jsglue): modern Emscripten'srun()snapshotsModule["noInitialRun"]into a local variable once, synchronously, before any click can occur, and never re-reads theModuleproperty again — so the shell's old (U3D-era) "flip the flag from the click handler" recovery path was dead code; clicking never actually calledcallMain(). The canvas still went visible regardless (a separate, unconditionalsetStatus("Running...")call fires either way), which is exactly why it looked like a working page that simply never draws anything, not an obvious error. Fix: removed the click-to-begin gate entirely and setnoInitialRun: falseso the game starts immediately — re-verified with a fresh, completely unpatched Chrome run (no test patches, no simulated click) showing the real Init/ gamer-select screen rendering right away. Also addedLINK_DEPENDSonshell-cna.html/pre.jsto theGalaxyEggbertCNAtarget — CMake doesn't treat--shell-file/--pre-jslinker-flag strings as tracked dependencies, so editing either file alone previously leftcmake --buildsilently serving the stale old.html/.js(bit us mid-fix here: the first "fix" build didn't actually relink until a source file was touched to force it). -
feat: Emscripten/WebAssembly web build for
GalaxyEggbertCNA(plan.mdBUILD-003). User-requested prototype build to publish on their own website. Turned out to be almost entirely CMake wiring — the CNA/easy-gl/meta-gl/SDL3 stack was already Emscripten-ready (vendored SDL3 has a.sdl-prebuilt-emscriptenpath,EasyGLGraphicsBackendalready requests a GLES context unconditionally which maps straight to WebGL2,Game.cppalready had anemscripten_set_main_looppath). MadeCNA_GRAPHICS_BACKENDdefault toEASYGLunderEMSCRIPTEN(Vulkan has no web bridge), added the.html/--preload-file/WebGL2 target properties, a newcmake/web/shell-cna.html, and IDBFS-backed save persistence (SaveData::kSavePathnow#if defined(__EMSCRIPTEN__), paired with a--pre-js). Only preloadsContent/icons/backgrounds/sounds(~24MB total), not mobile-eggbert's fullContent/(~460MB) — confirmed via grep thaticons4x/backgrounds4x(~437MB) are never read by this engine. Verified via a real headless-Chrome/WebGL2 run (DevTools-protocol-driven so the async asset load could be awaited): confirmed a realWebGL 2.0 (OpenGL ES 3.0 Chromium)context, terrain/sound/background loading, and a screenshot of the real Init/gamer-select screen rendering correctly. Nativebuild-cnarebuilt + full regression re-run after these changes, clean (only the pre-existing unrelatedeasy-gl-resource-smoke-testsfailure). -
feat: split
world001.vwr's demo content into a genuine 79th world,world999.vwr; enlarge world-hub plazas to 18x18 (plan.md SCORE-013). Explicit user follow-up request: galaxy-eggbert's main global hub should contain ONLY the real teleports (matching real mobile-eggbert's own global hub scope exactly), each sub-hub world should have proper teleports to its own sublevel worlds (already true, see the SCORE-013 full-scope expansion above), and hub-world floors should be enlarged to at least 10x10 so the teleport layout fits comfortably.world001.vwr's previous rich demo/mechanics-showcase content (dynamite/crates/vehicles/water/doors/every tile-icon exhibition/ etc.) is unaffected in substance — it moved wholesale to a new, engine-specific 79th world,world999.vwr(beyond mobile-eggbert's real 78, purely for this engine's own dev/test use).world001.vwritself is now the lean real global hub: an 18x18 plaza holding exactly the 12WorldSelectportals + the realObjectType7exit (→ mission 199) and nothing else. A newBlockTypes::DemoPortal(icon 177, engine-specific) sits in the global-hub plaza and leads directly to mission 999 — the only in-game way to reach the demo/test world, reusing the exact same contact-trigger portal-touch handling asWorldSelect.GenerateWorldHub()'s own spawn plaza grew from 11x11 to 18x18 (matching the global hub's size) for the same "fits comfortably" reason; the existing tested wall+ProgressDoorN-gate corridor mechanism for markers 2+ is unchanged, just now extends from a bigger plaza.VerifyBlupiMovement/VerifyBigDecorParsingCna/VerifyInteractionSystemdefault world paths updatedworld001.vwr→world999.vwraccordingly. Live headless verification (temporary debug instrumentation, reverted before commit): confirmedworld001.vwrhas exactly 12WorldSelect+ 1DemoPortal,LoadMission(999)loads the demo world correctly, the enlargedworld010.vwrshows its correct 4 markers, and reloading mission 1 afterward round-trips cleanly. Full regression clean (only the pre-existing unrelatedeasy-gl-resource-smoke-testsfailure). -
feat: implement third-person camera wall collision (plan.md CAM-005). User-prompted after discussing chunk streaming for future denser worlds — the real risk identified wasn't chunking, it was the chase camera having no wall check at all, letting it clip through geometry to end up outside a tunnel looking back in. New
RaymarchWallDistance()(a small fixed-step raymarch from Blupi's look-at point toward the desired chase position, stopping at the first solid block — simplified vs. the task's own "DDA" wording, since the real max distance is tiny and a small step is plenty precise there) pulls the eye in along the same direction whenever a wall sits between Blupi and the full chase distance. First-person is unaffected. Live headless verification: Blupi placed mid-tunnel facing a nearby wall, confirmed the naive 4-unit chase distance gets clamped to the real available ~1.5 units. Full regression clean. -
feat: expand the hub/mission-progression system to full real scope + real progress-gated doors (plan.md SCORE-013/014, SAVE-006). Explicit user follow-up request: galaxy-eggbert should contain the SAME hub/world scope as real mobile-eggbert (all 78 world files), not just the earlier 3-world proof, plus the real "doors that only open under certain conditions" mechanic gating sublevel access. Researched directly against
Decor.cpp: the real win-exit formula (Decor.cpp:6411-6434) is distinct from thePauseBackformula — mission 1's own exit goes to a real final bonus world (199), notmission/10*10— newWorldRuntime:: ComputeWinExitTarget()fixes a latent bug whereworld001.vwr's own pre-existing exit would have computed an invalid mission 0. Confirmed (by tracingDecor::IsWorld()directly) that real global-hub world-select markers are NEVER access-gated — the locked/unlocked icon swap is purely cosmetic — so no gating was needed there; but confirmed real per-world sublevel access IS gated by real physical door tiles (Decor::AdaptDoors()'sm_mission%10==0branch,SearchDoor()/icon 182, cross-checked againstworlds/world010.txt's own real sign+door layout), opened once the preceding sublevel is won (Decor::OpenDoorsWin()). Ported as newBlockTypes::ProgressDoor2..8+SaveData::IsMissionDoorUnlocked()/UnlockMissionDoor()(a per-gamer persisted flag array keyed directly by mission number), applied atLoadMission()-time (matching realAdaptDoors()running before the level is ever shown). ExtendedBlockTypes::WorldSelect1-8to1-12(all 12 real world hubs). Generated all 78 real mobile-eggbert world files with byte-for-byte identical filenames/mission numbers via a data-driven table inGenerateSampleWorld3D.cpp(1 global hub unchanged in content, 12 world hubs with real per-world sublevel counts 4/5/4/6/8/6/5/4/5/7/5/5, 64 minimal placeholder sublevels, 1 final bonus world) — every world beyondworld001stays a near-empty placeholder per explicit user instruction, until a real 3D world editor exists.world001.vwr's own rich demo content is completely unaffected (confirmed via the full existing test suite still passing unmodified). New tests (VerifyInteractionSystem/VerifyGESaveData) + 2 separate live headless verifications (the full navigation chain, and the door-gate mechanism itself: a fresh world's door starts closed, unlocking + reloading opens exactly that one door and no others) + full regression clean. -
feat: implement the hub/mission-progression system (plan.md SCORE-013..019, MENU-035/036, TILE-006). The single biggest new subsystem this session — real mobile-eggbert's actual structure (global hub, mission 1 → world hubs, mission X0 → sublevels, mission X1-X5 → back via exit/PauseBack/PauseRestart) ported faithfully via new
WorldRuntime:: ComputeWorldSelectTarget()/ComputeMissionBack()(pure, verified directly againstDecor.cpp's real mission-handler formulas andGame1::MissionBack()) andGalaxyEggbertGame::LoadMission(int)(loadsworlds3d/world{N:03d}.vwr, rebuilds terrain/ background, resets Blupi/interaction state to fresh per-level defaults preserving only lives — realPlayPrepare()'s exact scope). Renamed 8 long-unusedBlockTypesconstants (Sp0-Sp7→WorldSelect1-8) — these were ALREADY the correctly-identified real hub-screen world-select markers (icons 158-165, from170's earlier research), just never wired to anything. User-authorized exception to this session's normal "no 3D world editor work" rule: 3 new minimal placeholder worlds (worlds3d/world010/011/012.vwr— a world-1 hub + 2 nearly-empty sublevels, just a small stone-cube floor each) plus one new portal marker added to the existingworld001.vwr's icon-exhibition floor (which now doubles as the real global hub) — its own rich demo content (dynamite/crates/vehicles/water/doors/etc.) is completely unchanged, confirmed via the full existing test suite still passing unmodified. Wired every real mission-transition trigger: world-select portal contact, exit-reached (WinLostReturn),PauseBack(previously fully inert — its own button-press tracking already existed, just was never surfaced), andPauseRestart(upgraded from a position-only reset to a genuine level reload, matching realGame1.cpp:357-358). ResumeCONTINUEnow actually reads back the mission numberSaveDatawas already (write-only) persisting at every Win/Lost checkpoint. Known, deliberate divergence: every level completion still shows this engine's own Win screen before advancing (real source only does that for the true final mission 199, silently reloading otherwise) — kept as an intentional, already-built UX choice, not a new gap. 14 newVerifyInteractionSystemassertions + a live headless run proving the full chain end-to-end (mission 1 → portal → mission 10 → marker → mission 11, temporary debug instrumentation reverted before commit) + full regression clean (only the pre-existing unrelatedeasy-gl-resource-smoke-testsfailure). -
docs only: closed/corrected 15 more
BLUPI-1xxsound-channel duplicates (BLUPI-132-153range) — most were exact duplicates of already-fixedSOUND-0xx/PICKUP-0xxentries just never checked off; several additionally had wrong channel claims (ch41 isn't "glide", ch50 isn't shield-on, ch44 isn't shield-off, ch53 isn't Tank-fire, ch58 isn't Drink's, ch62 isn't Sucette's, ch51 isn't glue, ch21 isn't secret-exit, ch7 isn't door-open), each corrected to point at the real already-wired channel.BLUPI-149's "Electro sounds ch38/ch90" had both claims independently disproven (ch38 is crate-push, ch90 is a footstep-remap channel) and is left open rather than guessed. Also clarifiedTILE-024(water drip animation) is deliberately blocked — the real 48-frame table is a known transcription target, but the real visual is Billboard- rendered (not a terrain-cube atlas swap), so animating it needs the same render-mode decision as the already-deferred Saw-blade/ThinMechanicalitems. No code changes. -
feat: implement the real vehicle motor sound crossfade (plan.md SOUND-007/008, ch15-18/28-31). Real
Decor::AdaptMotorVehicleSound()gives Helicopter its own start/loop-high/loop-low/stop sound set (ch15/16/18/17) and Jeep/Tank/Overcraft share a second set (ch28/29/31/30) — Skateboard has no motor sound at all. NewBlupiController::HasVehicleMotor()/IsVehicleMotorHigh()(the real per-mode "moving vs idle" pitch-select flag, translated as nonzero horizontal speed for ground vehicles / nonzero vertical velocity for flight modes) +GalaxyEggbertGame::UpdateVehicleMotorSound(), a direct port of the real crossfade state machine (one-shot start/stop sounds bracket the looped motor sound, exactly mirroring realm_blupiMotorSound's sentinel-based transition logic). Closed 12 more stale/mislabeledBLUPI-0xxcheckboxes along the way (the whole "Vehicle Modes" subsection was in the same not-yet-cross-referenced state as the earlierBLUPI-104-117batch) — found and corrected a wrong channel claim (BLUPI-093's "ch53 = Tank fire sound" is actually the real out-of-ammo click; fire itself is ch52) and a repeated "Balloon" naming-trap mislabel (BLUPI-096/148:ObjectType46grants Overcraft, not a separate Balloon vehicle). Left genuinely open/blocked items alone (BLUPI-100Vent/fan propulsion — confirmed still entirely unmodeled;BLUPI-101Suspend — blocked on the deferred render-geometry decision;BLUPI-087helicopter debris — needs a particle system that doesn't exist). 4 newVerifyBlupiMovementassertions. Full regression clean (only the pre-existing unrelatedeasy-gl-resource-smoke-testsfailure); live headless launch smoke check clean. -
docs only: closed 14 stale
BLUPI-1xxcheckboxes (Shield/Hide/SuperBlupi/Invert/Sucette/ Ecrase/Dynamite/Perso/death-cause/footstep) — all already implemented under Phase 14/15/17 orHUD/CAMitems, just never cross-referenced back. CorrectedBLUPI-113's "walk up walls" premise (real Sucette/Power effect is a jump-velocity boost only). Also fully ruled outHUD-020's "EXIT OPEN!" popup (grepped the real localized-string resource file too — zero hits, same likely-invented status as its siblings). No code changes. -
feat: implement the real Blitz-emitter zap ambient sound (plan.md SOUND-079/VISUAL-024, ch69). Real
Decor::BlitzActif()plays ch69 per visible Blitz(305)-floor tile that has a BlitzEmitter(304) tile directly above it, on a fixed 6-tick-per-100-tick pattern. Since this engine'sSound::Play()has no positional audio (just a channel), a one-time lazy world scan for "does any qualifying Blitz/BlitzEmitter pair exist anywhere" (newInteractionSystem::HasBlitzEmitterPair(), same "-1 = not yet scanned" idiom as the existingtotalTreasures_lazy scan) is behaviorally equivalent to a real per-visible-tile check, at a tiny fraction of the cost — avoids a per-frame whole-world grid scan entirely. NewBlockTypes::BlitzEmitter=304constant (icon 304 iskPassable[304]==false, so this engine's world loader already preserved it verbatim; just needed a named constant). The visual half of the same checklist item ("tiles 66-68 draw 13px higher") is a separate, unrelatedm_bigDecorsprite-corner-anchor pixel nudge — confirmed non-portable, same category as other already- dismissed 2D-anchoring artifacts, left un-implemented deliberately. 4 newVerifyInteractionSystemassertions (lazy-scan correctness with/without a qualifying pair, full 100-tick-cycle smoke run both ways). Full regression clean (only the pre-existing unrelatedeasy-gl-resource-smoke-testsfailure); live headless launch smoke check clean. -
feat: implement real water splash/bubble effects (plan.md PICKUP-078/079/080, SOUND-032/033/ 034/074, BLUPI-151). Found while auditing the Pickups & Objects checklist: real channel 22 was wired backwards (playing on water ENTRY; all 4 real
PlaySound(ch22,...)call sites are EXIT paths,Decor.cpp:5356/5378/5392/5405) and the real entry splash (ch23, "Plouf",ObjectType14) wasn't wired to anything. Fixed ch22 to fire on exit only; added ch23 + a realInteractionSystem::SpawnWaterSplash()spawn on the dry->Surf/dry->Nage transition (single-instance-gated via newHasActiveObjectOfType()). Added the jump-exit-specific "Tiplouf" splash (ch64,ObjectType35) — this engine approximates "deliberate jump exit" asjumpPressedat the exact exit frame (documented simplification, single-point collision has no equivalent to the real 3-way exit-cause distinction). Added the ambient rising bubble while swimming (ch24,ObjectType15, newInteractionSystem::SpawnWaterBubble()— scans the real water column above Blupi viaBlockTypes::isWater(), spawns a bubble rising exactly that many tiles, self-deleting on arrival via the existingAdvancePatrolStep()ObjectType23-arrival branch, matching real source's identical treatment of both types there), triggered twice per ~3.5s cycle offWorldRuntime::GetAnimPhase() % 70. Also fixed 3 moreObjectIcons.cppwrong-approximation-formula bugs found along the way (same bug class as theexplo5/6/8fixes the day before):ObjectType14/15/35were using monotonic-range formulas instead of the real non-monotonic/shuffledtable_plouf/table_blup/table_tiplouftranscriptions. 14 newVerifyInteractionSystemassertions (spawn/self-delete timing for both splash types, corrected icon values, bubble column-scan height + self-delete-on-arrival + no-op-with-no-water-column guard). Full regression: 78/78 minus the pre-existing unrelatedeasy-gl-resource-smoke-testsfailure onbuild-cna; live headless launch smoke check clean on both backends. -
docs only: resolved both remaining
plan.md§3 Open Questions on user request ("hlubší research na icon 95/440"). Icon 95: not ambiguous —Tables.cpp:1872'stable_decor_eau1[6] = {92,93,94,95,94,93}(triggered only when the placed grid icon is 92,Decor.cpp:1068-1072) makes 95 a pure animation-derived frame, never placed directly; already correctly implemented (TerrainRenderer.cpp:298'skAnimWater1, TILE-018). Icon 440:Decor::OpenDoorsTresor()(Decor.cpp:11636-11658) confirms treasure-gated doors are421 + (N-1)for N treasures required, so 440 = a legitimate 20-treasure-door id — but a sweep of all 78 realworlds/*.txtfiles found 421-435/437 each placed at least once while 436/438/ 439/440 are never placed (highest real usage: 437 = 17 treasures); 436-439 still have validobject-m.pngart despite being unused, so 440's specific out-of-bounds atlas gap is best explained as the atlas simply being sized for exactly 440 slots (0-439) — one short of the numbering scheme's theoretical max — a slot nobody ever needed art for, not a bug ever hit in the shipped game. No code changes;BlockTypes.hppbounds deliberately left untouched as before. -
3c65870docs only: fixed 6 staleTILE-0XXentries — Fan (all 4 directions), Temperature, and Marine tile animations are already implemented inTerrainRenderer.cppbut were still marked not done. Flagged an honest, unresolved compass-direction-label mismatch for the 4 Fan entries (no gameplay consequence either way, this engine doesn't model wind-push physics). -
8a18d21docs only: fixedENEMY-005(likely hallucinated "5s enemy respawn timer" — verifiedDecor::ObjectDelete()has no such timer, no "respawn"/"revive" found anywhere for enemies) andENEMY-027(stale cross-reference to the already-corrected136premise). -
f1a96b9docs only: clarifiedBLUPI-012/013/014are architecturally superseded by this engine's own float-native position/3D collision, not gaps to port literally. -
fa13247docs only: fixed 5 stale/wrongBLUPI-0XXentries — crouch, respawn, and the position-history FIFO were already implemented but marked not done;BLUPI-005's "look-up glide" premise looks hallucinated (no such mechanic found anywhere inDecor.cpp);BLUPI-016labeled the water-breath gauge as a generic "vehicle charge gauge". -
f570da4docs only: finished theSOUND-0XXchecklist audit (3rd/final pass) — identified real meanings for the remaining "unknown" channels (2/5/6/32/34/35/39/65/68), fixing more wrong labels along the way (ch39 is crate-"Pop", not "key sparkle"; ch65 is the Mockery idle-taunt sound, not "suspend detach") and catching a 3rd copy of the already-cancelled hallucinated "stomp kill" claim (ch5,SOUND-015) that the first 2 cancellations missed. Every channel 1-92 now has a verified real meaning on record. No code changes. -
ff328f7feat: implement the real crate-push loop sound (ch38). Found during the checklist audit — real ch38 is the crate-push loop (not "electric arc"), a genuine missing sound for an already-working mechanic. NewSound::Stop(channel)(per-channel, sinceStopAll()would kill every other sound) +InteractionSystem::CrateBeingPushedThisFrame()signal +GalaxyEggbertGame::wasPushingCrate_turning it into a real start/stop loop. -
a1c8672/9efe096docs only: 2 earlier passes overplan.md'sSOUND-0XXchannel checklist, cross-referencing which channels this engine already plays against the checklist's[ ]marks — 23 entries corrected total (many already wired but marked "not done"/"unknown"; several with outright wrong real-channel labels, e.g. ch54/57/72/73/75). -
2bab5b4docs only: clarified that PICKUP-027 (m_blupiTimeNoAsc) and PICKUP-035 (crate-land shake) aren't independent gaps — the first is genuinely part ofAscenseurVertigo(same render-geometry block asPICKUP-024), the second needs crate fall-physics that doesn't exist (151), not "no shake system" as previously stated (stale since 2026-07-14). -
b3cbe64feat: implement secret exit (ObjectType21) contact (PICKUP-009/083). Verified againstDecor.cpp:6158-6184— shares the exact same real exit-gate logic as the regular exit; this engine previously only recognizedObjectType7, so a secret exit did nothing on contact. -
7d2e612docs only: cancelled PICKUP-086/087 (hallucinated "shield trail sound" — real channels 48/49 are unrelated Ouf3/Ouf4 idle-fidget sounds; the real trail-spawn code has noPlaySound()call at all). -
9505fd5fix: exacttable_explo5/6/8transcriptions + resolve the explo-mapping open question. Verified againstDecor.cpp:8395-8489: the realexplo1-8→ObjectTypemapping is a plain 1:1 correspondence, not "many-to-one or context-dependent" as the open question inplan.mdclaimed. explo1/2/3/4/7 were already exact (2026-07-14); explo5/6/8 were the 3 remaining cases still using an approximation formula, now exact. -
3f577c8feat: implement the Perso-decoy/lethal-decor enemy trap mechanic. Resolves a mystery left open since 2026-07-13 ("what does placing a Perso decoy actually DO gameplay-wise" — the original search targeted enemy-AI code and missed the real site). Verified againstDecor.cpp:7957-7975+Decor::MovePersoDetect(): small enemies (4/32/33) that patrol into contact with ANY real 200-203 object (the placed decoy, OR one of the 201-203 lethal decorations just below) mutually destroy each other. -
4fecb1efeat: implementObjectType201-203lethal contact (PICKUP-069). A real, previously-undiscovered gameplay gap (not a vehicle/immunity retrofit like the others above) — found via directDecor.cpp:6088-6115read: types 201-203 shareObjectType200Perso's real range but damage Blupi on contact (BlupiDead(Clear1, Clear2)coinflip, Shield/Hide immune, always channel 10 + SmallShake + anObjectType10pop effect).plan.md's own note that "no contact-damage logic found" was wrong — easy to miss reading only around the Perso branch. New block inInteractionSystem::Update(), 6 newVerifyInteractionSystemassertions. -
2284775docs only: fixed 3 more stale Shield/Hide-immunity notes (fired projectiles, Spikes, Fan) that claimed immunity "isn't modeled" — all 3 were already fixed when170(secret powers) landed 2026-07-12, just never updated afterward. Also corrected141's stale "Drip NOT done" claim (Drip was implemented 2026-07-14). -
a7622a0fix: Shield/Hide now grant real immunity to the water breath gauge. Verified againstDecor.cpp:4615-4620— the gauge only decrements at all while NOT Shield/Hide (this engine decremented unconditionally). An already-documented "not modeled" gap, now closed. -
6341319docs only: fixed 2 more stale "vehicles aren't modeled" comments found during a final sweep (TriggerTeleport's caller-side comment, the secret-power Trigger*() header note). -
9027b84fix: vehicles/Balloon/Ecrase now skip water Surf/Nage detection. Verified againstDecor.cpp:5284-5285— not a "forced dismount" as a stale comment claimed, water tiles just never register as Surf/Nage while riding/ballooned/squashed at all. -
51e4d6ffix:TriggerDeathLock()now clears vehicle/Balloon/Ecrase/secret-power/Invert state. The most significant fix this session — found while researching the large-creature contact: realBlupiDead()unconditionally clears ALL of this on every death, not a per-hazard special case. This engine's death-lock left it completely untouched across death/respawn. -
8fdc23bfix: springs now forcibly dismount vehicles before bouncing. Verified againstDecor.cpp:2837-2893. New sharedDismountAndDepositVehicle()helper (factored out of the existing voluntary dismount, which had the same logic inline). -
2783a9bdocs only: confirmed HUD-010/011/013/021 are non-features by reading the complete realDrawInfo()function end to end. -
ee91d1ffeat: real per-cause DeathLocked/PickupBusy animation frames (Blupi-model prep). ParsedTables::table_blupidirectly via a small script (validated by first reproducing the already-approvedkTeleportingFramesbyte-for-byte before trusting new output) to transcribe the 6 realDeathCausehurt-sprite frame arrays (Clear1-4/Glu/Drown) and 3 realPickupFreezeKindbusy-animation frame arrays (Sucette/Drink/Charge) — replaces the previous static "Stop pose" stand-in. NewBlupiController::m_deathCause(stored soGetAnimIcon()can select the right array;m_pickupFreezeKindalready existed). Format-agnostic icon-index data only — no 3D model exists yet to actually display it, becomes visible once069ships. NewVerifyBlupiMovementassertions (Clear1/Clear2/Sucette exact icon values). Full suite green both backends. -
9ca723ffix: real vehicle-mode gate for crate push. Verified againstDecor.cpp:6130-6132— excludes every vehicle mode + Balloon/Ecrase. NewblupiCanPushCrateparameter onInteractionSystem::Update(). -
34a1ccffix:TriggerMount()now excludes Balloon/Ecrase. Verified againstDecor.cpp:5649/5669/5687— these were missing from the mount gate entirely; its own header comment wrongly claimed Ecrase was "already handled elsewhere." -
f575064fix: real vehicle-mode gate for Dynamite/Perso placement. Verified directly againstDecor.cpp:4792-4794: placement (Dynamite AND Perso, sameif/else ifgate) excludes EVERY vehicle mode (Helicopter NOT exempt here, unlike switches) + Balloon/Ecrase. NewblupiCanUseHandsparameter onPlaceDynamite()/TryPerso()— deliberately does NOT gateTryPerso()'s separate pickup-an-already-placed-decoy branch (real source has no vehicle clause there). -
59db4adfix: real Over/Jeep/Tank/Skateboard/Balloon gate for switch activation. Verified againstDecor.cpp:5529-5531— Helicopter IS exempt here (unlike every other vehicle gate this session), a hovering Helicopter can still reach down and press a switch. Caught and fixed a drafting mistake before committing: an earlier version wrongly gated the WHOLE shared action-button block, which would have broken vehicle dismounting. -
2247c38fix: real Over/Jeep/Tank hazard immunity for Spike/Drip/Saw. Verified againstDecor.cpp:5497-5528— these 3 hazards specifically exempt Overcraft/Jeep/Tank (not Helicopter/ Skateboard); Lava/Blitz/Crusher deliberately have no vehicle clause at all. NewBlupiController::HasVehicleHazardImmunity(). -
5b105c0fix: real vehicle-mode gate + Skateboard velocity for ground jump. Verified againstDecor.cpp:2913-2947— Jeep/Tank/Helicopter/Overcraft/Balloon can't ground-jump at all; Skateboard gets its own real distinct velocity (-17 Power/-13 noPower) instead of the headroom-modulated ordinary values. NewkSkateboardJumpSpeed/kSkateboardJumpSpeedPowered. -
4c85517/8dcc986docs only: corrected 2 staleplan.mdnotes (066's turning-duration table turned out to be a discrete 2D facing-flip mechanic, not a portable continuous turn-rate table;067's "still missing hazard table" was already done by the death-lock system), and root-causedeasy-gl-resource-smoke-testsas entirely an../easy-gl-side bug (its test expectsglActiveTexture(GL_TEXTURE0)fromTexture::set_image_2d()/bind(), but neither calls it) — confirmed pre-existing/unrelated (that repo's last commit predates this session by 11 days), not fixed here since it's a separate independently-developed repo. -
1d99487fix: real vehicle-mode/Balloon/Ecrase pickup+teleport gates. Verified directly againstDecor.cpp:6025-6087and:5593-5594: Sucette(26)/Drink(30)/ Charge(31) pickups and the teleporter both really exclude every vehicle mount plus Balloon/ Ecrase (!m_blupiHelico/Over/Balloon/Ecrase/Jeep/Tank/Skate) — this engine had none of that for pickups, andTriggerTeleport()only checked Balloon/Ecrase (its own comment wrongly claimed "vehicles aren't modeled", stale sinceVehicleModewas added). Shield(25)/Invert(40) genuinely have no such clause in real source — confirmed, not just assumed; this closes §8 item 1 from the previous update (which had mistakenly also listed Shield). NewblupiVehicleOrSquashedgate inGalaxyEggbertGame.cpp'scanGrantPower/Cloud/Hidecomputation;BlupiController:: TriggerTeleport()now also checksm_vehicleMode. New tests inVerifyBlupiMovement.cpp(vehicle-mounted teleport no-op) andVerifyInteractionSystem.cpp(gated Sucette grant). -
f6753cafix: require the action button for Sucette/Drink pickups. Real mobile-eggbert gates Sucette(26)/Drink(30) on the action button held at contact (getButtonPressedProperty()==PlayAction); this engine previously granted both automatically. AddedblupiActionPressedEdgetoInteractionSystem::Update(). Charge(31) deliberately has no such gate (confirmed via direct source read). New positive/negative tests added. -
de6ef8bfeat: implement real Sucette/Drink/Charge 2-stage pickup delay. Real exact durations: Sucette=32 ticks(1.6s), Drink=36(1.8s), Charge=64(3.2s) — all three genuinely freeze Blupi, a third application of the freeze-timer pattern built forTriggerTeleport()/the death lock. Sucette/Drink defer their buff grant to completion; Charge's buff already grants at contact in real source (only its freeze/sounds were missing). NewBlupiController::TriggerPickupFreeze()/ConsumePickupFreezeResolved(), newInteractionSystem::RespawnPickupItem(). -
6a34aafdocs: confirm respawn invincibility window is a non-feature. Researchedplan.mditem103— no invincibility flag/timer exists anywhere in real mobile-eggbert source; post-respawn safety is purely spatial (the FIFO safe-position system). Closed, not implemented. -
b03b827docs: record live-visual verification of the death-lock system. Temporary, fully-reverted debug scaffold confirmed the death→lock→life-loss-Voyage→respawn sequence live in the running game. -
3261417feat: implement real death-lock + life-loss Voyage system. Every real hazard death (Clear1-4/Glu/Drown) now locks Blupi for a real fixed duration (Decor.cpp:6374-6392: 70/100/70/110/100/90 ticks), then plays the real life-loss Voyage (icon 48/Blupi channel, decrements lives at Voyage start, not completion) before respawning. NewBlupiController::TriggerDeathLock()/ConsumeDeathLockResolved()/IsDeathHidden(). This also completes the realGlu"stuck" mechanic (it shares this same pipeline, no unique VFX of its own). Found and fixed a real nuance: 3 causes (not the 2 first suspected) skip the safe-position respawn — Fan, generic-hazard-contact, and dynamite blast. -
fde1c54docs: document Glu death mechanic research. No code changes — established thatGlurequires the death-lock system above. -
b8e5a77feat: implement Clear2/Clear3/Clear4 death VFX. Of Blupi's 8 realBlupiAction::Clear1-Clear8types, only 3 have real VFX: Clear2/Clear3 fire a "soul ascends" HUD Voyage (icons 230/40), Clear4 (Saw) fires a 3-direction particle burst. Clear1 has none; Clear5-8 are confirmed dead code (never assigned anywhere in real source). -
4ff297afix: restore real immediate Dynamite/BulletPack Voyage sounds. A bug found via a fresh source re-read: both had been wrongly ported as silent. -
5a424e3feat: implement faithful Voyage pickup-reward system. Real mobile-eggbert defers pickup rewards (treasure/keys/egg/dynamite/Perso/bullet pack/door unlock) behind a 2D "fly to HUD icon" animation instead of applying them instantly on contact. NewInteractionSystem::VoyageKind/BeginVoyage()/TickVoyage(), newHud::ProjectWorldToHudSpace()(world→screen projection, didn't exist before).
There is no active build or test failure blocking progress. build-cna builds and passes
84 tests with the pre-existing easy-gl dependency test excluded (see §5). The original 3D world
editor plan is fully complete, and user-directed follow-ups through EDITOR-130 are implemented.
Resolved 2026-07-19, re-verified 2026-07-23: build-cna-vulkan builds and passes ctest
cleanly through the entire editor line of work (EDITOR-100..112) and every INFRA-/BUILD-
addition since, confirming everything is genuinely backend-agnostic.
The remaining blockers are items that genuinely need a human decision, not more engineering:
AscenseurVertigo(wide/shiftable lift platforms, icons 311-316) — needs the user to choose which of 3 existing render approaches to use.
(Saw blade (icon 378) render orientation — resolved 2026-07-20 after a 6-round live-feedback session; see §3's own writeup.)
If picking up this project without that answer, the honest "next thing to do" is one of the concrete, non-blocked tasks in §8 below, not a bug fix.
- Resolved 2026-07-20 (was here as a confirmed bug for over a week): Saw blade (icon 378) render orientation. See §3 for the full 6-round live-feedback history.
- Resolved 2026-07-21 (INFRA-005, plan.md §7): the airborne-wall-clip gap found 2026-07-18 while
implementing Balloon's horizontal drift (
BlupiController::TryMoveAxis()'s step-up gate only applying whilem_onGround) is fixed —ResolveMove()now applies the same real wall check unconditionally for every mode (plain jumping, every vehicle, Balloon's drift), matching realDecor::BlupiStep()'s own unconditionalTestPath()call. See §3's own writeup for the full resolver rewrite this came from. - Incomplete: No visible 3D Blupi model (blocked on the user providing one).
Incomplete (world editor, by design — the remaining plan milestone): no unsaved-changes guard (EDITOR-112)— done 2026-07-23, see §3/plan.md §6's ownEDITOR-112entry: Back is now a real 2-tap confirm when there are unsaved changes (dirty flag + 2nd-press-to-discard), unchanged single-tap otherwise.- Deliberately limited editor text: functional text now covers XYZ coordinates, numbered background thumbnails, browser labels, and the two-second not-implemented notice. The main tool palette remains icon-driven rather than becoming a general text UI.
- Resolved 2026-07-19:
build-cna-vulkanrebuilt/re-tested (see §4) — 76/76 (100%). - Risky assumption (world editor):
ObjectTypecategory names/membership inPaletteCategories.cppwere taken only fromObjectType.hpp's own documented comment groups. The "Confirmed" tab deliberately excludes types the game spawns itself (explosions, splashes, projectiles, door/bridge animations) — they remain reachable via "All Types". If a type turns out to be placeable/non-placeable contrary to that grouping, fix the grouping rather than inventing new semantics for it. - Fixed 2026-07-16: Sucette(26)/Drink(30)/Charge(31) pickups and
TriggerTeleport()now check vehicle mode + Balloon/Ecrase (Decor.cpp:6025-6087/:5593-5594). Shield(25)/Invert(40) confirmed to have no such clause in real source (the previous entry here mistakenly listed Shield). See §3. - Fixed 2026-07-16 (Blupi-model prep, format-agnostic — see §7.5): the real per-cause
hurt-sprite animation frame table (
Tables::table_blupi) is now transcribed for both the death-lock (AnimState::DeathLocked, 6 causes) and pickup-freeze (AnimState::PickupBusy, 3 kinds) states, replacing the static "Stop" pose stand-in — see §3. This is icon-index data only (no 3D model exists to actually display it yet); it becomes visible once069ships. - Needs verification:
easy-gl-resource-smoke-tests's single failing assertion (test_texture_upload_sets_unpack_alignment_wrap_and_unit0_binding,../easy-gl/tests/smoke/SmokeResourceTests.cpp:336) — root-caused 2026-07-16, confirmed entirely in../easy-gl, not galaxy-eggbert. The test doestexture.set_image_2d(...)thentexture.bind(Texture2D)and assertsg_state.last_active_texture == 0x84C0(GL_TEXTURE0), but neitherTexture::set_image_2d()norTexture::bind()(../easy-gl/src/Texture.cpp:52,118) ever callglActiveTexture— only the separateTexture::active_bind(unit, target)does (line 57).g_state.last_active_textureis never written, so it stays at itsreset_state()default (0), not0x84C0, and theassert()aborts. Either the test should callactive_bind()/not assert this, orset_image_2d()/bind()should callglActiveTexture(GL_TEXTURE0)first (both plausible fixes) — that decision belongs to../easy-gl's own maintainers/plan (its last commit,5a50c69, predates this session by 11 days, confirming it's genuinely pre-existing, not introduced or affected by anything done here). Not fixed in this session — it's a different git repository under its own independent development (same caveat as../sharp-runtime, see §1), out of scope to modify without the user's direction. - Risky assumption to keep in mind:
InteractionSystemis deliberately decoupled from bothBlupiControllerand any camera/graphics type (see §6). Every new feature that needs either has had to route through a same-frame "pending signal" (*ThisFrame()flags) consumed byGalaxyEggbertGame. It is tempting to "simplify" this by just passing aBlupiController&intoInteractionSystem::Update()— don't; this has been a deliberate, repeated architectural choice across at least 3 features this session (Voyage, death-lock, pickup-freeze), not an oversight.
Core classes (all in src/GalaxyEggbert/Game/ unless noted):
BlupiController— Blupi's own movement/physics/state machine. Owns position, velocity, secret-power state, vehicle mode, and now the death-lock and pickup-freeze timers. Has no dependency onInteractionSystem,Sound, or world/graphics types. Its freeze-timer shape (TriggerTeleport()/m_teleporting,TriggerDeathLock(),TriggerPickupFreeze()) is a reusable template: set a flag + a duration,Step()early-returns while the flag is set, auto-clears when the timer elapses. Reuse this exact shape for any future "freeze Blupi for N seconds" mechanic rather than inventing a new one.InteractionSystem— pickups, hazards, enemies, doors, lifts, crates, secret-power grants, cheats. Deliberately has no access toBlupiController,Easy3D::Camera3D, or theGraphicsDevice— every real mechanic that needs Blupi's controller state or screen-space projection communicates via same-frame*ThisFrame()boolean signals (and, where multi-frame state is needed, a small pending-request struct) thatGalaxyEggbertGamereads and acts on right afterUpdate()returns. Do not "fix" this by adding a direct dependency — see §5's own note.GalaxyEggbertGame(src/GalaxyEggbertCNA/GalaxyEggbertGame.hpp/.cpp) — ownsblupi_,interaction_,worldRuntime_,camera_,sound_,hud_. This is where cross-class orchestration lives:ResolvePendingVoyage(),ResolveDeathLock(),ResolvePickupFreeze()are all called once per frame, right afterinteraction_.Update()returns, to consume pending signals fromInteractionSystemand driveBlupiController/Hudaccordingly.WorldRuntime— the live, mutable per-frame world state (terrain block queries plus theMobileObjSpeclist of every pickup/enemy/effect object).InteractionSystemandGalaxyEggbertGameboth operate on the sameWorldRuntime&instance each frame.BlockDefinitionRegistry/ObjectDefinitionRegistry— the authoritative, backend-independent block and MoveObject type metadata. Rendering, collision, editor validation, imported-world support, texture-sheet selection, and shared declarative MoveObject capabilities consume these definitions; do not reintroduce parallel icon/type classification lists in consumers.Hud— 2D HUD rendering in mobile-eggbert's own 640x480 reference space, plus theProjectWorldToHudSpace()static utility (world position → that reference space, via a real clip-spaceVector4::Transformthen invertingHud's own ref↔viewport scale/offset math).include/GalaxyEggbert/Worlds/,Def/*.hpp,BlockTypes.hpp,BlockDefinitionRegistry.hpp— engine-agnostic data model (World/Chunk/Block, real enum IDs). Shared by any future engine target; keep it that way (no CNA/Easy3D-specific dependencies here).
World editor (src/GalaxyEggbert/Editor/, new 2026-07-18):
WorldEditoris the only classGalaxyEggbertGametalks to. It owns the free-fly camera, raycast/highlight state, undo stack, palette, box-fill state, and browsing/editing mode.- It follows the same "pending signal consumed by the owner" idiom as
InteractionSystem: it has noWorldRuntime, phase, or file-loading access, so it reportsConsumeNeedsPresentationRebuild()/ConsumePlayTestRequested()/ aBrowserRequest, andGalaxyEggbertGameperforms the actual rebuild/phase-switch/load. Keep it that way. - The lower helpers (
VoxelRaycast,BoxRegion,EditCommandStack,PaletteCategories,CustomWorldStorage) are pure logic with no CNA/Easy3D dependency, which is what letsVerifyGEWorldEditorlink and test them headlessly. Do not introduce graphics dependencies into these five — it would silently cost the whole test suite.
Coordinate invariants (the single easiest thing to get wrong here):
- Block centering: block index
Nspans[N-0.5, N+0.5)— not[N, N+1). Position→index conversion usesstd::lround, notfloor(seeWorldRuntime).VoxelRaycastbakes the corresponding+0.5shift into its DDA. - Render space vs raw grid space: render space = raw grid space shifted by
-WorldRuntime::kWorldCenterX/Z(= 50) on X and Z only; Y is identical in both.Worlds::WorldandMoveObjectRecordare always raw grid space; the camera is always render space.WorldEditor::Update()converts internally so callers never apply the shift themselves. MoveObjectRecordanchoring: a record is stored in the block-extra-metadata offloor(posStart), and at most one record may occupy a given anchor cell — placing a second silently replaces the first. Undo of an overwrite must therefore restore the previous record, not just clear the cell (EditCommandStack::ApplyMoveObjectStatedoes exactly this).RebuildWorldPresentation()now callsWorldRuntime::ResyncFromWorld()first. The editor mutates the liveWorldin place with no disk round-trip, soLoadFromVwrFile()— previously the only thing that populatedmobileObjects_/skyRegion_/missionNumber_— never runs for an in-editor edit. Removing this call makes placed objects invisible until save+reload.- Palette vs world clicks:
EditorPaletteclaims the mouse from the press frame onward (per press, not per cursor position), because world edits are press-triggered while the palette acts on release. Don't "simplify" this back to a release-only check — see §2's bug note.
Invariants / boundaries that must not be broken:
../mobile-eggbertis never modified, not even temporarily, not even for "just looking."../simple-3dis read-only.- No
#ifdefguards distinguishing engine backends anywhere inGalaxyEggbertCNA. InteractionSystemstays free ofBlupiController/camera/graphics dependencies (see above).- Real numeric IDs (
ObjectType,BlockTypes, sound channels) must never be renumbered — they encode the real mobile-eggbert data format. SaveData/InputPad/InteractionSystembyte layouts are intentionally not compatible with real mobile-eggbert's own save/format — don't "fix" this without a deliberate, separate decision (seeCLAUDE.md's reuse table).
Configure + build (EasyGL, the default backend):
cmake -S . -B build-cna -DGALAXY_EGGBERT_BUILD_CNA=ON
cmake --build build-cna --target GalaxyEggbertCNA -j2
Configure + build (Vulkan backend):
cmake -S . -B build-cna-vulkan -DGALAXY_EGGBERT_BUILD_CNA=ON -DCNA_GRAPHICS_RENDERER=VULKAN
cmake --build build-cna-vulkan --target GalaxyEggbertCNA -j2
(Use -j2 maximum — this environment has crashed under more parallel jobs.)
Run the game (must run from its own build dir — relative asset paths):
cd build-cna && ./GalaxyEggbertCNA
Run the full test suite:
cd build-cna && ctest
cd build-cna-vulkan && ctest
Build and run one specific verification tool directly (bypasses ctest's summary, shows every
PASS:/FAIL: line):
cmake --build build-cna --target VerifyInteractionSystem -j2
cd build-cna && ./VerifyInteractionSystem
Build + run the world-editor verification suite (the relevant one for all EDITOR-* work — prints
every PASS:/FAIL: line; expect 165 checks and ALL CHECKS PASSED):
cmake --build build-cna --target VerifyGEWorldEditor -j2
cd build-cna && ./VerifyGEWorldEditor
Run the engine-agnostic world-model gtest suite directly (expect 66/66):
cmake --build build-cna --target GalaxyEggbertWorldsTests -j2
cd build-cna && ./GalaxyEggbertWorldsTests
Reproduce the one known ctest failure (pre-existing, in the easy-gl dependency — not a
galaxy-eggbert bug, see §5):
cd build-cna && ctest -R easy-gl-resource-smoke-tests --output-on-failure
Headless live run (for visual verification of editor/gameplay changes; needs xvfb-run):
cd build-cna && timeout 80 xvfb-run -a ./GalaxyEggbertCNA
INFRA-001's deterministic golden-screenshot capture (plan.md §7): loads the fixed demo world,
runs a fixed 180 ticks, writes golden_frame_0060/0120/0180.png, then exits on its own (needs
xvfb-run, no timeout required since it self-terminates — confirmed via 2 independent runs
producing byte-identical PNGs). Not wired into the default ctest run — like the live headless
check above, it needs a real display/GL context, which isn't guaranteed in every environment that
runs ctest; run it explicitly:
cd build-cna && xvfb-run -a ./GalaxyEggbertCNA --golden-capture
INFRA-002's golden-image diffing (plan.md §7): runs the capture above and byte-compares each
frame against the approved reference under tests/golden/, printing PASS/FAIL per frame
(same precondition as INFRA-001 — needs a real display, not wired into default ctest):
xvfb-run -a tools/verify_golden_frames.sh build-cna
INFRA-002's "behavioral trace" half (plan.md §7, closed 2026-07-22): a SEPARATE mode,
--golden-capture-trace, that ALSO drives Blupi through a small scripted walk+jump down the
tested corridor (unlike --golden-capture above, which never feeds him any input at all) and
diffs the resulting per-tick golden_trace.txt against tests/golden/golden_trace.txt:
xvfb-run -a tools/verify_golden_trace.sh build-cna
Regenerate the sample world (only needed after editing tools/GenerateSampleWorld3D.cpp):
cmake --build build-cna --target GenerateSampleWorld3D -j2
cd build-cna && ./GenerateSampleWorld3D
No lint/format tooling is configured in this repository at present.
The user authorized an extended unattended session and pre-answered the questions that would otherwise block it. These answers stand for the remainder of THIS session (re-confirm with the user before treating them as permanent beyond it):
- Skip every task that needs visual judgment on a render/screenshot entirely —
Saw blade orientation(resolved 2026-07-20, see §3),ThinMechanicalgeometry (510), water surface treatment (512), architectural-kit assembly (514), enemy billboard walk-cycle angle (179),AscenseurVertigo(153/PICKUP-024). Do not touch their rendering code or take "best guesses" at geometry — leave them exactly as flagged until the user can look at a screenshot themselves. SaveDatastays an independent format, not byte-compatible with real mobile-eggbert saves (closesE3D-MIG-106/the open question in §3 of the main doc) — don't expandSAVE-*scope toward byte-layout matching.No 3D world editor work this session (SUPERSEDED 2026-07-18: the user explicitly commissioned the editor as its own effort and approved a 13-milestone plan (EDITOR-100..112), answering three design questions up front: it is an in-game mode insideEDITOR-000..010) — a dedicated future effort, not part of this one.GalaxyEggbertCNA(not a separate executable — this also overridesplan.md's ownEDITOR-000placeholder recommendation), custom worlds belong to the existing 3-gamer-slot save system, and the full feature set was requested rather than an MVP. A further standing instruction from that session: any investigation of free-eggbert's code must be delegated to a subagent that returns only what's needed, to keep it out of the main context.- Blupi-model prep IS authorized, format-agnostic only: e.g. animation-state timing/signal plumbing, third-person placeholder improvements, asset-loading scaffolding that would work regardless of the eventual real model's exact format/rig. Do NOT commit to a specific model format or file layout without the user's input.
- Similarly discovered this session: rescaling
kGravity/kJumpSpeed's own absolute magnitude to real tick-domain values (part of065) needs the user's live-feel judgment, same as the visual-judgment items above (see065's own plan.md entry for why) — left open, not a green light to guess.
The original editor plan is complete (EDITOR-100 through EDITOR-112, see §3/plan.md §6), and
the user-directed follow-ups through EDITOR-130 are implemented. Further editor work is selected
as a new follow-up rather than silently extending the original plan.
-
EDITOR-111 — Sky-region picker.— done 2026-07-23, see §3's own writeup andplan.md§6'sEDITOR-111entry for the full history (including the live Xvfb+xdotoolscreenshot verification and a deliberate bug-injection teeth-check). -
EDITOR-112 — Hardening pass + full regression.— done 2026-07-23, see §3's own writeup andplan.md§6'sEDITOR-112entry for the full history: unsaved-changes guard (dirty flag + 2-tap confirm on Back — scoped to that one call site after confirming it's the only real path out of an active editing session in this codebase), a box-fill test straddling the world's own Z=0/Z=99 bounds, and a completed top-of-file section index (found the file's section-naming already consistent throughout, so a wholesale physical reorder wasn't justified — see the plan.md entry for the full reasoning).build-cna-vulkanre-verified clean throughout. -
EDITOR-113 through EDITOR-122 — menu/layout maintainability and user-directed usability follow-ups.— done 2026-07-25, seeplan.md§6. The latest items add a Galaxy-only background group with 32 live thumbnails, make the top-left red-X control remove its current target through the undo/redo command stack, and replace three false temporary notices with verified Inverter, Wooden case, and Hovercraft mappings. -
EDITOR-123 through EDITOR-127 — complete all remaining source-menu entries.— done 2026-07-25. All 96 cells have one exact implementation; exhaustive real-pointer tests, the complete native suite, and the final 800×480 visual inspection pass. Seeplan.md§6.
Non-editor tasks:
-
Take the live screenshot verification of the death-lock/life-loss Voyage one step further— done 2026-07-20. Temporary, fully-reverted debug scaffold (env-var-gated: skip to Play, overwrite the ground block directly under Blupi's spawn with Lava viaWorldRuntime::GetWorldMutable().setBlock(), periodic stderr state log + timed screenshots captured post-HUD-draw, gated ondrawFrameIndex_ > terrainPixelPrintedFrame_same asscreenshot_hud.png's own established fix) confirmed the FULL real sequence live: Lava contact → real Clear3 lock (70 ticks=3.5s, with a concurrent Clear3Ascend cosmetic VFX voyage, icon 40, fixed 50 ticks=2.5s, completing partway through) → LifeLoss Voyage begins (icon 48, fixed 40 ticks=2.0s) with lives visibly dropping 3→2 in the SAME frame the icon starts (matches the real "decrements at Voyage START, not completion" citation exactly) → icon 48 now VISIBLY CAUGHT mid-flight in a screenshot (a small icon near the HUD's treasure-counter row, present partway through the window, gone once the Voyage completes) — the specific gap the previous pass (b03b827) missed. No production code changes survive (confirmed viagit diffshowing zero diff after revert). -
Get the user's visual judgment on the Saw blade (icon 378) orientation, then fix it.— done 2026-07-20, see §3's own 6-round writeup. -
Get the user's decision on
AscenseurVertigorender geometry (icons 311-316, which of the 3 existing render approaches to reuse), then implement it. Files:src/GalaxyEggbert/Game/TerrainRenderer.cpp. Verify:cmake --build build-cna --target VerifyTileUvBounds -j2plus a live screenshot. -
plan.md§7's correctness-infrastructure task breakdown (2026-07-21,INFRA-001throughINFRA-010) — this whole paragraph is now stale (INFRA-005/006/BUILD-011 landed since it was written) and had 4 outright "done" overclaims (external audit, 2026-07-22, independently re-verified againstREMAKE-ANALYSIS.md's own wording before accepting — seeplan.md's ownINFRA-002/003/004/005entries for the specific corrections). Read those entries directly rather than trust a status summary here — in short:INFRA-001/007/009hold up as described;INFRA-002had a real script bug (fixed) and was missing P0-1's "behavioral trace" half — closed 2026-07-22 (--golden-capture-trace/verify_golden_trace.sh, see plan.md's ownINFRA-002entry);INFRA-003was a regression lock only, missing the programmatic reference-doc cross-check P0-2 asked for — closed 2026-07-22 (tools/VerifyObjIconAgainstReferenceDoc.cpp, see plan.md's ownINFRA-003entry);INFRA-004's table is unused by the actual renderer (a documented reference list, not automatic enforcement);INFRA-005's 3-sequential-per-axis deviation from P1-1's literal single merged-position resolve — closed 2026-07-23, true merged X+Z+YResolveMove()now landed after a scoping session picked the most ambitious of 3 offered options, see §3's own writeup andplan.md'sINFRA-005entry for the full history (including a real ~19x walking-speed regression found and fixed during implementation, not just the merge itself).INFRA-006is an honest partial (7 focused families/extractions migrated).INFRA-010is now complete:CURRENT.mdis the compact current-status index; use its own document-ownership table rather than adding another summary here.
- Do not resurrect the retired pre-CNA engine path. Its last source state is already preserved
in git history at
4afd53e. - No modification of
../mobile-eggbert, not even temporarily, not even to "just check something" — copy a file out first if a working copy is genuinely needed. - No copying mobile-eggbert code/data (tables, enums, byte layouts) into this repository without explicit user approval, even when it looks like "just data."
- No new invented gameplay mechanics. Every feature must trace to something confirmed in real
mobile-eggbert source or
mobile-eggbert-reference/— verify before implementing, don't guess from plausibility. The world editor is the one approved exception, and only for editor UX (tools, palettes, camera, undo): the editor must not introduce gameplay behavior that mobile-eggbert doesn't have. - No expanding editor scope beyond EDITOR-110/111/112 until those three are done — no text rendering, no real per-type object icons, no copy/paste or brush tools, no terrain generation. Each is defensible later; none is in the approved plan.
- No graphics dependencies in the five pure-logic editor helpers (
VoxelRaycast,BoxRegion,EditCommandStack,PaletteCategories,CustomWorldStorage) — it would breakVerifyGEWorldEditor's headless linkage (see §6). - No whole-file
git checkoutto revert live-test instrumentation. It can silently discard real uncommitted work in the same file (this happened once during EDITOR-109 and had to be caught and restored). Revert with targeted edits, then confirm withgit diff. - No refactor of the
InteractionSystem/BlupiControllerdecoupling (see §5/§6) — it is a deliberate, repeatedly-reaffirmed design choice, not technical debt. No 3rd guess at the Saw blade orientation without the user's own visual input— resolved 2026-07-20, see §3. Still: don't second-guess it again without a fresh, explicit user report.- No broad refactor or unrelated cleanup while any of the §8 tasks are in flight — each is meant to be a single, small, independently-verifiable session.
- No Lua, MeshCraft, Mesh World, Nova3D, or alternate engine path — none is part of the locked Direct-CNA-+-Easy3D direction.
- No starting
INFRA-005/INFRA-006(plan.md§7's shared collision resolver /ObjectTypehandler-table task entries), or any dual-renderer work beyond the already-merged, unwiredSceneFrame.hpp, without the user's own explicit go-ahead on that specific item — see §7's own "do not bundle into one modernization effort" framing.INFRA-001/003/004(§8 task 4) are fine to pick up on their own.
Status as of the end of the 2026-07-23 autonomous session: repository is clean, builds and
passes on all 3 native backends (build-cna/build-cna-vulkan/cmake-build-debug, 81/82 or 79/79
— the one failure is the pre-existing unrelated easy-gl-resource-smoke-tests), everything is
committed and pushed to origin/develop. This session (in order): fixed a real ~19x horizontal-
walking-speed regression in the just-approved ResolveMove() merge (INFRA-005 follow-up); migrated
2 more INFRA-006 families (vehicle mapping, patrol-enemy icon dispatch), one predicate
extraction, and one terminal-patrol-arrival table; completed the entire 3D world editor plan (EDITOR-111 sky-region picker, EDITOR-112
hardening pass — all 13 milestones, EDITOR-100..112, now done); re-investigated and closed a stale
"grass-topped cubes walkable-through" bug report (does not reproduce — was step-up behavior, not a
bug); swept for and fixed a compiler warning; closed a 3-pickup-wide test-coverage gap (Dynamite/
Egg/BulletPack "second instance at the cap"); a further broader audit then found and fixed a real
out-of-bounds crash in SaveData::Load() (an unvalidated selectedGamer from a corrupted
save file segfaulted on the very next accessor call — confirmed via deliberate bug injection) plus
unified a duplicated kMaxEggCount constant; a final systematic public-API-vs-test-coverage sweep
(every public method in BlupiController/ObjectIcons/InputPad/WorldRuntime grepped
against its own test file) closed 3 more zero-coverage gaps (5 texture-atlas-selection predicates,
TriggerBye(), TriggerMockery()); one more follow-up audit with yet another fresh angle (file-I/O
trust boundaries — external/parsed data flowing unvalidated into array indices or numeric
conversions) swept every disk-reading path in src/GalaxyEggbertCNA//src/GalaxyEggbert/ and found
one more real bug: an uncaught std::stoi crash in LoadFromMobileEggbertFile()'s Decor:/
BigDecor: cell parser (confirmed via bug injection, fixed via a SafeStoi() wrapper), plus 2 minor
hardening gaps consciously left alone (see plan.md's TEST-005 follow-up note for the reasoning); one
final targeted audit (editor undo/redo/command-stack invariants — a fresh, specific angle, not a
repeat of the generic sweeps) came back clean, nothing found, after genuinely careful direct
code reading (not a rushed pass) — see §3's own entry for exactly what was checked. See §3 for each
entry's full writeup.
Genuinely next, in order of what's actually startable:
- The code-quality/edge-case audit category is now confirmed exhausted for this session, not
just narrowing. 8 rounds (TODO/FIXME sweep, stale-doc sweep, compiler warnings, a broad
edge-case pass that found the
SaveDatacrash, a systematic public-API sweep, a file-I/O trust-boundary sweep, and a targeted editor-invariant audit) found real work every time except the last, which came back genuinely clean after a careful, specific-angle look. Don't reach for "audit round 9" with another generic sweep as the default next move — the remaining items below are the actual backlog, and most need the user's input before proceeding (visual/design judgment, or scope confirmation for large content work). If truly nothing else is startable, that is the correct point to report back and ask, not to manufacture another audit. AscenseurVertigorender geometry (icons 311-316) and Suspended/hanging-bar mode (blocked on a NEW "thin-bar" render geometry for icon 202) both need the user to look at a screenshot/mockup and choose an approach — see plan.md §0's "Known open bugs" and theSuspendedwriteup in plan.md's171/178area. Do not guess at geometry; ask first.- Residual seam-line transparency (plan.md's pre-
## 1preamble list) has 2 untested hypotheses (MSAA edge AA; UV inset too small at extreme viewing angles) but assessing either fix ultimately means judging "does this look better," the same category of task explicitly deferred this session for the 2 items above — don't start this without checking whether the user wants to weigh in live first. TILE-005(plan.md) — 4 more hand-authored 3D worlds (Grassland, Forest, Ice Caves, Lava Fields, Space Station; only one small sample world exists so far) is real, substantial, valid work, but large-scope content creation, not a quick task — confirm scope/priority with the user before starting rather than assuming it's wanted next.INFRA-006's remainingObjectTypefamilies: already surveyed twice this session with genuinely diminishing returns (most of what's left is either already covered by existing predicates or has real per-type behavioral divergence unsuited to a shared table — see plan.md's ownINFRA-006entry for the specific candidates already ruled out). A fresh survey could still be tried, but temper expectations.— done 2026-07-23: CURRENT.md is the compact current-truth index; keep its status fields current and leave detailed history in this file andINFRA-010plan.md.
If picking this up cold: read this whole file first, then plan.md's most recent (top-dated)
entries under whichever section a task references, not just this prompt.