EBLAN_ENGINE/include/engine_cutscene.inc
Artvasil09 a1d1baa869 feat: complete engine and editor subsystems (Stages 1-5 from TODO_ARTEM)
- Stage 1: Eliminate fish-eye distortion with precomputed tangential ray offset table and fractional DDA depth
- Stage 2: Full asset management in EblanStudio (stb_image/stb_image_write, 128x128 BGRA texture packaging, WAV sound manager)
- Stage 3: Multi-level room streaming, LevelTransition/CELL_PORTAL trigger system, 16-frame screen fade to black
- Stage 4: 60Hz cutscene engine in pure x86 ASM (v4 binary track, waypoint camera motion, shortest-arc pan, CP1251 Russian subtitles, letterbox overlay, audio)
- Stage 5: EblanStudio cutscene timeline (F6) and windowless Live ASM Viewport over shared-memory IPC directly rendered into DX11
- Tooling & Tests: comprehensive regression test suite (94 editor + 107 engine tests), IPC tests, and cutscene demo
2026-10-22 16:24:22 +03:00

583 lines
14 KiB
PHP

; Timed 60 Hz cutscene track. Strings are bounded CP1251 data, never pointers.
.code
ValidateSoundFilename PROC uses ebx ecx esi, namePtr:DWORD
mov esi, namePtr
xor ecx, ecx
cs_sound_character:
mov al, [esi+ecx]
test al, al
jz cs_sound_end
cmp al, 32
jb cs_sound_invalid
cmp al, 127
jae cs_sound_invalid
cmp al, 92
je cs_sound_invalid
cmp al, '/'
je cs_sound_invalid
cmp al, ':'
je cs_sound_invalid
cmp al, '*'
je cs_sound_invalid
cmp al, '?'
je cs_sound_invalid
cmp al, 34
je cs_sound_invalid
cmp al, '<'
je cs_sound_invalid
cmp al, '>'
je cs_sound_invalid
cmp al, '|'
je cs_sound_invalid
inc ecx
cmp ecx, 64
jb cs_sound_character
cs_sound_invalid:
xor eax, eax
ret
cs_sound_end:
cmp ecx, 5
jb cs_sound_invalid
mov eax, [esi+ecx-4]
or eax, 20202000h
cmp eax, 7661772Eh
jne cs_sound_invalid
mov eax, 1
ret
ValidateSoundFilename ENDP
ValidateStagedCutscene PROC uses ebx ecx edx esi edi, eventCount:DWORD, stringCount:DWORD, flags:DWORD, cells:DWORD, transitions:DWORD
LOCAL previous:DWORD, eventIndex:DWORD
cmp eventCount, MAX_CUTSCENE_EVENTS
ja csv_invalid
cmp stringCount, MAX_CUTSCENE_STRINGS
ja csv_invalid
mov eax, flags
and eax, 0FFFFFFFEh
jnz csv_invalid
mov esi, OFFSET stagedCutsceneStrings
mov edx, stringCount
csv_next_string:
test edx, edx
jz csv_events
xor ecx, ecx
csv_string_char:
mov al, [esi+ecx]
test al, al
jz csv_string_end
cmp al, 10
je csv_string_advance
cmp al, 32
jb csv_invalid
csv_string_advance:
inc ecx
cmp ecx, CUTSCENE_STRING_BYTES
jb csv_string_char
jmp csv_invalid
csv_string_end:
add esi, CUTSCENE_STRING_BYTES
dec edx
jmp csv_next_string
csv_events:
mov esi, OFFSET stagedCutsceneEvents
mov previous, 0
mov eventIndex, 0
csv_event:
mov eax, eventIndex
cmp eax, eventCount
jae csv_finished
movzx eax, [esi].CutsceneEvent.tickTimestamp
cmp eax, previous
jb csv_invalid
mov previous, eax
movzx eax, [esi].CutsceneEvent.opCode
cmp eax, OP_CHANGE_SUBLEVEL
ja csv_invalid
movzx edx, [esi].CutsceneEvent.param8
cmp eax, OP_PLAY_SOUND
je csv_sound_flags
test edx, edx
jnz csv_invalid
jmp csv_dispatch
csv_sound_flags:
cmp edx, 1
ja csv_invalid
csv_dispatch:
cmp eax, OP_END
je csv_end
cmp eax, OP_WAIT
je csv_wait
cmp eax, OP_MOVE_CAM_TO
je csv_move
cmp eax, OP_PAN_CAM_TO
je csv_pan
cmp eax, OP_FADE_SCREEN
je csv_fade
cmp eax, OP_SHOW_SUBTITLE
je csv_text
cmp eax, OP_PLAY_SOUND
je csv_sound
cmp eax, OP_CHANGE_SUBLEVEL
je csv_transition
jmp csv_next
csv_end:
mov eax, eventIndex
inc eax
cmp eax, eventCount
jne csv_invalid
jmp csv_next
csv_wait:
mov eax, [esi].CutsceneEvent.paramA
cmp eax, CUTSCENE_MAX_DURATION
ja csv_invalid
jmp csv_next
csv_move:
mov eax, [esi].CutsceneEvent.paramA
cmp eax, MAP_W*CELL_SIZE
jae csv_invalid
mov ebx, [esi].CutsceneEvent.paramB
cmp ebx, MAP_H*CELL_SIZE
jae csv_invalid
shr eax, CELL_SHIFT
shr ebx, CELL_SHIFT
shl ebx, 5
add ebx, eax
add ebx, cells
mov al, [ebx]
cmp al, CELL_EMPTY
je csv_move_duration
cmp al, CELL_DOOR_OPEN
je csv_move_duration
cmp al, CELL_PORTAL
jne csv_invalid
csv_move_duration:
mov eax, [esi].CutsceneEvent.dataId
cmp eax, CUTSCENE_MAX_DURATION
ja csv_invalid
jmp csv_next
csv_pan:
cmp [esi].CutsceneEvent.paramA, ANGLE_MASK
ja csv_invalid
jmp csv_duration
csv_fade:
cmp [esi].CutsceneEvent.paramA, 16
ja csv_invalid
jmp csv_duration
csv_text:
mov eax, [esi].CutsceneEvent.paramA
cmp eax, stringCount
jae csv_invalid
csv_duration:
cmp [esi].CutsceneEvent.paramB, CUTSCENE_MAX_DURATION
ja csv_invalid
jmp csv_next
csv_sound:
mov eax, [esi].CutsceneEvent.paramA
cmp eax, -1
je csv_next
cmp eax, stringCount
jae csv_invalid
imul eax, CUTSCENE_STRING_BYTES
add eax, OFFSET stagedCutsceneStrings
invoke ValidateSoundFilename, eax
test eax, eax
jz csv_invalid
jmp csv_next
csv_transition:
mov eax, [esi].CutsceneEvent.paramA
cmp eax, transitions
jae csv_invalid
csv_next:
movzx eax, [esi].CutsceneEvent.opCode
xor ebx, ebx
cmp eax, OP_MOVE_CAM_TO
jne csv_span_not_move
mov ebx, [esi].CutsceneEvent.dataId
jmp csv_check_span
csv_span_not_move:
cmp eax, OP_WAIT
jne csv_span_not_wait
mov ebx, [esi].CutsceneEvent.paramA
jmp csv_check_span
csv_span_not_wait:
cmp eax, OP_PAN_CAM_TO
je csv_span_param_b
cmp eax, OP_FADE_SCREEN
je csv_span_param_b
cmp eax, OP_SHOW_SUBTITLE
jne csv_check_span
csv_span_param_b:
mov ebx, [esi].CutsceneEvent.paramB
csv_check_span:
movzx eax, [esi].CutsceneEvent.tickTimestamp
add eax, ebx
cmp eax, 65535
ja csv_invalid
add esi, sizeof CutsceneEvent
inc eventIndex
jmp csv_event
csv_finished:
cmp eventCount, 0
je csv_valid
cmp BYTE PTR [esi-sizeof CutsceneEvent+CutsceneEvent.opCode], OP_END
jne csv_invalid
csv_valid:
mov eax, 1
ret
csv_invalid:
xor eax, eax
ret
ValidateStagedCutscene ENDP
StopCutscene PROC uses eax ebx
mov isCutsceneActive, 0
mov cutscenePending, 0
mov cutsceneSubtitle, -1
mov cutsceneSubtitleUntil, 0
mov cutsceneFade, 0
mov csMoveDuration, 0
mov csAngleDuration, 0
mov csFadeDuration, 0
mov cutsceneWait, 0
mov eax, playerY
sar eax, CELL_SHIFT
shl eax, 5
mov ebx, playerX
sar ebx, CELL_SHIFT
add eax, ebx
mov portalLatchCell, eax
invoke PlaySoundA, 0, 0, 0
ret
StopCutscene ENDP
StartCutscene PROC
mov cutscenePending, 0
cmp cutsceneCount, 0
je cs_start_empty
call StopCutscene
mov cutsceneTick, 0
mov cutsceneIndex, 0
mov isCutsceneActive, 1
mov eax, 1
ret
cs_start_empty:
xor eax, eax
ret
StartCutscene ENDP
UpdateCutscene PROC uses ebx ecx edx esi edi
LOCAL transitionFade:DWORD
cmp isCutsceneActive, 0
je cs_update_done
cmp csMoveDuration, 0
je cs_step_angle
inc csMoveElapsed
mov eax, csMoveToX
sub eax, csMoveFromX
imul csMoveElapsed
idiv csMoveDuration
add eax, csMoveFromX
mov playerX, eax
mov eax, csMoveToY
sub eax, csMoveFromY
imul csMoveElapsed
idiv csMoveDuration
add eax, csMoveFromY
mov playerY, eax
mov eax, csMoveElapsed
cmp eax, csMoveDuration
jb cs_step_angle
mov csMoveDuration, 0
cs_step_angle:
cmp csAngleDuration, 0
je cs_step_fade
inc csAngleElapsed
mov eax, csAngleDelta
imul csAngleElapsed
idiv csAngleDuration
add eax, csAngleFrom
and eax, ANGLE_MASK
mov playerAngle, eax
mov eax, csAngleElapsed
cmp eax, csAngleDuration
jb cs_step_fade
mov csAngleDuration, 0
cs_step_fade:
cmp csFadeDuration, 0
je cs_subtitle_expiry
inc csFadeElapsed
mov eax, csFadeTo
sub eax, csFadeFrom
imul csFadeElapsed
idiv csFadeDuration
add eax, csFadeFrom
mov cutsceneFade, eax
mov eax, csFadeElapsed
cmp eax, csFadeDuration
jb cs_subtitle_expiry
mov csFadeDuration, 0
cs_subtitle_expiry:
cmp cutsceneSubtitleUntil, 0
je cs_step_wait
mov eax, cutsceneTick
cmp eax, cutsceneSubtitleUntil
jb cs_step_wait
mov cutsceneSubtitle, -1
mov cutsceneSubtitleUntil, 0
cs_step_wait:
cmp cutsceneWait, 0
je cs_dispatch
dec cutsceneWait
jnz cs_advance_tick
cs_dispatch:
mov eax, cutsceneIndex
cmp eax, cutsceneCount
jae cs_advance_tick
shl eax, 4
add eax, OFFSET cutsceneEvents
mov esi, eax
movzx eax, [esi].CutsceneEvent.tickTimestamp
cmp eax, cutsceneTick
ja cs_advance_tick
inc cutsceneIndex
movzx eax, [esi].CutsceneEvent.opCode
cmp eax, OP_END
je cs_end
cmp eax, OP_WAIT
je cs_wait
cmp eax, OP_MOVE_CAM_TO
je cs_move
cmp eax, OP_PAN_CAM_TO
je cs_pan
cmp eax, OP_SHOW_SUBTITLE
je cs_text
cmp eax, OP_HIDE_SUBTITLE
je cs_hide_text
cmp eax, OP_PLAY_SOUND
je cs_play_sound
cmp eax, OP_FADE_SCREEN
je cs_fade
cmp eax, OP_CHANGE_SUBLEVEL
je cs_change_level
jmp cs_end
cs_wait:
mov eax, [esi].CutsceneEvent.paramA
mov cutsceneWait, eax
test eax, eax
jnz cs_advance_tick
jmp cs_dispatch
cs_move:
mov eax, playerX
mov csMoveFromX, eax
mov eax, playerY
mov csMoveFromY, eax
mov eax, [esi].CutsceneEvent.paramA
mov csMoveToX, eax
mov eax, [esi].CutsceneEvent.paramB
mov csMoveToY, eax
mov eax, [esi].CutsceneEvent.dataId
mov csMoveDuration, eax
mov csMoveElapsed, 0
test eax, eax
jnz cs_dispatch
mov eax, csMoveToX
mov playerX, eax
mov eax, csMoveToY
mov playerY, eax
jmp cs_dispatch
cs_pan:
mov eax, playerAngle
mov csAngleFrom, eax
mov ebx, [esi].CutsceneEvent.paramA
sub ebx, eax
add ebx, 256
and ebx, ANGLE_MASK
sub ebx, 256
mov csAngleDelta, ebx
mov csAngleElapsed, 0
mov eax, [esi].CutsceneEvent.paramB
mov csAngleDuration, eax
test eax, eax
jnz cs_dispatch
mov eax, [esi].CutsceneEvent.paramA
mov playerAngle, eax
jmp cs_dispatch
cs_text:
mov eax, [esi].CutsceneEvent.paramA
mov cutsceneSubtitle, eax
mov eax, [esi].CutsceneEvent.paramB
test eax, eax
jz cs_text_expiry_ready
add eax, cutsceneTick
cs_text_expiry_ready:
mov cutsceneSubtitleUntil, eax
jmp cs_dispatch
cs_hide_text:
mov cutsceneSubtitle, -1
mov cutsceneSubtitleUntil, 0
jmp cs_dispatch
cs_play_sound:
mov eax, [esi].CutsceneEvent.paramA
cmp eax, -1
jne cs_sound_start
invoke PlaySoundA, 0, 0, 0
jmp cs_dispatch
cs_sound_start:
imul eax, CUTSCENE_STRING_BYTES
add eax, OFFSET cutsceneStrings
push esi
mov esi, OFFSET csSoundPrefix
mov edi, OFFSET csSoundPath
cs_copy_sound_prefix:
lodsb
stosb
test al, al
jnz cs_copy_sound_prefix
dec edi
pop esi
mov eax, [esi].CutsceneEvent.paramA
imul eax, CUTSCENE_STRING_BYTES
add eax, OFFSET cutsceneStrings
mov ebx, esi
mov esi, eax
cs_copy_sound_name:
lodsb
stosb
test al, al
jnz cs_copy_sound_name
movzx eax, [ebx].CutsceneEvent.param8
shl eax, 3
or eax, 00020003h ; SND_FILENAME | SND_ASYNC | SND_NODEFAULT (+ SND_LOOP)
invoke PlaySoundA, OFFSET csSoundPath, 0, eax
jmp cs_dispatch
cs_fade:
mov eax, cutsceneFade
mov csFadeFrom, eax
mov eax, [esi].CutsceneEvent.paramA
mov csFadeTo, eax
mov eax, [esi].CutsceneEvent.paramB
mov csFadeDuration, eax
mov csFadeElapsed, 0
test eax, eax
jnz cs_dispatch
mov eax, csFadeTo
mov cutsceneFade, eax
jmp cs_dispatch
cs_change_level:
mov eax, cutsceneFade
mov transitionFade, eax
mov ebx, [esi].CutsceneEvent.paramA
imul ebx, sizeof LevelTransition
add ebx, OFFSET transitionList
call StopCutscene
movzx eax, [ebx].LevelTransition.targetSpawnX
movzx ecx, [ebx].LevelTransition.targetSpawnY
movzx edx, [ebx].LevelTransition.targetAngle
movzx esi, [ebx].LevelTransition.fadeEffect
lea edi, [ebx].LevelTransition.targetMap
invoke SwitchSublevel, edi, eax, ecx, edx, esi
cmp fadeState, 1
jne cs_transition_ready
mov eax, transitionFade
cmp eax, fadeLevel
jbe cs_transition_ready
mov fadeLevel, eax ; continue from the cinematic fade without flashing bright
cs_transition_ready:
ret
cs_end:
call StopCutscene
ret
cs_advance_tick:
inc cutsceneTick
cs_update_done:
ret
UpdateCutscene ENDP
; Letterbox (320 / 2.35 -> 136-pixel picture) and up to three wrapped lines.
RenderCutsceneOverlay PROC uses ebx ecx edx esi edi
LOCAL row:DWORD, count:DWORD, pixelY:DWORD
invoke DrawRect, 0, 0, SCREEN_W, 52, COLOR_BLACK
invoke DrawRect, 0, 188, SCREEN_W, 52, COLOR_BLACK
invoke DrawString8x8, 232, 10, OFFSET strSkipScene, COLOR_GRAY
mov eax, cutsceneSubtitle
cmp eax, -1
je cs_overlay_done
cmp eax, cutsceneStringCount
jae cs_overlay_done
imul eax, CUTSCENE_STRING_BYTES
add eax, OFFSET cutsceneStrings
mov esi, eax
; Semitransparent subtitle panel over the bottom of the picture.
mov edi, OFFSET screenBuffer + 158*SCREEN_PITCH
mov ecx, 30*SCREEN_W
cs_subtitle_dim:
mov eax, [edi]
and eax, 00FEFEFEh
shr eax, 1
stosd
loop cs_subtitle_dim
mov row, 0
cs_subtitle_line:
mov edi, OFFSET csLineBuffer
mov count, 0
cs_subtitle_char:
lodsb
test al, al
jz cs_subtitle_string_end
cmp al, 10
je cs_subtitle_emit
stosb
inc count
cmp count, 38
jb cs_subtitle_char
jmp cs_subtitle_emit
cs_subtitle_string_end:
dec esi
cs_subtitle_emit:
mov byte ptr [edi], 0
mov eax, count
shl eax, 2
mov ebx, SCREEN_HALF_W
sub ebx, eax
mov eax, row
imul eax, 10
add eax, 160
invoke DrawString8x8, ebx, eax, OFFSET csLineBuffer, COLOR_WHITE
cmp byte ptr [esi], 0
je cs_overlay_done
inc row
cmp row, 3
jb cs_subtitle_line
cs_overlay_done:
ret
RenderCutsceneOverlay ENDP
ApplyCutsceneFade PROC uses ebx ecx edx esi edi
mov ebx, 16
sub ebx, cutsceneFade
cmp ebx, 16
jae cs_fade_done
mov edi, OFFSET screenBuffer
mov ecx, SCREEN_W*SCREEN_H
cs_fade_pixel:
mov eax, [edi]
movzx edx, al
imul edx, ebx
shr edx, 4
movzx eax, ah
imul eax, ebx
shr eax, 4
shl eax, 8
or edx, eax
mov eax, [edi]
shr eax, 16
imul eax, ebx
shr eax, 4
shl eax, 16
or eax, edx
stosd
loop cs_fade_pixel
cs_fade_done:
ret
ApplyCutsceneFade ENDP