EBLAN_ENGINE/docs/STABILIZATION.md
Игорь Федосеев aa64510def feat: stabilize fixed-tick simulation
Decouple gameplay from presentation and preserve signed movement fractions. Separate cinematic camera ownership with safe body handoff and transactional current-room restarts.

Resample input latches on focus regain, discard modal timing debt, and attach cinematic lighting to the view to match editor preview.

Verify editor 137/137 and ASM 288/288, cinematic traces, RGB bake and IPC framebuffer parity without changing project assets.
2026-10-31 22:31:20 +03:00

11 KiB

Stabilization roadmap

Stages are delivered separately. Each completed stage is tested, committed and pushed before work stops for confirmation. Editor and runtime UI redesign will use an Unreal Engine 5-inspired visual style; stage 1 does not redesign UI.

Stage 1 — correctness and data safety

Implemented:

  • DrawRectOutline keeps loop counters in a callee-saved register, not the volatile registers overwritten by DrawPixel. Nonpositive sizes draw nothing.
  • TraceShotWall traverses current map cells independently of rendering. Distances are 24.8 world units; closed doors, glass, portals and out-of-map cells block shots. Open doors do not. Corner-adjacent walls block diagonal seam shots.
  • Hitscan chooses the nearest live guard intersecting the forward billboard hitbox. Half-width is 16 world units times authored scale (zero scale means legacy 1.0), not a constant number of screen pixels.
  • IsGameplayBlocked centralizes gameplay gating. UpdatePlayer latches blocked ticks before processing cinematic state. AI, pickups and weapon timers freeze during active/pending cutscenes and room fades. END/skip and instant transitions cannot resume AI or fire stale input halfway through a tick. Death blocks player movement, turning and firing; lethal damage stops the remaining entity pass.
  • Packed entity metadata remains binary-compatible. Only flags == 0 enables legacy texture-based archetypes; any nonzero flags explicitly encode the low type byte, including Prop. Health uses one byte: 1..255, zero = legacy 100 HP. The C++ setter clamps before narrowing, and the inspector exposes 1..255.
  • IDE source writes use a unique sibling temporary file, checked full write, flush, close and atomic replacement. Failure preserves disk contents and dirty buffer. Loading publishes a new document only after a complete valid text read. File switches and builds stop on failed autosave. Saves target the file actually opened, not a newly resolved fallback path.
  • Compiler output is drained even with the IDE hidden. Process startup uses the system command processor with quoting for project paths containing spaces.

Verification

Windows commands:

build.bat
editor\build_editor.bat
python tools\test_regressions.py

Results: editor 112/112, ASM 148/148. The suite also checks live x86/x64 preview IPC, DX11 pixel equality, cinematic trace equality and unchanged project assets. New tests exercise production gameplay and IDE procedures, including failed atomic replacement under a Windows sharing lock and hidden compiler output exceeding pipe capacity. Interactive UI playtesting remains separate.

Preparation for stage 2 — integration with current main

Integrated server history through 2bac213 with stage-1 commit f42679f in an isolated worktree, preserving both histories and the original working directory. Retained map v5, preview IPC v2, authored lighting, three weapons, explosive barrels, modal dialogues, peaceful mode and the updated studio UI.

Compatibility adjustments:

  • Pause and modal dialogue join the shared gameplay gate, including direct calls to gameplay procedures. Blocked player updates cannot activate portal triggers.
  • Knife/pistol/shotgun keep their range, damage, ammunition and reserved light behavior. World-space half-widths are respectively 24/16/32 times sprite scale; the shotgun remains a single-target wider hit test, not three separate rays.
  • Barrel texture fallback is enabled only with zero flags in both editor and ASM. Explicit Prop overrides barrel artwork and cannot explode.
  • A new explosion regression reproduced access violation 0xC0000005: the AOE loop kept a sprite pointer in EDX and destroyed it with CDQ. The pointer now stays in EBX. A nearby guard receives blast damage without crashing.
  • IDE tab navigation, API catalog and docked view retain stage-1 safe writes.

Verification: both production builds succeeded; editor 137/137, ASM 191/191. All 1024 baked RGB cells match between C++ and ASM; classic and lit preview frames match direct ASM rendering, including the x86/x64/DX11 path. Project assets remain byte-for-byte unchanged. The original working directory still has only its pre-existing editor/imgui.ini modification. No separate interactive UI playtest was performed during this preparation.

Stage 2 implementation contract

  1. Separate message/input polling, fixed simulation ticks and drawing/presentation. Use QPC and a 60 Hz accumulator with bounded catch-up. Consume edge-triggered commands once, not once per catch-up tick. Reset timing debt after pause/focus stalls; advance world timers/lights only on the appropriate simulation tick.
  2. Keep movement fractions in runtime state without changing serialized raw-unit coordinates. Cancel opposing inputs, normalize combined forward/strafe input, and use symmetric signed accumulation. Clear fractions on load/teleport/restart.
  3. Check the full collision body on each axis and preserve wall sliding; prevent corner clipping and occupied-door closure. Keep portals intentionally walkable.
  4. Centralize Playing/Cutscene/Transition/Dead/Paused/Dialog update rules, including Game_OnTick, dynamic lights, fade, toasts and modal dismissal. Render routines must not advance those timers or perform input-driven state changes.
  5. Separate cinematic camera pose from gameplay/player position. Define END/skip handoff explicitly and validate the restored gameplay collision body, including paths crossing walls. Keep C++ preview and ASM cinematic traces aligned.
  6. Track the current room path and its loaded entry spawn separately from startup map. Restart restores that room and entry pose, resets gameplay transients and fractional movement, and preserves project mode/settings. Failed reload must not partially reset the current room. Make death-R and pause restart share it.

Acceptance checks: identical simulation after 60 ticks under different render schedules; single consumption of edge inputs during catch-up; symmetric movement for all quadrants and equal cardinal/diagonal speed; collision corner/slide cases; no simulation advancement during pause/dialog; no pickups/AI along camera paths; safe cinematic handoff; restart after a room transition; failed restart rollback; unchanged v1..v5 loading, lighting bake parity and IPC v2 framebuffer parity.

Stage 2 — predictable simulation

Implemented without changing EBLN v1..v5, sprite coordinates or preview IPC v2:

  • include/engine_simulation.inc owns QPC scheduling and Engine_Tick. The accumulator stores elapsed counts multiplied by 60, avoiding rounded tick periods. Each pump runs at most eight ticks; stalls over 250 ms, backwards counter samples, pause/resume, focus changes, modal boundaries and successful restarts discard timing debt. Drawing runs independently, without interpolation. Under overload the engine drops debt rather than fast-forwarding.
  • PollInput records held movement and pending command edges. An edge survives render-only frames and is consumed after one simulation tick, including blocked ticks. On focus regain physical latches are resampled and new edges discarded; movement may resume while held, but actions require release and a fresh press. F12 remains a presentation-only screenshot request.
  • Movement uses signed 16.16 runtime remainders with truncation toward zero. Opposing axes cancel before rounding; combined forward/strafe uses 46341/65536 normalization. Collision checks the axis-aligned body against every overlapped cell, stepping one world unit per axis with wall sliding (maximum 64 substeps per tick). Blocked axes discard their remainder. Portals remain walkable; open doors cannot close over the body. Fractions reset on room load and camera handoff.
  • State precedence is Unfocused, Paused, Dialog, Transition, Dead, Cutscene, Playing. Playing updates player, entities, combat, Game_OnTick, lights and toast lifetime in that order, rechecking the shared gameplay gate between subsystems and entities. Cutscene updates camera/track and lights only; transition advances only its fade/load sequencer. Death accepts R; pause accepts menu commands; dialog accepts E/Space, Enter or attack dismissal. Other world timers freeze in those states. END/skip and transition completion never resume gameplay partway through the same tick.
  • Engine_Frame and HUD/overlay procedures do not update gameplay timers, dismiss dialogs or load rooms. UpdateScreenFade is the tick sequencer; ApplyScreenFade is only a framebuffer operation.
  • Cinematics own csCameraX/Y/Angle; rendering selects viewX/Y/Angle without moving the player's body. Wall samples and camera-attached flashlight use the cinematic view, matching the editor preview. Paths may cross walls. END/Tab transfers position and angle only if the entire body is walkable at the camera pose; otherwise the previous safe player pose remains. No path search or collision correction is attempted. Existing sound/overlay cleanup remains.
  • RestartCurrentRoom reloads the remembered room path with its actual entry spawn (including transition overrides), then resets entities, health/ammo, keys/score, combat flashes/cooldown, dialog/toast, fades, pending transition and movement fractions. Authored lighting and original room data are reloaded; game genre, weapon/HUD configuration and selected weapon remain unchanged. Procedural startup fallback restarts procedurally. Failed file reload leaves the active room and gameplay intact; it does not revive a dead player or resume a paused game. Death R and pause-menu item 4 use this same procedure.

Verification commands are the same as stage 1. New production ASM checks cover different render schedules, catch-up limits and 64-bit QPC arithmetic, pending edges and focus regain, pause/modal timing, all 512 movement directions, diagonal normalization, sprint, body corners/sliding/doors, high-speed wall collision, wall-crossing cutscene skip/END, lit camera framebuffer parity, both restart entry points and failed-reload rollback. The runner's lighting/cutscene labels now follow the actual ASM call order rather than mislabelling failures. Interactive keyboard and UI playtesting remains separate; no UI redesign is included in this stage.

Verification (2026-10-31): production engine and editor builds succeeded; editor 137/137, ASM 288/288. All 320 projection angles, frame-by-frame C++/ASM cinematic traces and 1024 baked RGB cells agree. Classic/lit IPC v2 frames match direct ASM rendering through the x86/x64/DX11 path. Project assets remain byte-for-byte unchanged. No manual interactive playtest was performed.

Remaining stages

  1. Honest editor/gameplay: complete undo, keys/switches and supported interactions; UE5-inspired editor/runtime UI redesign.
  2. Project lifecycle and validated, safe standalone export.
  3. Measurements and targeted renderer/editor optimizations.
  4. Finished multi-room demo game and release verification.
  5. Data-driven extensions and subsequent rendering features.

Stages 1 and 2 do not claim that all audit findings are fixed: AI visibility/melee through walls, full undo and exporter safety remain assigned to subsequent stages. Existing map and texture assets are not migrated or rewritten.