EBLAN_ENGINE/tests/simulation_engine_checks.inc
Игорь Федосеев 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

471 lines
17 KiB
PHP

.data
scheduleSnapshot dd 8 dup(0)
symmetryOK dd 1
speedOK dd 1
COMM cinematicReference:BYTE:FRAME_BUFFER_SIZE
.code
ResetSimulationFixture PROC
call ResetCombatFixture
mov spriteCount, 0
mov playerX, 800
mov playerY, 800
mov playerMoveSpeed, 3
mov playerRunSpeed, 6
mov playerRadius, 16
mov keyAttack, 0
mov keyForward, 1
mov simulationTicks, 0
mov lightingTick, 0
mov gameToastTimer, 100
mov mapErrorFrames, 0
mov DWORD PTR qpcFrequency, 60000
mov DWORD PTR qpcFrequency+4, 0
mov DWORD PTR tickAccumulator, 0
mov DWORD PTR tickAccumulator+4, 0
ret
ResetSimulationFixture ENDP
MoveSixtyTicks PROC uses esi
mov esi, 60
move_sixty_loop:
call UpdatePlayer
dec esi
jnz move_sixty_loop
ret
MoveSixtyTicks ENDP
RunSimulationChecks PROC uses ebx esi edi
call ResetSimulationFixture
mov playerAngle, 32
mov esi, 60
schedule_one:
invoke AdvanceSimulation, 1000, 0
call Render3DScene
dec esi
jnz schedule_one
mov eax, playerX
mov scheduleSnapshot, eax
mov eax, playerY
mov scheduleSnapshot[4], eax
mov eax, playerFractionX
mov scheduleSnapshot[8], eax
mov eax, playerFractionY
mov scheduleSnapshot[12], eax
CHECK simulationTicks, 60 ; QPC accumulator executes exactly sixty ticks per second
CHECK lightingTick, 60 ; lights advance once per simulation tick
CHECK gameToastTimer, 40 ; toast lifetime follows simulation rather than renders
call ResetSimulationFixture
mov playerAngle, 32
mov esi, 20
schedule_three:
invoke AdvanceSimulation, 3000, 0
call Render3DScene
call Render3DScene
dec esi
jnz schedule_three
mov ebx, scheduleSnapshot
CHECK playerX, ebx ; batched ticks preserve X under a different render schedule
mov ebx, scheduleSnapshot[4]
CHECK playerY, ebx ; batched ticks preserve Y under a different render schedule
mov ebx, scheduleSnapshot[8]
CHECK playerFractionX, ebx ; batched ticks preserve fractional X
mov ebx, scheduleSnapshot[12]
CHECK playerFractionY, ebx ; batched ticks preserve fractional Y
CHECK simulationTicks, 60 ; catch-up does not change total simulated time
CHECK gameToastTimer, 40 ; repeated drawing does not age toasts
invoke AdvanceSimulation, 60000, 0
CHECK simulationTicks, 60 ; long stalls discard timing debt instead of fast-forwarding
invoke AdvanceSimulation, 15000, 0
CHECK simulationTicks, 68 ; catch-up is bounded to eight ticks
CHECK DWORD PTR tickAccumulator, 0 ; overload discards excess catch-up debt
call ResetSimulationFixture
mov keyForward, 0
mov keyHud, 1
mov hudStyle, HUD_STYLE_CLEAN
invoke AdvanceSimulation, 100, 0
movzx eax, keyHud
CHECK eax, 1 ; pending edge survives a frame with no simulation tick
invoke AdvanceSimulation, 2900, 0
CHECK hudStyle, HUD_STYLE_DEBUG ; edge toggles exactly once across three catch-up ticks
movzx eax, keyHud
CHECK eax, 0 ; simulation consumes edge after its first tick
mov keyPause, 1
invoke AdvanceSimulation, 3000, 0
CHECK isGamePaused, 1 ; pause edge is applied inside the simulation
CHECK simulationTicks, 4 ; pause boundary discards remaining catch-up ticks
CHECK DWORD PTR tickAccumulator, 0 ; pause clears timing debt
mov ebx, lightingTick
mov keyAttack, 1
call Engine_Tick
CHECK lightingTick, ebx ; pause freezes dynamic lights
CHECK playerAmmo, 50 ; paused tick consumes attack without firing
mov keyMenu1, 1
call Engine_Tick
CHECK isGamePaused, 0 ; menu resume returns to gameplay without advancing world
mov clockReset, 0
mov gameDialogActive, 1
mov keyDismiss, 1
call Engine_Tick
CHECK gameDialogActive, 0 ; Enter dismisses dialogue on a simulation tick
CHECK lightingTick, ebx ; dialogue dismissal does not advance world timers
CHECK gameToastTimer, 97 ; pause and modal ticks preserve toast lifetime
call ResetSimulationFixture
call MoveSixtyTicks
CHECK playerX, 980 ; cardinal speed remains three world units per tick
CHECK playerY, 800 ; cardinal movement has no orthogonal drift
call ResetSimulationFixture
mov keyStrafeRight, 1
call MoveSixtyTicks
CHECK playerX, 927 ; diagonal input is normalized rather than sqrt-two faster
CHECK playerY, 927 ; diagonal normalization is symmetric on both axes
call ResetSimulationFixture
mov playerAngle, 32
call MoveSixtyTicks
mov ebx, playerX
sub ebx, 800
mov scheduleSnapshot, ebx
mov ebx, playerY
sub ebx, 800
mov scheduleSnapshot[4], ebx
call ResetSimulationFixture
mov playerAngle, 288
call MoveSixtyTicks
mov ebx, 800
sub ebx, scheduleSnapshot
CHECK playerX, ebx ; opposite angle has exactly symmetric accumulated X
mov ebx, 800
sub ebx, scheduleSnapshot[4]
CHECK playerY, ebx ; opposite angle has exactly symmetric accumulated Y
call ResetSimulationFixture
mov keyBackward, 1
mov keyStrafeLeft, 1
mov keyStrafeRight, 1
call MoveSixtyTicks
CHECK playerX, 800 ; opposing forward inputs cancel before rounding
CHECK playerY, 800 ; opposing strafe inputs cancel before rounding
call ResetSimulationFixture
mov playerX, 175
mov playerY, 184
mov byte ptr [gameMap+3*MAP_W+3], CELL_BRICK
call UpdatePlayer
CHECK playerX, 175 ; collision checks leading corners beyond the center cell
CHECK playerFractionX, 0 ; blocked axis discards its fractional remainder
mov keyStrafeLeft, 1
call UpdatePlayer
CHECK playerY, 182 ; blocked X still permits sliding on free Y
invoke CanOccupyPlayer, 8, 160
CHECK eax, 0 ; collision body cannot extend outside the map
mov byte ptr [gameMap+3*MAP_W+3], CELL_PORTAL
invoke CanOccupyPlayer, 184, 184
CHECK eax, 1 ; portal cells remain walkable for the complete collision body
mov byte ptr [gameMap+2*MAP_W+3], CELL_DOOR_OPEN
mov playerX, 180
mov playerY, 160
mov playerAngle, 0
call InteractDoor
movzx eax, byte ptr [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR_OPEN ; door cannot close over an overlapping player body
mov playerX, 160
call InteractDoor
movzx eax, byte ptr [gameMap+2*MAP_W+3]
CHECK eax, CELL_DOOR ; unoccupied door still closes normally
call ResetSimulationFixture
mov isCutsceneActive, 1
mov csCameraX, 184
mov csCameraY, 184
mov csCameraAngle, 128
mov byte ptr [gameMap+3*MAP_W+3], CELL_BRICK
call SelectRenderCamera
CHECK viewX, 184 ; renderer selects cinematic camera without replacing player pose
CHECK playerX, 800 ; camera ownership leaves gameplay position untouched
call StopCutscene
CHECK playerX, 800 ; unsafe cinematic handoff retains last safe gameplay position
CHECK playerAngle, 0 ; unsafe cinematic handoff also retains gameplay orientation
mov isCutsceneActive, 1
mov csCameraX, 864
mov csCameraY, 800
call StopCutscene
CHECK playerX, 864 ; walkable cinematic handoff transfers camera position
CHECK playerAngle, 128 ; walkable cinematic handoff transfers orientation
; Render routines must not dismiss, age or transition anything.
mov gameToastTimer, 12
mov gameDialogActive, 1
mov keyAction, 1
mov fadeState, 1
mov fadeLevel, 4
call RenderGameToast
call RenderGameDialog
call ApplyScreenFade
call ApplyScreenFade
CHECK gameToastTimer, 12 ; drawing toast never decrements its timer
CHECK gameDialogActive, 1 ; drawing modal never consumes input or closes it
CHECK fadeLevel, 4 ; drawing fade never advances transition level
CHECK fadeState, 1 ; drawing fade never switches room state
call ResetSimulationFixture
invoke LoadSublevelBinary, OFFSET subPath, 4, 6, 64
mov playerX, 600
mov playerHealth, 1
mov playerAmmo, 2
mov playerKeys, 3
mov playerScore, 700
mov isPlayerDead, 1
mov gameDialogActive, 1
mov playerFractionX, 12345
mov gameMode, GAME_MODE_ADVENTURE
mov enableWeapons, 0
call RestartCurrentRoom
CHECK eax, 1 ; current-room restart succeeds after a sublevel transition
CHECK playerX, 288 ; restart retains actual transition entry X rather than file spawn
CHECK playerY, 416 ; restart retains actual transition entry Y
CHECK playerAngle, 64 ; restart retains actual transition entry angle
CHECK spriteCount, 1 ; restart reloads current sublevel rather than startup room
CHECK playerHealth, 100 ; restart resets health after successful reload
CHECK playerAmmo, 50 ; restart resets ammunition after successful reload
CHECK playerKeys, 0 ; restart clears acquired keys
CHECK playerScore, 0 ; restart clears room score
CHECK playerFractionX, 0 ; restart clears fractional movement
CHECK gameDialogActive, 0 ; restart clears old modal dialogue
CHECK gameMode, GAME_MODE_ADVENTURE ; restart preserves configured game genre
CHECK enableWeapons, 0 ; restart preserves weapon configuration
; Replace only remembered path in this isolated fixture to force load failure.
mov DWORD PTR currentMapPath, 73696D61h
mov DWORD PTR currentMapPath+4, 676E6973h
mov DWORD PTR currentMapPath+8, 7461642Eh
mov BYTE PTR currentMapPath+12, 0
mov playerHealth, 17
mov playerAmmo, 9
mov playerFractionX, 222
mov isPlayerDead, 1
call RestartCurrentRoom
CHECK eax, 0 ; failed restart reports failure without procedural fallback
CHECK playerHealth, 17 ; failed restart preserves health transactionally
CHECK playerAmmo, 9 ; failed restart preserves ammunition transactionally
CHECK playerFractionX, 222 ; failed restart preserves movement remainder
CHECK isPlayerDead, 1 ; failed restart does not revive player in old room
CHECK spriteCount, 1 ; failed restart preserves active room entities
CHECK playerX, 288 ; failed restart preserves current camera pose
; Keys pressed while another application owns focus must not become commands.
call ResetSimulationFixture
mov hEngineWnd, 1
mov testFocus, 1
mov testKeys[VK_KEY_E*2], 0
mov testKeys[VK_KEY_CONTROL*2], 0
mov testKeys[VK_KEY_SPACE*2], 0
call PollInput
call ConsumeInputEdges
mov testFocus, 0
call PollInput
mov testKeys[VK_KEY_E*2], 8000h
mov testKeys[VK_KEY_CONTROL*2], 8000h
mov testFocus, 1
call PollInput
movzx eax, keyAction
CHECK eax, 0 ; focus regain suppresses action pressed while unfocused
movzx eax, keyAttack
CHECK eax, 0 ; focus regain suppresses attack pressed while unfocused
call ConsumeInputEdges
call PollInput
movzx eax, keyAttack
CHECK eax, 0 ; resynchronized held attack does not fire on the next poll
mov testKeys[VK_KEY_CONTROL*2], 0
call PollInput
mov testKeys[VK_KEY_CONTROL*2], 8000h
call PollInput
movzx eax, keyAttack
CHECK eax, 1 ; releasing and pressing again rearms attack after focus regain
call ResetSimulationFixture
invoke ShowGameDialog, 0, 0
CHECK clockReset, 1 ; opening a modal discards outstanding timing debt
mov clockReset, 0
call CloseGameDialog
CHECK clockReset, 1 ; closing a modal through the API discards timing debt
call ResetSimulationFixture
mov lightingMode, LIGHTING_MODE_DYNAMIC
mov isFlashlightOn, 1
mov isCutsceneActive, 1
mov csCameraX, 160
mov csCameraY, 160
mov csCameraAngle, 0
invoke SamplePointLighting, 224, 160, 0, 0
mov eax, lightRed
mov scheduleSnapshot, eax
mov isCutsceneActive, 0
mov playerX, 160
mov playerY, 160
invoke SamplePointLighting, 224, 160, 0, 0
mov ebx, scheduleSnapshot
CHECK lightRed, ebx ; cinematic flashlight matches preview camera rather than safe player pose
; Exhaust all half-turn pairs, including signed fractional remainders.
mov edi, 0
mov symmetryOK, 1
mov speedOK, 1
symmetry_angle:
call ResetSimulationFixture
mov playerAngle, edi
call MoveSixtyTicks
mov eax, playerX
sub eax, 800
mov scheduleSnapshot, eax
imul eax, eax
mov ebx, playerY
sub ebx, 800
mov scheduleSnapshot[4], ebx
imul ebx, ebx
add eax, ebx
cmp eax, 178*178
jb speed_mismatch
cmp eax, 181*181
jbe speed_valid
speed_mismatch:
mov speedOK, 0
speed_valid:
mov eax, playerFractionX
mov scheduleSnapshot[8], eax
mov eax, playerFractionY
mov scheduleSnapshot[12], eax
call ResetSimulationFixture
lea eax, [edi+256]
mov playerAngle, eax
call MoveSixtyTicks
mov eax, 800
sub eax, scheduleSnapshot
cmp playerX, eax
jne symmetry_mismatch
mov eax, 800
sub eax, scheduleSnapshot[4]
cmp playerY, eax
jne symmetry_mismatch
mov eax, scheduleSnapshot[8]
neg eax
cmp playerFractionX, eax
jne symmetry_mismatch
mov eax, scheduleSnapshot[12]
neg eax
cmp playerFractionY, eax
je symmetry_next
symmetry_mismatch:
mov symmetryOK, 0
symmetry_next:
inc edi
cmp edi, 256
jb symmetry_angle
CHECK symmetryOK, 1 ; all 512 directions retain opposite-position and fraction symmetry
CHECK speedOK, 1 ; all angular speeds remain within integer projection tolerance
call ResetSimulationFixture
mov playerX, 160
mov playerY, 160
mov byte ptr [gameMap+2*MAP_W+4], CELL_BRICK
mov cutsceneCount, 2
mov [cutsceneEvents].CutsceneEvent.tickTimestamp, 0
mov [cutsceneEvents].CutsceneEvent.opCode, OP_MOVE_CAM_TO
mov [cutsceneEvents].CutsceneEvent.paramA, 416
mov [cutsceneEvents].CutsceneEvent.paramB, 160
mov [cutsceneEvents].CutsceneEvent.dataId, 4
mov [cutsceneEvents+16].CutsceneEvent.tickTimestamp, 5
mov [cutsceneEvents+16].CutsceneEvent.opCode, OP_END
call StartCutscene
call UpdatePlayer
call UpdatePlayer
call UpdatePlayer
CHECK csCameraX, 288 ; cinematic path can cross a wall independently of the body
CHECK playerX, 160 ; wall-crossing camera path never teleports gameplay body
mov keySkipScene, 1
call UpdatePlayer
CHECK playerX, 160 ; skipping inside a wall retains safe player pose
mov keySkipScene, 0
call StartCutscene
mov esi, 6
wall_scene_tick:
call UpdatePlayer
dec esi
jnz wall_scene_tick
CHECK isCutsceneActive, 0 ; wall-crossing cinematic reaches END normally
CHECK playerX, 416 ; END hands off safe endpoint across the wall
call ResetSimulationFixture
mov playerMoveSpeed, 64
mov playerX, 175
mov playerY, 160
mov byte ptr [gameMap+2*MAP_W+3], CELL_BRICK
call UpdatePlayer
CHECK playerX, 175 ; high-speed movement cannot tunnel through a wall
call ResetSimulationFixture
mov keyRun, 1
call MoveSixtyTicks
CHECK playerX, 1160 ; sprint retains six world units per fixed tick
mov keyForward, 0
mov keyTurnLeft, 1
mov keyTurnRight, 1
call UpdatePlayer
CHECK playerAngle, 0 ; opposing turn inputs cancel
call ResetSimulationFixture
invoke LoadSublevelBinary, OFFSET subPath, 4, 6, 64
mov isPlayerDead, 1
mov keyRestart, 1
call Engine_Tick
CHECK isPlayerDead, 0 ; death R uses the shared current-room restart
CHECK playerX, 288 ; death restart restores transition entry pose
mov clockReset, 0
mov isGamePaused, 1
mov playerHealth, 7
mov keyMenu4, 1
call Engine_Tick
CHECK isGamePaused, 0 ; pause-menu restart resumes only after successful reload
CHECK playerHealth, 100 ; pause-menu restart uses the same transient reset
CHECK playerY, 416 ; pause-menu restart keeps the current room entry
call ResetSimulationFixture
; Frequency > 2^32 exercises carries in accumulator and elapsed multiplication.
mov DWORD PTR qpcFrequency, 8
mov DWORD PTR qpcFrequency+4, 1
invoke AdvanceSimulation, 71582788, 0
CHECK simulationTicks, 0 ; QPC scheduler keeps a fractional high-frequency tick
invoke AdvanceSimulation, 2, 0
CHECK simulationTicks, 1 ; QPC scheduler carries elapsed counts across DWORD boundaries
CHECK DWORD PTR tickAccumulator, 96 ; QPC scheduler retains exact high-frequency remainder
mov clockReset, 1
invoke AdvanceSimulation, 1000, 0
CHECK DWORD PTR tickAccumulator, 0 ; explicit clock boundary discards existing fractional debt
invoke AdvanceSimulation, -1, -1
CHECK simulationTicks, 1 ; backwards counter samples cannot fast-forward simulation
call ResetSimulationFixture
call InitLightingSystem
mov lightingMode, LIGHTING_MODE_DYNAMIC
mov isCutsceneActive, 1
mov csCameraX, 160
mov csCameraY, 160
mov csCameraAngle, 32
mov byte ptr [gameMap+3*MAP_W+6], CELL_BRICK
call Render3DScene
mov esi, OFFSET screenBuffer
mov edi, OFFSET cinematicReference
mov ecx, FRAME_BUFFER_SIZE/4
rep movsd
mov isCutsceneActive, 0
mov playerX, 160
mov playerY, 160
mov playerAngle, 32
call Render3DScene
mov esi, OFFSET screenBuffer
mov edi, OFFSET cinematicReference
mov ecx, FRAME_BUFFER_SIZE/4
repe cmpsd
sete al
movzx eax, al
CHECK eax, 1 ; cinematic lit framebuffer equals camera-only preview with separate gameplay body
ret
RunSimulationChecks ENDP