; ============================================================================== ; EBLAN ENGINE - Map System & World Grid ; ============================================================================== .code ; Forward prototypes for map & sublevel transition procedures SwitchSublevel PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD LoadSublevelBinary PROTO :DWORD, :DWORD, :DWORD, :DWORD DoSublevelTransition PROTO ApplyScreenFade PROTO ValidateMapFilename PROTO :DWORD ValidateStagedCutscene PROTO :DWORD, :DWORD, :DWORD, :DWORD, :DWORD CheckPortalTriggers PROTO InitEntities PROTO ShowGameToast PROTO :DWORD, :DWORD, :DWORD BakeMapLighting PROTO InitLightingSystem PROTO ValidateLightingData PROTO :DWORD,:DWORD,:DWORD CommitLightingData PROTO :DWORD,:DWORD,:DWORD,:DWORD ResetMovementFractions PROTO CanOccupyPlayer PROTO :DWORD,:DWORD ; ------------------------------------------------------------------------------ ; InitMap: Creates a rich test level with rooms, hallways, pillars & doors ; ------------------------------------------------------------------------------ InitMap PROC uses eax ecx edi call StopCutscene ; Clear map to empty mov edi, OFFSET gameMap mov ecx, MAP_SIZE xor eax, eax rep stosb ; 1. Build Outer Boundary Walls (Type 1: Brick) mov ecx, 0 loop_boundary_x: ; Top row (y = 0) mov byte ptr [gameMap + ecx], CELL_BRICK ; Bottom row (y = 31) mov byte ptr [gameMap + (31 * MAP_W) + ecx], CELL_BRICK inc ecx cmp ecx, MAP_W jl loop_boundary_x mov ecx, 0 loop_boundary_y: ; Left column (x = 0) mov eax, ecx shl eax, 5 ; eax = ecx * 32 mov byte ptr [gameMap + eax], CELL_BRICK ; Right column (x = 31) mov byte ptr [gameMap + eax + 31], CELL_BRICK inc ecx cmp ecx, MAP_H jl loop_boundary_y ; 2. Build Internal Room Walls (y = 8, x from 1 to 20, Stone Brick) mov ecx, 1 loop_wall1: mov byte ptr [gameMap + (8 * MAP_W) + ecx], CELL_STONE inc ecx cmp ecx, 20 jle loop_wall1 ; Create Door at (x = 10, y = 8) mov byte ptr [gameMap + (8 * MAP_W) + 10], CELL_DOOR ; 3. Build Vertical Dividing Wall (x = 20, y from 1 to 24, Wood) mov ecx, 1 loop_wall2: cmp ecx, 14 ; Leave opening at y = 14 je @F mov eax, ecx shl eax, 5 ; ecx * 32 mov byte ptr [gameMap + eax + 20], CELL_WOOD @@: inc ecx cmp ecx, 24 jle loop_wall2 ; 4. Add Pillars in the main courtyard (Type 4: Metal / Type 7: Pillar) mov byte ptr [gameMap + (4 * MAP_W) + 4], CELL_PILLAR mov byte ptr [gameMap + (4 * MAP_W) + 7], CELL_PILLAR mov byte ptr [gameMap + (12 * MAP_W) + 5], CELL_METAL mov byte ptr [gameMap + (12 * MAP_W) + 15], CELL_METAL mov byte ptr [gameMap + (18 * MAP_W) + 8], CELL_PILLAR mov byte ptr [gameMap + (18 * MAP_W) + 12], CELL_PILLAR ; 5. South wing decorative walls mov byte ptr [gameMap + (24 * MAP_W) + 5], CELL_STONE mov byte ptr [gameMap + (24 * MAP_W) + 6], CELL_STONE mov byte ptr [gameMap + (24 * MAP_W) + 7], CELL_DOOR mov byte ptr [gameMap + (24 * MAP_W) + 8], CELL_STONE mov byte ptr [gameMap + (24 * MAP_W) + 9], CELL_STONE ; Safe player spawn + clear entities (raw world units) mov playerX, 160 mov playerY, 160 mov playerAngle, 0 mov currentMapPath, 0 mov roomEntryX, 2 mov roomEntryY, 2 mov roomEntryAngle, 0 call ResetMovementFractions mov cutsceneCount, 0 mov cutsceneStringCount, 0 mov cutsceneFlags, 0 mov cutscenePending, 0 mov spriteCount, 0 mov transitionCount, 0 mov portalLatchCell, -1 mov mapCeilingColor, COLOR_CEILING mov mapFloorColor, COLOR_FLOOR call InitLightingSystem ret InitMap ENDP ; ------------------------------------------------------------------------------ ; GetMapCell: Returns cell value at grid coordinates (gridX, gridY) ; ------------------------------------------------------------------------------ GetMapCell PROC mapX:DWORD, mapY:DWORD mov eax, mapX cmp eax, 0 jl out_of_bounds cmp eax, MAP_W jge out_of_bounds mov edx, mapY cmp edx, 0 jl out_of_bounds cmp edx, MAP_H jge out_of_bounds shl edx, 5 ; mapY * 32 add edx, eax movzx eax, byte ptr [gameMap + edx] ret out_of_bounds: mov eax, CELL_BRICK ; Solid wall outside bounds ret GetMapCell ENDP ; ------------------------------------------------------------------------------ ; SetMapCell: Sets cell value at grid coordinates (gridX, gridY) ; ------------------------------------------------------------------------------ SetMapCell PROC mapX:DWORD, mapY:DWORD, cellVal:DWORD mov eax, mapX cmp eax, 0 jl done_set cmp eax, MAP_W jge done_set mov edx, mapY cmp edx, 0 jl done_set cmp edx, MAP_H jge done_set shl edx, 5 add edx, eax mov al, byte ptr [cellVal] mov byte ptr [gameMap + edx], al done_set: ret SetMapCell ENDP ; ------------------------------------------------------------------------------ ; IsSolidCell: Returns 1 if cell blocks player and rays, 0 otherwise ; ------------------------------------------------------------------------------ IsSolidCell PROC cellType:DWORD mov eax, cellType cmp eax, CELL_EMPTY je not_solid cmp eax, CELL_DOOR_OPEN je not_solid mov eax, 1 ; Solid wall, closed door, or portal wall ret not_solid: xor eax, eax ret IsSolidCell ENDP ; ------------------------------------------------------------------------------ ; InteractDoor: Checks if player is facing a door or portal and handles interaction ; ------------------------------------------------------------------------------ ; Returns resulting cell or zero. Shared by direct interaction and switches. ToggleDoorCell PROC uses ebx ecx edx, cellX:DWORD, cellY:DWORD invoke GetMapCell, cellX, cellY cmp eax, CELL_DOOR jne toggle_check_close invoke SetMapCell, cellX, cellY, CELL_DOOR_OPEN mov eax, CELL_DOOR_OPEN ret toggle_check_close: cmp eax, CELL_DOOR_OPEN jne toggle_blocked mov eax, playerX sub eax, playerRadius sar eax, CELL_SHIFT cmp eax, cellX jg toggle_close mov eax, playerX add eax, playerRadius sar eax, CELL_SHIFT cmp eax, cellX jl toggle_close mov eax, playerY sub eax, playerRadius sar eax, CELL_SHIFT cmp eax, cellY jg toggle_close mov eax, playerY add eax, playerRadius sar eax, CELL_SHIFT cmp eax, cellY jge toggle_blocked toggle_close: invoke SetMapCell, cellX, cellY, CELL_DOOR mov eax, CELL_DOOR ret toggle_blocked: xor eax, eax ret ToggleDoorCell ENDP InteractDoor PROC uses ebx ecx edx esi edi LOCAL targetX:DWORD, targetY:DWORD ; Compute point 60 units in front of player mov eax, playerAngle and eax, ANGLE_MASK ; dx = cos(angle) * 60 mov ebx, [tableCos + eax*4] imul ebx, 60 sar ebx, 16 add ebx, playerX sar ebx, 6 ; divide by CELL_SIZE (64) mov targetX, ebx ; dy = sin(angle) * 60 mov ecx, [tableSin + eax*4] imul ecx, 60 sar ecx, 16 add ecx, playerY sar ecx, 6 mov targetY, ecx invoke GetMapCell, targetX, targetY cmp eax, CELL_DOOR je interact_door_found cmp eax, CELL_DOOR_OPEN jne check_portal_interact interact_door_found: mov keyAction, 0 ; A bound door is switch-only, or key-unlockable. Ordinary doors stay free. mov edi, targetY shl edi, CELL_SHIFT-1 ; MAP_W is 32 add edi, targetX cmp byte ptr [doorUnlocked+edi], 0 jne interact_toggle mov esi, OFFSET spriteList mov ecx, spriteCount interact_binding_loop: test ecx, ecx jz interact_toggle mov eax, [esi].SpriteStruct.flags cmp al, ENTITY_TYPE_SWITCH jne interact_binding_next mov edx, eax and edx, 0FEFFh cmp edx, 0A805h jne interact_binding_next mov edx, eax shr edx, 16 and edx, 0FFh cmp edx, targetX jne interact_binding_next mov edx, eax shr edx, 24 cmp edx, targetY jne interact_binding_next test eax, 100h jz interact_locked cmp playerKeys, 0 je interact_locked dec playerKeys mov byte ptr [doorUnlocked+edi], 1 jmp interact_toggle interact_binding_next: add esi, sizeof SpriteStruct dec ecx jmp interact_binding_loop interact_locked: invoke ShowGameToast, OFFSET szDoorLockedToast, 50, COLOR_YELLOW ret interact_toggle: invoke ToggleDoorCell, targetX, targetY cmp eax, CELL_DOOR_OPEN je interact_opened cmp eax, CELL_DOOR jne no_door invoke ShowGameToast, OFFSET szDoorClosedToast, 50, COLOR_YELLOW ret interact_opened: invoke ShowGameToast, OFFSET szDoorOpenedToast, 50, COLOR_GREEN ret check_portal_interact: ; Check if facing a transition trigger or CELL_PORTAL cmp transitionCount, 0 je no_door mov esi, OFFSET transitionList mov edx, 0 portal_find_loop: cmp edx, transitionCount jge no_door movzx eax, [esi].LevelTransition.triggerCellX cmp eax, targetX jne portal_find_next movzx eax, [esi].LevelTransition.triggerCellY cmp eax, targetY jne portal_find_next ; Found transition! mov keyAction, 0 movzx eax, [esi].LevelTransition.targetSpawnX movzx ebx, [esi].LevelTransition.targetSpawnY movzx ecx, [esi].LevelTransition.targetAngle movzx edx, [esi].LevelTransition.fadeEffect lea edi, [esi].LevelTransition.targetMap invoke SwitchSublevel, edi, eax, ebx, ecx, edx ret portal_find_next: add esi, sizeof LevelTransition inc edx jmp portal_find_loop no_door: ret InteractDoor ENDP ; ------------------------------------------------------------------------------ ; CheckDoorHint: Returns 1 for Door, 2 for Portal, 0 for none (for HUD hint) ; ------------------------------------------------------------------------------ CheckDoorHint PROC uses ebx ecx edx LOCAL targetX:DWORD, targetY:DWORD ; Compute point 60 units in front of player mov eax, playerAngle and eax, ANGLE_MASK mov ebx, [tableCos + eax*4] imul ebx, 60 sar ebx, 16 add ebx, playerX sar ebx, 6 mov targetX, ebx mov ecx, [tableSin + eax*4] imul ecx, 60 sar ecx, 16 add ecx, playerY sar ecx, 6 mov targetY, ecx invoke GetMapCell, targetX, targetY cmp eax, CELL_DOOR je hint_door cmp eax, CELL_DOOR_OPEN je hint_door cmp eax, CELL_PORTAL je hint_portal ; Check if target is a trigger in transitionList cmp transitionCount, 0 je hint_none mov edx, 0 mov eax, OFFSET transitionList hint_check_loop: cmp edx, transitionCount jge hint_none movzx ebx, [eax].LevelTransition.triggerCellX cmp ebx, targetX jne hint_check_next movzx ebx, [eax].LevelTransition.triggerCellY cmp ebx, targetY je hint_portal hint_check_next: add eax, sizeof LevelTransition inc edx jmp hint_check_loop hint_none: xor eax, eax ret hint_door: mov eax, 1 ret hint_portal: mov eax, 2 ret CheckDoorHint ENDP ; ------------------------------------------------------------------------------ ; CheckPortalTriggers: Checks if player stepped onto/into a transition trigger cell ; ------------------------------------------------------------------------------ CheckPortalTriggers PROC uses ebx ecx edx esi edi cmp fadeState, 0 jne cpt_done cmp transitionCount, 0 je cpt_done ; Current player cell X and Y mov eax, playerX sar eax, 6 mov ebx, eax mov eax, playerY sar eax, 6 mov ecx, eax shl eax, 5 add eax, ebx cmp eax, portalLatchCell je cpt_done mov portalLatchCell, eax mov esi, OFFSET transitionList mov edx, 0 cpt_loop: cmp edx, transitionCount jge cpt_done movzx eax, [esi].LevelTransition.triggerCellX cmp eax, ebx jne cpt_next movzx eax, [esi].LevelTransition.triggerCellY cmp eax, ecx jne cpt_next ; Walked into trigger cell! Initiate transition movzx eax, [esi].LevelTransition.targetSpawnX movzx ebx, [esi].LevelTransition.targetSpawnY movzx ecx, [esi].LevelTransition.targetAngle movzx edx, [esi].LevelTransition.fadeEffect lea edi, [esi].LevelTransition.targetMap invoke SwitchSublevel, edi, eax, ebx, ecx, edx ret cpt_next: add esi, sizeof LevelTransition inc edx jmp cpt_loop cpt_done: ret CheckPortalTriggers ENDP ; ------------------------------------------------------------------------------ ; SwitchSublevel: Triggers transition sequence to target sublevel file ; ------------------------------------------------------------------------------ SwitchSublevel PROC uses esi edi, pszNextMap:DWORD, spawnX:DWORD, spawnY:DWORD, spawnAngle:DWORD, fadeEffect:DWORD cmp fadeState, 0 jne switch_busy ; Even an instant room switch consumes this tick. AI/combat must not use ; pre-transition input in the newly loaded room. mov gameplayTickBlocked, 1 mov esi, pszNextMap lea edi, pendingMapPath mov ecx, 32 copy_next_map: lodsb stosb test al, al jz next_map_copied dec ecx jnz copy_next_map mov byte ptr [edi-1], 0 next_map_copied: mov eax, spawnX mov pendingSpawnX, eax mov eax, spawnY mov pendingSpawnY, eax mov eax, spawnAngle mov pendingSpawnAngle, eax cmp fadeEffect, 0 je switch_instant ; Initiate smooth Fade Out (up to 16 fixed ticks, ~0.26s). mov fadeState, 1 mov fadeLevel, 1 ret switch_instant: call DoSublevelTransition switch_busy: ret SwitchSublevel ENDP ; ------------------------------------------------------------------------------ ; DoSublevelTransition: Unloads old sector and loads new sector ; ------------------------------------------------------------------------------ DoSublevelTransition PROC uses ebx ecx edx ; The loader replaces the room only after validating every staged value. invoke LoadSublevelBinary, OFFSET pendingMapPath, pendingSpawnX, pendingSpawnY, pendingSpawnAngle test eax, eax jnz transition_loaded mov mapErrorFrames, 180 ret transition_loaded: call InitEntities ret DoSublevelTransition ENDP ; ------------------------------------------------------------------------------ ; UpdateScreenFade: fixed-tick transition sequencer, never called from drawing. ; ------------------------------------------------------------------------------ UpdateScreenFade PROC cmp fadeState, 0 je fade_tick_done cmp fadeState, 1 je do_fade_out cmp fadeState, 2 je do_switch_state cmp fadeState, 3 je do_fade_in jmp fade_tick_done do_fade_out: inc fadeLevel cmp fadeLevel, 16 jl fade_tick_done mov fadeLevel, 16 mov fadeState, 2 call DoSublevelTransition mov fadeState, 3 jmp fade_tick_done do_switch_state: call DoSublevelTransition mov fadeState, 3 jmp fade_tick_done do_fade_in: dec fadeLevel cmp fadeLevel, 0 jg fade_tick_done mov fadeLevel, 0 mov fadeState, 0 fade_tick_done: ret UpdateScreenFade ENDP ; Pure framebuffer post-process. Repeated drawing never switches rooms. ApplyScreenFade PROC uses ebx ecx edx esi edi apply_darkening: cmp fadeLevel, 0 jle fade_done cmp fadeLevel, 16 jl fade_math ; Fully black screen mov edi, OFFSET screenBuffer mov ecx, SCREEN_W * SCREEN_H xor eax, eax rep stosd jmp fade_done fade_math: mov ebx, 16 sub ebx, fadeLevel ; scale: 1..15 mov esi, OFFSET screenBuffer mov edi, esi mov ecx, SCREEN_W * SCREEN_H fade_pixel_loop: mov eax, [esi] ; Blue movzx edx, al imul edx, ebx shr edx, 4 ; Green movzx eax, ah imul eax, ebx shr eax, 4 shl eax, 8 or edx, eax ; Red mov eax, [esi] shr eax, 16 movzx eax, al imul eax, ebx shr eax, 4 shl eax, 16 or eax, edx mov [edi], eax add esi, 4 add edi, 4 dec ecx jnz fade_pixel_loop fade_done: ret ApplyScreenFade ENDP ; ------------------------------------------------------------------------------ ; LoadSublevelBinary: Parameterized level loader for any .dat file ; Overrides player spawn if spawnX != -1, populates gameMap, sprites, transitions. ; Returns: eax = 1 on success, 0 on fallback ; ------------------------------------------------------------------------------ LoadSublevelBinary PROC uses ebx ecx edx esi edi, pszPath:DWORD, spawnX:DWORD, spawnY:DWORD, spawnAngle:DWORD LOCAL hFile:DWORD, bytesRead:DWORD, header:MapHeader LOCAL numSprites:DWORD, numTransitions:DWORD, index:DWORD LOCAL numEvents:DWORD, numStrings:DWORD, sceneFlags:DWORD, numLights:DWORD LOCAL resolvedPath[MAX_PATH_BYTES]:BYTE LOCAL stagedMap[MAP_SIZE]:BYTE LOCAL stagedSprites[MAX_SPRITES * (sizeof SpriteStruct)]:BYTE LOCAL stagedTransitions[MAX_TRANSITIONS * (sizeof LevelTransition)]:BYTE LOCAL newX:DWORD, newY:DWORD, newAngle:DWORD ; Relative map names live in assets. Accept an actual assets prefix, ; drive-qualified paths and absolute paths, never just the first letter. mov esi, pszPath test esi, esi jz load_default_path cmp byte ptr [esi], 0 jne resolve_map_path load_default_path: mov esi, OFFSET szMapFilePath resolve_map_path: lea edi, resolvedPath mov ecx, MAX_PATH_BYTES - 1 cmp byte ptr [esi], 0 je load_failed cmp byte ptr [esi+1], ':' je copy_map_path cmp byte ptr [esi], 92 je copy_map_path cmp byte ptr [esi], '/' je copy_map_path push esi push ecx mov ebx, OFFSET szAssetsPrefix mov ecx, 6 check_assets_prefix: mov al, [esi] or al, 20h cmp al, [ebx] jne prefix_not_assets inc esi inc ebx loop check_assets_prefix cmp byte ptr [esi], 92 je prefix_is_assets cmp byte ptr [esi], '/' je prefix_is_assets prefix_not_assets: pop ecx pop esi mov ebx, OFFSET szAssetsPrefix copy_assets_prefix: mov al, [ebx] test al, al jz copy_map_path stosb inc ebx dec ecx jmp copy_assets_prefix prefix_is_assets: pop ecx pop esi copy_map_path: lodsb stosb test al, al jz open_level dec ecx jnz copy_map_path jmp load_failed open_level: invoke CreateFileA, ADDR resolvedPath, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 mov hFile, eax cmp eax, INVALID_HANDLE_VALUE je load_failed invoke ReadFile, hFile, ADDR header, sizeof MapHeader, ADDR bytesRead, 0 test eax, eax jz close_load_failed cmp bytesRead, sizeof MapHeader jne close_load_failed cmp header.magic, MAP_MAGIC jne close_load_failed cmp header.version, 1 jb close_load_failed cmp header.version, MAP_FORMAT_VERSION ja close_load_failed cmp header.mapWidth, MAP_W jne close_load_failed cmp header.mapHeight, MAP_H jne close_load_failed cmp header.playerStartX, MAP_W jae close_load_failed cmp header.playerStartY, MAP_H jae close_load_failed cmp header.playerStartAngle, ANGLE_MASK ja close_load_failed movzx eax, header.spriteCount cmp eax, MAX_SPRITES ja close_load_failed mov numSprites, eax xor eax, eax cmp header.version, 1 je old_map_no_transitions ; Accept the extended v2 files written by early EblanStudio builds. movzx eax, header.transitionCount old_map_no_transitions: cmp eax, MAX_TRANSITIONS ja close_load_failed mov numTransitions, eax mov numEvents, 0 mov numStrings, 0 mov sceneFlags, 0 cmp header.version, 4 jb old_map_no_cutscene movzx eax, header.cutsceneCount cmp eax, MAX_CUTSCENE_EVENTS ja close_load_failed mov numEvents, eax movzx eax, header.cutsceneStringCount cmp eax, MAX_CUTSCENE_STRINGS ja close_load_failed mov numStrings, eax movzx eax, header.cutsceneFlags mov sceneFlags, eax old_map_no_cutscene: mov numLights,0 cmp header.version,5 jb old_map_no_lighting cmp header.lightingReserved,0 jne close_load_failed movzx eax,header.lightCount cmp eax,13 ja close_load_failed mov numLights,eax old_map_no_lighting: mov eax, numTransitions imul eax, sizeof LevelTransition mov ebx, eax mov eax, numSprites imul eax, sizeof SpriteStruct add ebx, eax add ebx, sizeof MapHeader + MAP_SIZE mov eax, numEvents imul eax, sizeof CutsceneEvent add ebx, eax mov eax, numStrings imul eax, CUTSCENE_STRING_BYTES add ebx, eax cmp header.version,5 jb map_lighting_size_ready mov eax,numLights imul eax,40 add ebx,eax add ebx,32+4096 map_lighting_size_ready: invoke GetFileSize, hFile, 0 cmp eax, ebx jne close_load_failed invoke ReadFile, hFile, ADDR stagedMap, MAP_SIZE, ADDR bytesRead, 0 test eax, eax jz close_load_failed cmp bytesRead, MAP_SIZE jne close_load_failed mov eax, numSprites imul eax, sizeof SpriteStruct mov ebx, eax invoke ReadFile, hFile, ADDR stagedSprites, ebx, ADDR bytesRead, 0 test eax, eax jz close_load_failed cmp bytesRead, ebx jne close_load_failed mov eax, numTransitions imul eax, sizeof LevelTransition mov ebx, eax invoke ReadFile, hFile, ADDR stagedTransitions, ebx, ADDR bytesRead, 0 test eax, eax jz close_load_failed cmp bytesRead, ebx jne close_load_failed mov ebx, numEvents imul ebx, sizeof CutsceneEvent invoke ReadFile, hFile, OFFSET stagedCutsceneEvents, ebx, ADDR bytesRead, 0 test eax, eax jz close_load_failed cmp bytesRead, ebx jne close_load_failed mov ebx, numStrings imul ebx, CUTSCENE_STRING_BYTES invoke ReadFile, hFile, OFFSET stagedCutsceneStrings, ebx, ADDR bytesRead, 0 test eax, eax jz close_load_failed cmp bytesRead, ebx jne close_load_failed mov esi,OFFSET legacyLightingSettings mov edi,OFFSET stagedLightSettings mov ecx,8 rep movsd mov edi,OFFSET stagedLightRGB mov ecx,MAP_SIZE mov eax,00282828h rep stosd cmp header.version,5 jb staged_lighting_ready invoke ReadFile,hFile,OFFSET stagedLightSettings,32,ADDR bytesRead,0 test eax,eax jz close_load_failed cmp bytesRead,32 jne close_load_failed mov ebx,numLights imul ebx,40 invoke ReadFile,hFile,OFFSET stagedLightSources,ebx,ADDR bytesRead,0 test eax,eax jz close_load_failed cmp bytesRead,ebx jne close_load_failed invoke ReadFile,hFile,OFFSET stagedLightRGB,4096,ADDR bytesRead,0 test eax,eax jz close_load_failed cmp bytesRead,4096 jne close_load_failed invoke ValidateLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights test eax,eax jz close_load_failed mov esi,OFFSET stagedLightRGB mov ecx,MAP_SIZE map_check_light_rgb: lodsd test eax,0FF000000h jnz close_load_failed loop map_check_light_rgb staged_lighting_ready: invoke ValidateStagedCutscene, numEvents, numStrings, sceneFlags, ADDR stagedMap, numTransitions test eax, eax jz close_load_failed ; Validate all payload data before touching the active room. lea esi, stagedMap mov ecx, MAP_SIZE validate_grid: lodsb cmp al, 15 ja close_load_failed loop validate_grid lea esi, stagedSprites mov ecx, numSprites validate_sprite: test ecx, ecx jz validate_transitions cmp header.version, 1 jne sprite_coords_raw sar [esi].SpriteStruct.x, 16 sar [esi].SpriteStruct.y, 16 sprite_coords_raw: cmp [esi].SpriteStruct.x, 0 jl close_load_failed cmp [esi].SpriteStruct.x, MAP_W * CELL_SIZE jge close_load_failed cmp [esi].SpriteStruct.y, 0 jl close_load_failed cmp [esi].SpriteStruct.y, MAP_H * CELL_SIZE jge close_load_failed cmp [esi].SpriteStruct.textureId, MAX_TEXTURES jae close_load_failed cmp [esi].SpriteStruct.scale, 0 jl close_load_failed cmp [esi].SpriteStruct.scale, 16 * FIXED_ONE ja close_load_failed mov eax, [esi].SpriteStruct.flags cmp al, ENTITY_TYPE_SWITCH jne validated_sprite_binding mov edx, eax and edx, 0FEFFh cmp edx, 0A805h jne validated_sprite_binding mov edx, eax shr edx, 24 cmp edx, MAP_H jae close_load_failed shr eax, 16 and eax, 0FFh cmp eax, MAP_W jae close_load_failed shl edx, 5 add edx, eax mov al, stagedMap[edx] cmp al, CELL_DOOR je binding_cell_ok cmp al, CELL_DOOR_OPEN jne close_load_failed binding_cell_ok: lea edi, stagedSprites binding_unique_loop: cmp edi, esi jae validated_sprite_binding mov eax, [edi].SpriteStruct.flags cmp al, ENTITY_TYPE_SWITCH jne binding_unique_next mov edx, eax and edx, 0FEFFh cmp edx, 0A805h jne binding_unique_next shr eax, 16 mov edx, [esi].SpriteStruct.flags shr edx, 16 cmp eax, edx je close_load_failed binding_unique_next: add edi, sizeof SpriteStruct jmp binding_unique_loop validated_sprite_binding: add esi, sizeof SpriteStruct dec ecx jmp validate_sprite validate_transitions: lea esi, stagedTransitions mov index, 0 validate_transition: mov eax, index cmp eax, numTransitions jae validate_portal_cells cmp [esi].LevelTransition.triggerCellX, MAP_W jae close_load_failed cmp [esi].LevelTransition.triggerCellY, MAP_H jae close_load_failed cmp [esi].LevelTransition.targetSpawnX, MAP_W jae close_load_failed cmp [esi].LevelTransition.targetSpawnY, MAP_H jae close_load_failed cmp [esi].LevelTransition.targetAngle, ANGLE_MASK ja close_load_failed cmp [esi].LevelTransition.fadeEffect, 1 ja close_load_failed push ecx lea edi, [esi].LevelTransition.targetMap invoke ValidateMapFilename, edi pop ecx test eax, eax jz close_load_failed movzx eax, [esi].LevelTransition.triggerCellY shl eax, 5 movzx edx, [esi].LevelTransition.triggerCellX add eax, edx mov al, stagedMap[eax] cmp al, CELL_EMPTY je transition_grid_ok cmp al, CELL_DOOR_OPEN je transition_grid_ok cmp al, CELL_PORTAL jne close_load_failed transition_grid_ok: lea edi, stagedTransitions mov ecx, index unique_transition: test ecx, ecx jz transition_unique mov ax, WORD PTR [esi] cmp ax, WORD PTR [edi] je close_load_failed add edi, sizeof LevelTransition dec ecx jmp unique_transition transition_unique: add esi, sizeof LevelTransition inc index jmp validate_transition validate_portal_cells: xor ebx, ebx portal_grid_loop: cmp ebx, MAP_SIZE jae validate_spawn cmp stagedMap[ebx], CELL_PORTAL jne next_portal_grid lea esi, stagedTransitions mov ecx, numTransitions find_grid_transition: test ecx, ecx jz close_load_failed movzx eax, [esi].LevelTransition.triggerCellY shl eax, 5 movzx edx, [esi].LevelTransition.triggerCellX add eax, edx cmp eax, ebx je next_portal_grid add esi, sizeof LevelTransition dec ecx jmp find_grid_transition next_portal_grid: inc ebx jmp portal_grid_loop validate_spawn: movzx eax, header.playerStartX movzx ebx, header.playerStartY movzx ecx, header.playerStartAngle cmp spawnX, -1 je spawn_values_ready mov eax, spawnX mov ebx, spawnY mov ecx, spawnAngle spawn_values_ready: cmp eax, MAP_W jae close_load_failed cmp ebx, MAP_H jae close_load_failed cmp ecx, ANGLE_MASK ja close_load_failed mov newX, eax mov newY, ebx mov newAngle, ecx shl ebx, 5 add ebx, eax mov al, stagedMap[ebx] cmp al, CELL_EMPTY je commit_level cmp al, CELL_DOOR_OPEN je commit_level cmp al, CELL_PORTAL jne close_load_failed commit_level: invoke CloseHandle, hFile call StopCutscene mov esi, OFFSET stagedCutsceneEvents mov edi, OFFSET cutsceneEvents mov ecx, numEvents shl ecx, 2 rep movsd mov esi, OFFSET stagedCutsceneStrings mov edi, OFFSET cutsceneStrings mov ecx, numStrings imul ecx, CUTSCENE_STRING_BYTES / 4 rep movsd mov eax, numEvents mov cutsceneCount, eax mov eax, numStrings mov cutsceneStringCount, eax mov eax, sceneFlags mov cutsceneFlags, eax and eax, CUTSCENE_AUTOPLAY mov cutscenePending, eax lea esi, stagedMap mov edi, OFFSET gameMap mov ecx, MAP_SIZE / 4 rep movsd lea esi, stagedSprites mov edi, OFFSET spriteList mov ecx, numSprites imul ecx, sizeof SpriteStruct / 4 rep movsd lea esi, stagedTransitions mov edi, OFFSET transitionList mov ecx, numTransitions imul ecx, sizeof LevelTransition rep movsb mov eax, numSprites mov spriteCount, eax mov eax, numTransitions mov transitionCount, eax mov eax, header.ceilingColor and eax, 00FFFFFFh mov mapCeilingColor, eax mov eax, header.floorColor and eax, 00FFFFFFh mov mapFloorColor, eax mov eax, newY shl eax, 5 add eax, newX mov portalLatchCell, eax mov eax, newX shl eax, CELL_SHIFT add eax, CELL_SIZE / 2 mov playerX, eax mov eax, newY shl eax, CELL_SHIFT add eax, CELL_SIZE / 2 mov playerY, eax mov eax, newAngle mov playerAngle, eax mov eax, newX mov roomEntryX, eax mov eax, newY mov roomEntryY, eax mov eax, newAngle mov roomEntryAngle, eax lea esi, resolvedPath mov edi, OFFSET currentMapPath remember_room_path: lodsb stosb test al, al jnz remember_room_path call ResetMovementFractions mov mapErrorFrames, 0 invoke CommitLightingData,OFFSET stagedLightSettings,OFFSET stagedLightSources,numLights,OFFSET stagedLightRGB mov eax, 1 ret close_load_failed: invoke CloseHandle, hFile load_failed: xor eax, eax ret LoadSublevelBinary ENDP ; Map references in EBLN are bounded ASCII basenames, never arbitrary pointers. ValidateMapFilename PROC uses ebx ecx edx esi, pszName:DWORD mov esi, pszName xor ecx, ecx filename_character: mov al, [esi+ecx] test al, al jz filename_end cmp al, 32 jb filename_invalid cmp al, 127 jae filename_invalid cmp al, 92 je filename_invalid cmp al, '/' je filename_invalid cmp al, ':' je filename_invalid cmp al, '*' je filename_invalid cmp al, '?' je filename_invalid cmp al, 34 je filename_invalid cmp al, '<' je filename_invalid cmp al, '>' je filename_invalid cmp al, '|' je filename_invalid inc ecx cmp ecx, 32 jb filename_character jmp filename_invalid filename_end: cmp ecx, 5 jb filename_invalid mov eax, DWORD PTR [esi+ecx-4] or eax, 20202000h cmp eax, 7461642Eh ; .dat jne filename_invalid mov eax, 1 ret filename_invalid: xor eax, eax ret ValidateMapFilename ENDP LoadMapBinary PROC mov eax, OFFSET szMapFilePath cmp startupMapPath, 0 je load_startup_map mov eax, OFFSET startupMapPath load_startup_map: invoke LoadSublevelBinary, eax, -1, -1, 0 test eax, eax jnz startup_map_ready call InitMap startup_map_ready: push eax call InitEntities pop eax ret LoadMapBinary ENDP ; Reload the current room and the actual entry spawn, not the startup map. ; LoadSublevelBinary commits only on success, so failure preserves all gameplay. RestartCurrentRoom PROC cmp currentMapPath, 0 je restart_procedural invoke LoadSublevelBinary, OFFSET currentMapPath, roomEntryX, roomEntryY, roomEntryAngle test eax, eax jz restart_failed jmp restart_committed restart_procedural: call InitMap restart_committed: call InitEntities call ResetMovementFractions mov playerHealth, 100 mov playerAmmo, 50 mov playerScore, 0 mov playerKeys, 0 mov isPlayerDead, 0 mov isGamePaused, 0 mov gameDialogActive, 0 mov gameToastTimer, 0 mov playerAttackCooldown, 0 mov playerFlashTimer, 0 mov playerHitFlash, 0 mov fadeState, 0 mov fadeLevel, 0 mov mapErrorFrames, 0 mov pendingMapPath, 0 mov pendingSpawnX, 0 mov pendingSpawnY, 0 mov pendingSpawnAngle, 0 mov gameplayTickBlocked, 1 mov clockReset, 1 mov eax, 1 ret restart_failed: xor eax, eax ret RestartCurrentRoom ENDP