feat: update casino environments

This commit is contained in:
Игорь Федосеев 2026-10-31 02:28:37 +03:00
parent 5dedaf459f
commit 7e4f7d7be1
36 changed files with 2332 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Binary file not shown.

View file

@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
{
static class Program
{
const string ExpectedPokerSha256 = "25FBDF78EDF3DFD3F6B72657E78C3156F74931A656B4CBDB15260D496C96849A";
const string ExpectedPokerSha256 = "B0F5CE31D9A2467CFE8AF66CA90CC545F75318F11D149473A4377A2A278C8B23";
[STAThread]
static void Main()

Binary file not shown.

View file

@ -170,7 +170,8 @@ include src_graphics\native_coin_data.inc
include src_graphics\native_dice_data.inc
include src_graphics\scratch_data.inc
include src_graphics\hell_tower_data.inc
include src_graphics\charms_data.inc
include src_graphics\charms_data.inc
include src_graphics\backrooms_data.inc
include src_graphics\observer_data.inc
include src_graphics\darts_data.inc
include src_graphics\jackbox_data.inc
@ -471,7 +472,7 @@ screenBuffer dd SCREEN_SIZE dup(?)
ALIGN 4
zBuffer dd SCREEN_W dup(?)
ALIGN 4
texturesBuffer dd (104 * 128 * 128) dup(?)
texturesBuffer dd (185 * 128 * 128) dup(?)
hudNumBuffer db 32 dup(?)
@ -3249,6 +3250,25 @@ dice_start_roll:
invoke RandomRange3D, 6
inc eax
mov dice3d_val2, eax
call BackroomsLuck3D
cmp eax,-1
je br_dice_fixed
xor eax,1
mov ecx,dice3d_choice
dec ecx
xor eax,ecx
mov edx,dice3d_val1
add edx,dice3d_val2
and edx,1
cmp eax,edx
je br_dice_fixed
xor edx,edx
mov eax,dice3d_val2
mov ecx,6
div ecx
inc edx
mov dice3d_val2,edx
br_dice_fixed:
invoke PlayCasinoSound3D, OFFSET soundDiceRoll, 0, 1
call SaveProgress3D
ret
@ -3485,6 +3505,21 @@ InitSkullsGame ENDP
FinalizeSkullsGuess PROC uses ebx
cmp skulls3d_state,3
jne skulls_guess_done
cmp brSkullsLuck3D,-1
je br_skulls_fixed
mov eax,skulls3d_choice
cmp brSkullsLuck3D,1
je @F
inc eax
xor edx,edx
mov ecx,3
div ecx
mov eax,edx
@@:
mov skulls3d_ballPos,eax
; Reality distortion explicitly covers relocation before the reveal.
mov skulls3d_timer,0
br_skulls_fixed:
mov eax, skulls3d_choice
cmp eax, skulls3d_ballPos
jne skulls_guess_wrong
@ -3568,6 +3603,8 @@ skulls_bet_down:
ret
skulls_start_game:
call BackroomsLuck3D
mov brSkullsLuck3D,eax
mov eax, skulls3d_bet
cmp eax, bankroll
jle @F
@ -3716,6 +3753,16 @@ RenderSkullsGame PROC uses ebx esi edi
jne @F
invoke DrawString8x8, 28, 62, OFFSET textSkullsPhase3, 00FFFF00h
jmp skulls_draw_idle_cups
@@:
; Short artifact glitch, then reveal the actual ball and matching payout.
cmp brSkullsLuck3D,-1
je @F
cmp skulls3d_timer,12
jae @F
inc skulls3d_timer
invoke DrawBox,8,76,304,122,00130F20h
invoke DrawString8x8,108,128,OFFSET brRealityGlitch3D,00FF88CCh
jmp skulls_render_controls
@@:
; Phase 4: Reveal outcome
mov eax, skulls3d_win
@ -4633,6 +4680,8 @@ check_total_movement:
mov playerY, eax
input_action:
cmp brActive3D,0
jne player_input_complete
cmp engineMode,MODE_STAIRS
je player_input_complete
invoke Pressed3D, 45h
@ -4706,9 +4755,12 @@ CheckInteractions ENDP
; (Legacy console arcade bezel bridge removed)
Trigger3DInteraction PROC
cmp brActive3D,0
jne trigger_backrooms_station
call TriggerHallInteraction3D
test eax, eax
jnz trigger_done
trigger_backrooms_station:
mov eax, interactTargetId
test eax, eax
jz trigger_done
@ -5200,7 +5252,8 @@ include src_graphics\hell_tower.inc
include src_graphics\charms.inc
include src_graphics\observer.inc
include src_graphics\darts.inc
include src_graphics\jackbox.inc
include src_graphics\jackbox.inc
include src_graphics\backrooms.inc
; --- Main Loop of the 3D Casino Engine ---
StartHellTower3D PROC uses ebx esi edi
@ -5279,6 +5332,9 @@ engine_observer_skip:
cmp engineMode, MODE_WALK
jne leave_table
pause_interior_walk:
cmp brCutscene3D,0
jne engine_tick
mov brEntryHold3D,0
xor paused3D, 1
jmp engine_tick
leave_table:
@ -5343,10 +5399,15 @@ check_arrival_mode:
check_walk_mode:
cmp engineMode, MODE_WALK
jne check_slots_mode
cmp brActive3D,0
jne engine_backrooms
call UpdateHall3D
call UpdateCamera3D
call CheckInteractions
call HandlePlayerInput
call BackroomsEntry3D
cmp brActive3D,0
jne engine_backrooms
call HandleJukeboxKeys3D
call UpdateInteriorTravel3D
call UpdateInteriorEye3D
@ -5365,6 +5426,14 @@ walk_world_ready:
call RenderHUD
call RenderMinimap
jmp present_frame
engine_backrooms:
call HandleBackrooms3D
cmp brActive3D,0
je check_walk_mode
cmp engineMode,MODE_WALK
jne check_slots_mode
call RenderBackrooms3D
jmp present_frame
check_slots_mode:
cmp engineMode,MODE_JACKBOX
jne check_jackbox_done
@ -5531,6 +5600,7 @@ check_russian_mode:
call RenderRussianGame3D
jmp present_frame
draw_pause:
mov brEntryHold3D,0
invoke DrawBoxBorder, 28, 36, 264, 168, 00151C21h, 00D8BC6Ah, 2
invoke DrawString8x8, 44, 48, OFFSET textPause3D, 00FFFFFFh
cmp displayFill3D, 0
@ -5559,7 +5629,10 @@ pause_draw_quality_value:
invoke DrawString8x8, 44, 130, OFFSET textSoundLevel3D, 00BBBBBBh
invoke FormatNumber, settingsVolume, OFFSET hudNumBuffer
invoke DrawString8x8, 124, 130, OFFSET hudNumBuffer, 00FFFFFFh
invoke DrawString8x8, 44, 150, OFFSET textFullscreenKey3D, 00A0A0A0h
cmp brOwned3D,0
jne @F
invoke DrawString8x8, 44, 150, OFFSET textFullscreenKey3D, 00A0A0A0h
@@:
invoke DrawString8x8, 44, 170, OFFSET textResume3D, 00D8BC6Ah
invoke DrawString8x8, 44, 188, OFFSET textQuit3D, 00BBBBBBh
invoke Pressed3D, 31h
@ -5597,12 +5670,37 @@ pause_low_spec_saved:
call ApplySoundVolume
call SaveProgress3D
@@:
cmp brOwned3D,0
je br_pause_inventory_done
invoke DrawString8x8,44,150,OFFSET brToggle3D,00D8BC6Ah
invoke DrawString8x8,36,214,OFFSET brInventory3D,00D8BC6Ah
mov edx,OFFSET brDisabledText3D
cmp brEnabled3D,0
je @F
mov edx,OFFSET brEnabledText3D
@@:
invoke DrawString8x8,248,150,edx,00FFFFFFh
invoke Pressed3D,52h
test eax,eax
jz br_pause_inventory_done
xor brEnabled3D,1
call SaveProgress3D
br_pause_inventory_done:
invoke Pressed3D, 51h
test eax, eax
jz @F
call SaveProgress3D
jmp engine_shutdown
present_frame:
cmp brActive3D,0
je br_record_clock_done
cmp engineMode,MODE_WALK
je br_record_clock_done
cmp paused3D,0
jne br_record_clock_done
mov eax,deltaTime3D
add brClock3D,eax
br_record_clock_done:
call UpdateJukeboxAudio3D
call RenderTowerInGameBadge
call RenderJukeboxVolumeHUD
@ -5638,6 +5736,7 @@ present_frame:
invoke Sleep, ecx
jmp engine_loop
engine_idle:
mov brEntryHold3D,0
call UnlockMouse3D
invoke Sleep, 50
jmp engine_loop
@ -5659,6 +5758,7 @@ engine_shutdown:
@@:
call SaveProgress3D
invoke PlayCasinoSound3D, 0, 0, 0
invoke FreeAnimPack3D,OFFSET brMaterials3D
call FreeRussianAssets3D
call FreeDarts3D
call FreeStartupAssets3D

Binary file not shown.

View file

@ -820,7 +820,14 @@ save_stats_played_loop:
mov DWORD PTR [saveBuffer+296],eax
mov eax,charmEnabledMask3D
mov DWORD PTR [saveBuffer+300],eax
mov DWORD PTR [saveBuffer+304],2
mov eax,brOwned3D
shl eax,16
mov ecx,brEnabled3D
and ecx,brOwned3D
shl ecx,17
or eax,ecx
or eax,2
mov DWORD PTR [saveBuffer+304],eax
invoke ArtifactSaveSignature3D,OFFSET saveBuffer
mov DWORD PTR [saveBuffer+308],eax
call BuildSaveFullPath
@ -887,6 +894,14 @@ load_file_opened:
mov charmDevilUses,eax
mov eax,DWORD PTR [saveBuffer+300]
mov charmEnabledMask3D,eax
mov eax,DWORD PTR [saveBuffer+304]
shr eax,16
and eax,1
mov brOwned3D,eax
mov eax,DWORD PTR [saveBuffer+304]
shr eax,17
and eax,1
mov brEnabled3D,eax
load_signature_ok:
mov eax, DWORD PTR [saveBuffer+4]
mov bankroll, eax
@ -2043,7 +2058,14 @@ test_error_exit_engine:
RunHellTowerTest3D ENDP
ENDIF
IFDEF BACKROOMS_TEST
include tools/tests/backrooms_test.inc
ENDIF
main PROC
IFDEF BACKROOMS_TEST
call RunBackroomsTest3D
invoke ExitProcess,eax
ENDIF
IFDEF HELL_TOWER_TEST
call RunHellTowerTest3D
invoke ExitProcess,eax

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -1,4 +1,7 @@
HandleArtifactShop3D PROC uses ebx
call HandleBackroomsInventory3D
test eax,eax
jnz artifact_shop_done
invoke Pressed3D,09h
test eax,eax
jz @F
@ -95,6 +98,11 @@ HandleArtifactShop3D ENDP
RenderArtifactShop3D PROC uses ebx esi edi
local y:DWORD,x:DWORD,ink:DWORD
cmp brInventoryOpen3D,0
je @F
call RenderBackroomsInventory3D
ret
@@:
invoke DrawBox,0,0,320,240,0015191Bh
invoke DrawBox,0,0,320,24,002B302Ah
invoke DrawString8x8,8,7,OFFSET artifactShopTitle3D,00EED5A0h
@ -140,7 +148,13 @@ artifact_shop_card:
invoke DrawString8x8,120,77,charmDescs3D[ebx*4],00FFFFFFh
; Detail descriptions use full width below cards to avoid clipped text.
invoke DrawString8x8,8,216,artifactRules3D[ebx*4],00DDCDAAh
cmp brOwned3D,0
je @F
invoke DrawString8x8,8,228,OFFSET brInventoryHelp3D,00D8BC6Ah
jmp br_shop_help_done
@@:
invoke DrawString8x8,8,228,OFFSET artifactHelp3D,0099AAA5h
br_shop_help_done:
invoke DrawString8x8,120,96,OFFSET artifactPack3D,0099AAA5h
invoke FormatNumber,charmUsesPack3D[ebx*4],OFFSET artifactNum3D
invoke DrawString8x8,280,96,OFFSET artifactNum3D,00FFFFFFh

View file

@ -32,8 +32,15 @@ ValidateArtifactSave3D PROC uses esi edi source:DWORD
mov esi,source
cmp DWORD PTR [esi+280],32465241h
jne artifact_save_invalid
cmp DWORD PTR [esi+304],2
mov eax,[esi+304]
and eax,0FFFCFFFFh
cmp eax,2
jne artifact_save_invalid
test DWORD PTR [esi+304],20000h
jz @F
test DWORD PTR [esi+304],10000h
jz artifact_save_invalid
@@:
invoke ArtifactSaveSignature3D,esi
cmp eax,[esi+308]
jne artifact_save_invalid
@ -65,6 +72,8 @@ ArtifactCredit3D PROC amount:DWORD
ArtifactCredit3D ENDP
ResetArtifacts3D PROC
mov brOwned3D,0
mov brEnabled3D,1
mov charmCloverUses,0
mov charmHorseshoeUses,0
mov charmSkullUses,0

1068
src_graphics/backrooms.inc Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,86 @@
; Dedicated world/session. All coordinates are 16.16, all time is active ms.
HALL_BACKROOMS equ 6
BR_WALL equ 180
BR_FLOOR equ 181
BR_CEILING equ 182
BR_LAMP equ 183
BR_EXIT equ 184
BackroomsEntry3D PROTO
BeginBackrooms3D PROTO
GenerateBackrooms3D PROTO
HandleBackrooms3D PROTO
RenderBackrooms3D PROTO
LeaveBackrooms3D PROTO :DWORD
BackroomsLuck3D PROTO
BackroomsWheelLuck3D PROTO :DWORD
BackroomsDeck3D PROTO :DWORD,:DWORD,:DWORD
BackroomsBlackjackDeck3D PROTO
BackroomsPokerDeck3D PROTO
BackroomsDurakDeck3D PROTO
BackroomsBuckshotLuck3D PROTO
BackroomsRussianLuck3D PROTO
BackroomsRussianCylinder3D PROTO
LoadBackroomsAssets3D PROTO
HandleBackroomsInventory3D PROTO
RenderBackroomsInventory3D PROTO
.data
brActive3D dd 0
brEntryHold3D dd 0
brEntryAngle3D dd 0
brCutscene3D dd 0
brClock3D dd 0
brSkullsLuck3D dd -1
brSlotLuck3D dd -1
brRussianLuck3D dd -1
brBuckshotLuck3D dd -1
brDeckTargets3D dd 16 dup(0)
brDialogue3D dd 0
brOwned3D dd 0
brEnabled3D dd 1
brVhs3D dd 1
brExitCell3D dd 0
brTarget3D dd -1
brInventoryOpen3D dd 0
brMap3D db 1024 dup(BR_WALL)
brDistance3D dd 1024 dup(-1)
brStack3D dd 1024 dup(0)
brCandidates3D dd 4 dup(0)
brSteps3D dd -64,2,64,-2
brGames3D dd INTERACT_COIN,INTERACT_DICE,INTERACT_SLOTS_777,INTERACT_BLACKJACK,INTERACT_ROULETTE,INTERACT_SLOTS_CLOVER
brGameCells3D dd 6 dup(0)
brGamePacks3D dd OFFSET hallCardFurniture3D,OFFSET hallCardFurniture3D,OFFSET hallSlotFurniture3D,OFFSET hallCardFurniture3D,OFFSET hallWheelFurniture3D,OFFSET hallSlotFurniture3D
brGameFrames3D dd 0,0,0,0,0,1
brGamePrompts3D dd OFFSET brCoinPrompt3D,OFFSET brDicePrompt3D,OFFSET brSlotPrompt3D,OFFSET brCardPrompt3D,OFFSET brWheelPrompt3D,OFFSET brCloverPrompt3D
brCoinPrompt3D db 'E: COIN',0
brDicePrompt3D db 'E: DICE',0
brSlotPrompt3D db 'E: 777',0
brCardPrompt3D db 'E: BLACKJACK',0
brWheelPrompt3D db 'E: ROULETTE',0
brCloverPrompt3D db 'E: CLOVERPIT',0
brProp3D CityProp3D <0,0,FP_1*3/5,FP_1*4/5,0,OFFSET hallCardFurniture3D>
brMaterials3D AnimPack3D <0>
brMaterialPath3D db "assets\sprites\backrooms\materials.bin",0
brRec3D db 'REC',0
brTime3D db '00:00:00',0
brExitPrompt3D db 'E: EXIT',0
brHelp3D db 'V: VHS ESC: PAUSE',0
brInventory3D db 'REALITY ERROR: 90% / INFINITE',0
brToggle3D db 'R: TOGGLE REALITY ERROR',0
brEnabledText3D db 'ON',0
brDisabledText3D db 'OFF',0
brRealityGlitch3D db 'REALITY ERROR',0
brAward3D db 'REALITY ERROR / INFINITE / 90%',0
brInventoryHelp3D db 'R: REALITY ERROR / E: TOGGLE',0
brInventoryReturn3D db 'R / TAB: BACK',0
brInventoryRules1 db '90% FAVORABLE RANDOM OUTCOME',0
brInventoryRules2 db 'NO CHARGES / NO DUPLICATES',0
brInventoryRules3 db 'NORMAL CASINO ONLY',0
brInventoryRules4 db 'OFF IN TOWER / BACKROOMS',0
brInventoryRules5 db 'SKILL / CARD RULES STILL APPLY',0
brInventoryRules6 db 'BILLIARDS/BOWLING/DARTS UNCHANGED',0
brGlyph3D db 0,0
; Exact CP1251 dialogue, wrapped only at spaces by the renderer.
brText3D db 221,242,238,32,237,229,32,242,243,242,44,32,242,251,32,237,229,32
db 228,238,235,230,229,237,32,225,251,242,252,32,231,228,229,241,252,44,32
db 241,250,229,225,232,32,239,238,32,225,240,224,242,241,234,232,32,239,230,0
.code

View file

@ -97,6 +97,7 @@ bs_reload_next:
inc bs3d_blankCount
dec bs3d_liveCount
@@:
call BackroomsBuckshotLuck3D
; The loading motion loops once per actual cartridge, then racks the pump.
mov eax, bs3d_shellCount
imul eax, 330

View file

@ -5,6 +5,19 @@ cityRayY3D dd SCREEN_W dup(?)
; Exterior-only materials do not consume or overwrite the hall's texture slots.
ResolveCitySurface3D PROC material:DWORD
mov eax,material
cmp hallLevel3D,HALL_BACKROOMS
jne br_surface_other_world
cmp eax,BR_WALL
jb @F
cmp eax,BR_EXIT
ja @F
shl eax,16
add eax,OFFSET texturesBuffer
mov ecx,128
mov edx,128
ret
@@:
br_surface_other_world:
cmp eax,BW_MATERIAL
jb @F
cmp eax,BW_LAST_MATERIAL
@ -290,6 +303,27 @@ city_vertical_along:
dec edx
and eax, edx
@@:
; Door lettering faces the viewer on either wall face. Other worlds keep UVs.
cmp hallLevel3D,HALL_BACKROOMS
jne br_door_uv_done
cmp material,BR_EXIT
jne br_door_uv_done
cmp axis,0
jne br_door_uv_y
mov edx,playerX
cmp edx,coordinate
jle br_door_uv_done
jmp br_door_uv_flip
br_door_uv_y:
mov edx,playerY
cmp edx,coordinate
jge br_door_uv_done
br_door_uv_flip:
mov edx,textureW
dec edx
sub edx,eax
mov eax,edx
br_door_uv_done:
mov sourceX, eax
mov eax, viewEye3D
sub eax, zHigh

View file

@ -19,7 +19,13 @@ hall_plane_row:
sub ecx, eax
mov eax, viewCeiling3D
sub eax, viewEye3D
mov material, 101
mov material, 101
cmp brActive3D,0
je @F
cmp hallLevel3D,HALL_BACKROOMS
jne @F
mov material,BR_CEILING
@@:
cmp hallLevel3D,2
jne @F
mov material,125
@ -130,7 +136,27 @@ hall_plane_crossroad:
jl hall_plane_sample
hall_plane_sidewalk:
mov ebx, 72
hall_plane_sample:
hall_plane_sample:
cmp brActive3D,0
je br_plane_sample_done
cmp hallLevel3D,HALL_BACKROOMS
jne br_plane_sample_done
mov eax,row
cmp eax,viewHorizon3D
jae br_plane_sample_done
mov ebx,BR_CEILING
mov eax,wx
shr eax,16
and eax,3
cmp eax,1
jne br_plane_sample_done
mov eax,wy
shr eax,16
and eax,3
cmp eax,1
jne br_plane_sample_done
mov ebx,BR_LAMP
br_plane_sample_done:
cmp outdoorRender3D, 0
jne hall_plane_low_sample
mov eax, row

View file

@ -120,6 +120,7 @@ StartNativeBlackjack3D PROC
mov cbDealerCount3D, 2
mov cbCursor3D, 4
invoke ShuffleCards3D, OFFSET cbDeck3D
call BackroomsBlackjackDeck3D
mov eax, cbDeck3D[0]
mov cbPlayer3D[0], eax
mov eax, cbDeck3D[4]

View file

@ -104,6 +104,41 @@ clp_start_rows:
inc esi
cmp esi,5
jb clp_start_column
call BackroomsLuck3D
cmp eax,-1
je br_clover_fixed
mov ebx,eax
xor esi,esi
br_clover_row:
xor edi,edi
br_clover_column:
mov eax,esi
imul eax,2
add eax,edi
xor edx,edx
mov ecx,7
div ecx
cmp ebx,1
jne @F
mov edx,1
@@:
mov eax,esi
imul eax,5
add eax,edi
mov clpGrid3D[eax*4],edx
mov eax,clpSteps3D[edi*4]
add eax,esi
mov ecx,edi
shl ecx,8
lea ecx,clpTape3D[ecx]
mov [ecx+eax*4],edx
inc edi
cmp edi,5
jb br_clover_column
inc esi
cmp esi,3
jb br_clover_row
br_clover_fixed:
call SaveProgress3D
invoke PlayCasinoSound3D,OFFSET soundSlotSpin,0,1
clp_start_done:

View file

@ -308,6 +308,16 @@ coin_native_start:
invoke RandomRange3D,2
inc eax
mov coin3d_result,eax
call BackroomsLuck3D
cmp eax,-1
je br_coin_fixed
xor eax,1
mov ecx,coin3d_choice
dec ecx
xor eax,ecx
inc eax
mov coin3d_result,eax
br_coin_fixed:
call SaveProgress3D
invoke PlayCasinoSound3D,OFFSET soundCoin,0,1
call ClearKeys3D

View file

@ -40,6 +40,7 @@ dk_compact:
inc esi
cmp esi, 52
jb dk_compact
call BackroomsDurakDeck3D
mov eax, dkDeck3D[35*4]
mov durakTrumpCard, eax
invoke DurakCardSuit, eax

View file

@ -75,6 +75,7 @@ StartNativePoker3D PROC uses ebx esi
mov pkRaise3D, eax
mov pkCurrent3D, eax
invoke ShuffleCards3D, OFFSET pkDeck3D
call BackroomsPokerDeck3D
mov esi, pkButton3D
xor esi, 1
shl esi, 3 ; first card goes to the non-button player

View file

@ -69,11 +69,22 @@ StartNativeSlots3D PROC uses ebx esi
mov slotLeaveAfter3D,0
mov slotError3D,0
mov slotEffects3D,0
call BackroomsLuck3D
mov brSlotLuck3D,eax
xor esi,esi
slot_start_reel:
mov eax,slotStops3D[esi*4]
mov slotStartStops3D[esi*4],eax
invoke RandomRange3D,20
cmp brSlotLuck3D,-1
je br_slot_target
mov eax,0
cmp brSlotLuck3D,1
je br_slot_target
; Distinct symbols, with no refund pair, for the unlucky 10%.
mov eax,esi
inc eax
br_slot_target:
mov slotTargets3D[esi*4],eax
mov edx,slotStrip3D[eax*4]
mov slotReel1[esi*4],edx

View file

@ -352,6 +352,7 @@ wheel_regular_spin:
mov roulState, 1
mov wheelTime3D, 0
invoke RandomRange3D, 37
invoke BackroomsWheelLuck3D,eax
mov wheelResult3D, eax
call SaveProgress3D
invoke PlayCasinoSound3D, OFFSET soundRoulette,0,1

View file

@ -137,6 +137,12 @@ event_key_up:
cmp ecx, 255
ja event_done
mov keys3D[ecx], 0
cmp ecx,57h
je event_entry_release
cmp ecx,26h
jne event_done
event_entry_release:
mov brEntryHold3D,0
jmp event_done
event_fullscreen:
test lp, 40000000h
@ -144,6 +150,7 @@ event_fullscreen:
call ToggleFullscreen3D
jmp event_done
event_focus_lost:
mov brEntryHold3D,0
mov dtHolding3D,0
mov dtMouseReleased3D,0
call ClearKeys3D
@ -505,9 +512,12 @@ UpdateCamera3D ENDP
; Four corners of a 0.3-tile-wide body; bounds checked before every map read.
CanWalk3D PROC uses ebx esi edi px:DWORD, py:DWORD
cmp brActive3D,0
jne br_generic_collision
invoke CanWalkHall3D, px, py
test eax, eax
jz walk_blocked
br_generic_collision:
mov esi, -9830
walk_corner_x:
mov edi, -9830
@ -550,6 +560,8 @@ LoadProgress3D PROC uses ebx
mov charmSkullUses, 0
mov charmDevilUses, 0
mov charmEnabledMask3D,7
mov brOwned3D,0
mov brEnabled3D,1
invoke CreateFileA, OFFSET savePath3D, 80000000h, 1, 0, 3, 80h, 0
cmp eax, -1
je progress_loaded
@ -566,7 +578,23 @@ LoadProgress3D PROC uses ebx
cmp eax,saveData3D[36]
jne progress_loaded
mov eax,saveData3D[32]
mov charmEnabledMask3D,eax
test eax,0FFFCFFF0h
jnz progress_loaded
test eax,20000h
jz @F
test eax,10000h
jz progress_loaded
@@:
mov ecx,eax
and ecx,15
mov charmEnabledMask3D,ecx
shr eax,16
and eax,1
mov brOwned3D,eax
mov eax,saveData3D[32]
shr eax,17
and eax,1
mov brEnabled3D,eax
jmp progress_version_ok
progress_legacy_version:
cmp saveData3D, 33443343h
@ -653,7 +681,15 @@ SaveProgress3D PROC uses ebx
mov saveData3D[24], eax
mov eax, charmDevilUses
mov saveData3D[28], eax
mov eax,charmEnabledMask3D
mov eax,brOwned3D
and eax,1
shl eax,16
mov ecx,brEnabled3D
and ecx,brOwned3D
and ecx,1
shl ecx,17
or eax,ecx
or eax,charmEnabledMask3D
mov saveData3D[32],eax
invoke PreviewArtifactSignature3D,OFFSET saveData3D
mov saveData3D[36],eax

View file

@ -69,6 +69,10 @@ rr_party_cartridge:
jb rr_party_cartridge
invoke RandomRange3D,6
mov rr3d_chamber,eax
cmp rrPartyReloads3D,0
je @F
call BackroomsRussianCylinder3D
@@:
inc rrPartyReloads3D
ret
RRLoadCylinder3D ENDP

View file

@ -98,9 +98,11 @@ StartRussianRound3D PROC uses ebx esi
sub bankroll, eax
mov rr3d_payout, 0
mov rr3d_error, 0
mov brRussianLuck3D,-1
call RRLoadCylinder3D
invoke RandomRange3D, rrPartySize3D
mov rr3d_turn, eax
call BackroomsRussianLuck3D
mov rr3d_state, RR_LOAD
mov rr3d_timer, 0
invoke PlayCasinoSound3D, OFFSET rrLoadSound3D, 0, 20003h

View file

@ -246,6 +246,16 @@ BuyScratch3D PROC uses ebx esi edi
jmp scratch_buy_save
@@:
invoke RandomRange3D,1000
push eax
call BackroomsLuck3D
pop edx
cmp eax,-1
je br_scratch_original
imul eax,650
jmp br_scratch_fixed
br_scratch_original:
mov eax,edx
br_scratch_fixed:
cmp eax,600
jb scratch_losing_bag
xor esi,esi

View file

@ -321,6 +321,16 @@ portrait_fill:
jb portrait_fill
mov scrPortraitOutcome3D,0
invoke RandomRange3D,1000
push eax
call BackroomsLuck3D
pop edx
cmp eax,-1
je br_portrait_original
imul eax,920
jmp br_portrait_fixed
br_portrait_original:
mov eax,edx
br_portrait_fixed:
cmp eax,600
jb portrait_build_payout
mov ebx,eax

View file

@ -0,0 +1,32 @@
"""Offline-only texture packing; runtime generation/rendering stays MASM."""
from pathlib import Path
import struct
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[2]
def main():
source = ROOT / 'assets/sprites/backrooms'
frames = []
for name in ('wall', 'floor', 'ceiling', 'ceiling_lamp'):
image = Image.open(source / f'Backrooms_{name}.jpg').convert('RGBA')
image = image.resize((128, 128), Image.Resampling.LANCZOS)
frames.append(image.tobytes('raw', 'BGRA'))
# Exit is an offline-generated material, not a second game implementation.
image = Image.new('RGBA', (128, 128), (36, 60, 38, 255))
draw = ImageDraw.Draw(image)
draw.rectangle((9, 4, 119, 127), fill=(61, 84, 55), outline=(143, 160, 98), width=5)
# Eye-level sign stays visible even when standing in the exit cell.
draw.rectangle((18, 45, 110, 73), fill=(32, 170, 77))
font = ImageFont.truetype('arialbd.ttf', 23)
draw.text((64, 59), 'EXIT', fill='white', font=font, anchor='mm')
draw.rectangle((94, 76, 99, 82), fill=(240, 211, 101))
frames.append(image.tobytes('raw', 'BGRA'))
out = source / 'materials.bin'
out.write_bytes(struct.pack('<5I', 0x314D4E41, 128, 128, len(frames), len(frames)*65536) + b''.join(frames))
print(f'Packed {len(frames)} Backrooms materials: {out.relative_to(ROOT)}')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,391 @@
; Additional native invariants. No player files or runtime writes.
RunBackroomsExtendedTest3D PROC uses ebx esi edi
local rounds:DWORD,wins:DWORD,kind:DWORD,party:DWORD,bullets:DWORD,steps:DWORD
mov brOwned3D,1
mov brEnabled3D,0
mov rngSeed3D,87654
call BackroomsLuck3D
cmp eax,-1
jne br_extended_fail
cmp rngSeed3D,87654
jne br_extended_fail
mov brEnabled3D,1
mov obsActive3D,1
call BackroomsLuck3D
cmp eax,-1
jne br_extended_fail
mov obsActive3D,0
mov brTestStage,111
; Actual blackjack party dealing, all guest counts.
mov party,0
br_ext_bj_party:
mov wins,0
mov rounds,1000
br_ext_bj_round:
mov bankroll,1000000
mov eax,party
mov cbBotCount3D,eax
mov cbState3D,CB_IDLE
call StartNativeBlackjack3D
cmp cbPlayerScore3D,21
jne @F
inc wins
cmp cbDealerScore3D,19
jne br_extended_fail
@@:
dec rounds
jnz br_ext_bj_round
cmp wins,860
jb br_extended_fail
cmp wins,940
ja br_extended_fail
inc party
cmp party,4
jb br_ext_bj_party
mov cbBotCount3D,0
; Every scratch design, generated by the actual purchase path.
mov scrReady3D,1
mov brTestStage,112
mov kind,0
br_ext_ticket_type:
mov eax,kind
add eax,120
mov brTestStage,eax
mov wins,0
mov rounds,1000
br_ext_ticket:
mov bankroll,1000000
mov scrState3D,0
mov scrPriceIndex3D,0
mov eax,kind
mov scrType3D,eax
call BuyScratch3D
mov eax,scrGross3D
test eax,eax
jz @F
inc wins
@@:
cmp kind,4
jb @F
call ScratchPortraitPayout3D
cmp eax,scrGross3D
jne br_extended_fail
@@:
dec rounds
jnz br_ext_ticket
cmp wins,860
jb br_extended_fail
cmp wins,940
ja br_extended_fail
inc kind
cmp kind,7
jb br_ext_ticket_type
; Roulette: a real straight-up bet, including actual multiplier matching.
lea edi,wheelBets3D
mov brTestStage,113
xor eax,eax
mov ecx,49
rep stosd
mov wheelBets3D[7*4],100
mov wheelStake3D,100
mov wins,0
mov rounds,1000
br_ext_wheel:
invoke BackroomsWheelLuck3D,0
cmp eax,36
ja br_extended_fail
invoke WheelBetMultiplier3D,7,eax
test eax,eax
jz @F
inc wins
@@:
dec rounds
jnz br_ext_wheel
cmp wins,860
jb br_extended_fail
cmp wins,940
ja br_extended_fail
; Durak: 36 distinct legal cards, correctly exposed trump on favorable deals.
mov rounds,1000
br_ext_durak:
mov brTestStage,114
mov bankroll,1000000
mov dkState3D,DK_IDLE
call StartNativeDurak3D
xor esi,esi
br_ext_durak_card:
mov eax,dkDeck3D[esi*4]
cmp eax,52
jae br_extended_fail
invoke DurakCardRank,eax
cmp eax,4
jb br_extended_fail
lea edi,[esi+1]
br_ext_durak_unique:
cmp edi,36
jae br_ext_durak_next
mov eax,dkDeck3D[edi*4]
cmp eax,dkDeck3D[esi*4]
je br_extended_fail
inc edi
jmp br_ext_durak_unique
br_ext_durak_next:
inc esi
cmp esi,36
jb br_ext_durak_card
cmp dkDeck3D,33
jne @F
cmp durakTrumpSuit,2
jne br_extended_fail
@@:
dec rounds
jnz br_ext_durak
; Russian roulette: real party elimination/reload rules, all 1..3 guests/bullets.
mov party,1
mov brTestStage,115
br_ext_rr_party:
mov bullets,1
br_ext_rr_bullets:
mov rounds,300
mov wins,0
br_ext_rr_round:
mov brTestStage,151
mov eax,party
mov rrPartyCount3D,eax
call RRPartyReset3D
mov eax,bullets
mov rr3d_bullets,eax
mov rrPartySettled3D,0
mov rrPartyStake3D,50
mov rr3d_bet,50
mov rr3d_turn,0
call BackroomsRussianLuck3D
mov rrPartyReloads3D,1
mov steps,0
br_ext_rr_shot:
mov eax,rr3d_mask
mov ecx,rr3d_chamber
bt eax,ecx
jnc br_ext_rr_advance
call RRPartyHit3D
cmp rrPartySettled3D,0
jne br_ext_rr_outcome
br_ext_rr_advance:
call RRPartyNext3D
inc steps
cmp steps,100
jae br_extended_fail
jmp br_ext_rr_shot
br_ext_rr_outcome:
mov brTestStage,152
cmp rr3d_state,RR_WIN
jne @F
inc wins
cmp brRussianLuck3D,1
jne br_extended_fail
@@:
cmp brRussianLuck3D,1
jne @F
mov eax,party
imul eax,10
add eax,bullets
add eax,200
mov brTestStage,eax
cmp rr3d_state,RR_WIN
jne br_extended_fail
@@:
dec rounds
jnz br_ext_rr_round
mov brTestStage,154
cmp wins,240
jb br_extended_fail
cmp wins,295
ja br_extended_fail
inc bullets
cmp bullets,4
jb br_ext_rr_bullets
inc party
cmp party,4
jb br_ext_rr_party
; Buckshot permutations retain cartridge counts and valid binary identities.
mov rounds,1000
br_ext_bs:
mov brTestStage,116
call BuckshotReload3D
xor edi,edi
xor esi,esi
br_ext_bs_shell:
mov eax,bs3d_shells[esi*4]
cmp eax,1
ja br_extended_fail
add edi,eax
inc esi
cmp esi,bs3d_shellCount
jb br_ext_bs_shell
cmp edi,bs3d_liveCount
jne br_extended_fail
add edi,bs3d_blankCount
cmp edi,bs3d_shellCount
jne br_extended_fail
dec rounds
jnz br_ext_bs
xor eax,eax
ret
br_extended_fail:
mov eax,brTestStage
ret
RunBackroomsExtendedTest3D ENDP
RunBackroomsSaveTest3D PROC uses ebx esi edi
; Signed but impossible enabled-without-owned flag must be rejected.
mov DWORD PTR saveBuffer[304],20002h
invoke ArtifactSaveSignature3D,OFFSET saveBuffer
mov DWORD PTR saveBuffer[308],eax
invoke ValidateArtifactSave3D,OFFSET saveBuffer
test eax,eax
jnz br_save_fail
; Native preview keeps its 40-byte, signed backward-compatible layout.
invoke CopyCString,OFFSET savePath3D,OFFSET brTestPreviewPath
mov nativeProfileActive3D,0
mov arrivalConfirmed3D,1
mov brOwned3D,1
mov brEnabled3D,1
call SaveProgress3D
mov brOwned3D,0
mov brEnabled3D,0
call LoadProgress3D
cmp brOwned3D,1
jne br_save_fail
cmp brEnabled3D,1
jne br_save_fail
mov brActive3D,1
mov brEnabled3D,0
invoke LeaveBackrooms3D,1
cmp brOwned3D,1
jne br_save_fail
cmp brEnabled3D,0
jne br_save_fail
call LoadProgress3D
cmp brEnabled3D,0
jne br_save_fail
xor eax,eax
ret
br_save_fail:
mov eax,102
ret
RunBackroomsSaveTest3D ENDP
.data
brContextModes dd MODE_COIN,MODE_DICE,MODE_SLOTS,MODE_BLACKJACK,MODE_ROULETTE,MODE_CLOVER
brContextMap db 1024 dup(0)
.code
RunBackroomsContextTest3D PROC uses ebx esi edi
local index:DWORD,x:DWORD,y:DWORD
call GenerateBackrooms3D
mov brActive3D,1
mov brCutscene3D,0
mov hallLevel3D,HALL_BACKROOMS
mov worldMap3D,OFFSET brMap3D
lea esi,brMap3D
lea edi,brContextMap
mov ecx,256
rep movsd
mov index,0
br_context_game:
mov esi,index
mov ebx,brGameCells3D[esi*4]
mov eax,ebx
and eax,31
shl eax,16
add eax,FP_HALF
mov x,eax
mov playerX,eax
mov eax,ebx
shr eax,5
shl eax,16
add eax,FP_HALF
mov y,eax
mov playerY,eax
mov playerAngle,77
mov engineMode,MODE_WALK
mov mouseLocked3D,0
mov deltaTime3D,16
call ClearKeys3D
mov pressedKeys3D[45h],1
call HandleBackrooms3D
mov esi,index
mov eax,brContextModes[esi*4]
cmp eax,engineMode
jne br_context_fail
cmp eax,MODE_DICE
jne @F
call DiceEscape3D
jmp br_context_returned
@@:
call CardTableEscape3D
br_context_returned:
cmp engineMode,MODE_WALK
jne br_context_fail
mov eax,x
cmp playerX,eax
jne br_context_fail
mov eax,y
cmp playerY,eax
jne br_context_fail
cmp playerAngle,77
jne br_context_fail
cmp worldMap3D,OFFSET brMap3D
jne br_context_fail
cmp hallLevel3D,HALL_BACKROOMS
jne br_context_fail
lea esi,brMap3D
lea edi,brContextMap
mov ecx,1024
repe cmpsb
jne br_context_fail
inc index
cmp index,6
jb br_context_game
mov brActive3D,0
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
xor eax,eax
ret
br_context_fail:
mov eax,103
ret
RunBackroomsContextTest3D ENDP
RunBackroomsWallTest3D PROC uses esi
mov brActive3D,0
mov engineMode,MODE_WALK
mov mouseLocked3D,0
mov playerX,16*FP_1+FP_HALF
mov playerY,27*FP_1+FP_HALF
mov playerAngle,128
mov deltaTime3D,16
mov brEntryHold3D,0
call ClearKeys3D
mov keys3D[57h],1
mov esi,500
br_wall_walk:
call HandlePlayerInput
call BackroomsEntry3D
cmp brActive3D,1
je br_wall_entered
dec esi
jnz br_wall_walk
mov eax,104
ret
br_wall_entered:
cmp playerY,30*FP_1
jae br_wall_fail
cmp playerY,30*FP_1-15000
jb br_wall_fail
call ClearKeys3D
xor eax,eax
ret
br_wall_fail:
mov eax,105
ret
RunBackroomsWallTest3D ENDP

View file

@ -0,0 +1,288 @@
.data
brTestPath db 'build\scratch\backrooms-native.bin',0
brTestFrame db 'build\scratch\backrooms-frame.bin',0
brTestExitFrame db 'build\scratch\backrooms-exit.bin',0
brTestIo dd 0
brTestSeeds dd 256
brTestStage dd 91
brTestRows dd 0
brTestLuckPath db 'build\scratch\backrooms-luck.bin',0
brTestProfileDir db 'build\scratch',0
brTestProfileName db 'backrooms-profile.dat',0
brTestProfilePath db 'build\scratch\backrooms-profile.dat',0
brTestPreviewPath db 'build\scratch\br-preview.dat',0
brTestFacesPath db 'build\scratch\backrooms-exit-faces.bin',0
brTestFaceNeighbors dd -32,32,-1,1
brTestFaceAngles dd 384,128,256,0
.code
include tools/tests/backrooms_extended_test.inc
RunBackroomsTest3D PROC uses ebx esi edi
invoke CreateFileA,OFFSET brTestPath,40000000h,0,0,2,80h,0
mov ebx,eax
cmp eax,-1
je br_test_fail
mov rngSeed3D,12345
br_test_seed:
call GenerateBackrooms3D
invoke WriteFile,ebx,OFFSET brMap3D,1024,OFFSET brTestIo,0
invoke WriteFile,ebx,OFFSET brExitCell3D,4,OFFSET brTestIo,0
invoke WriteFile,ebx,OFFSET brGameCells3D,24,OFFSET brTestIo,0
dec brTestSeeds
jnz br_test_seed
invoke CloseHandle,ebx
mov brTestStage,92
mov obsActive3D,0
mov tournamentMode,0
mov brActive3D,0
mov engineMode,MODE_WALK
mov hallLevel3D,0
mov playerX,16*FP_1+FP_HALF
mov playerY,30*FP_1-11000
mov playerAngle,128
mov keys3D[57h],1
mov deltaTime3D,50
mov esi,39
br_test_entry:
call BackroomsEntry3D
cmp brActive3D,0
jne br_test_fail
dec esi
jnz br_test_entry
cmp brEntryHold3D,1950
jne br_test_fail
mov brTestStage,93
mov playerAngle,129
call BackroomsEntry3D
cmp brEntryHold3D,0
jne br_test_fail
mov playerAngle,128
call BackroomsEntry3D
cmp brEntryHold3D,50
jne br_test_fail
mov keys3D[57h],0
call BackroomsEntry3D
cmp brEntryHold3D,0
jne br_test_fail
mov brTestStage,94
mov keys3D[57h],1
mov brEntryHold3D,1950
mov brEntryAngle3D,128
call BackroomsEntry3D
cmp brActive3D,1
jne br_test_fail
cmp brCutscene3D,1
jne br_test_fail
mov brClock3D,900
call HandleBackrooms3D
mov brTestStage,95
invoke CanWalk3D,FP_1+FP_HALF,FP_1+FP_HALF
cmp eax,1
jne br_test_fail
invoke CanWalk3D,FP_HALF,FP_HALF
test eax,eax
jnz br_test_fail
mov brTestStage,96
; Set up standard projection LUT through the real hall initializer first.
call InitHall3D
call RunBackroomsWallTest3D
test eax,eax
jnz br_test_return
mov brActive3D,1
mov hallLevel3D,HALL_BACKROOMS
mov worldMap3D,OFFSET brMap3D
mov floorTexture3D,BR_FLOOR
mov viewHorizon3D,HALF_H
mov brClock3D,8100
mov brDialogue3D,2500
mov brCutscene3D,0
mov playerX,FP_1+FP_HALF
mov playerY,FP_1+FP_HALF
mov playerAngle,64
call RenderBackrooms3D
invoke CreateFileA,OFFSET brTestFrame,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET screenBuffer,SCREEN_SIZE*4,OFFSET brTestIo,0
invoke CloseHandle,ebx
; Door capture from its reachable exit cell, facing a neighboring wall.
mov ebx,brExitCell3D
mov eax,ebx
and eax,31
shl eax,16
add eax,FP_HALF
mov playerX,eax
mov eax,ebx
shr eax,5
shl eax,16
add eax,FP_HALF
mov playerY,eax
mov playerAngle,384
cmp brMap3D[ebx-32],0
jne br_test_exit_camera
mov playerAngle,128
cmp brMap3D[ebx+32],0
jne br_test_exit_camera
mov playerAngle,256
cmp brMap3D[ebx-1],0
jne br_test_exit_camera
mov playerAngle,0
br_test_exit_camera:
mov brDialogue3D,10000
call RenderBackrooms3D
invoke CreateFileA,OFFSET brTestExitFrame,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET screenBuffer,SCREEN_SIZE*4,OFFSET brTestIo,0
invoke CloseHandle,ebx
mov arrivalConfirmed3D,0
call RunBackroomsDoorFrames3D
invoke LeaveBackrooms3D,1
cmp brOwned3D,1
jne br_test_fail
cmp brActive3D,0
jne br_test_fail
cmp hallLevel3D,0
jne br_test_fail
mov brTestStage,97
call RunBackroomsContextTest3D
test eax,eax
jnz br_test_return
mov brActive3D,1
call BackroomsLuck3D
cmp eax,-1
jne br_test_fail
mov brActive3D,0
mov tournamentMode,1
call BackroomsLuck3D
cmp eax,-1
jne br_test_fail
mov tournamentMode,0
xor edi,edi
mov esi,100000
br_test_luck:
call BackroomsLuck3D
add edi,eax
dec esi
jnz br_test_luck
cmp edi,89000
jb br_test_fail
cmp edi,91000
ja br_test_fail
call RunBackroomsExtendedTest3D
test eax,eax
jnz br_test_return
mov brTestStage,98
call LoadNativeCoinArt3D
call InitNativeSlots3D
call InitNativeClover3D
call InitNativeBlackjack3D
call InitNativePoker3D
invoke CreateFileA,OFFSET brTestLuckPath,40000000h,0,0,2,80h,0
mov ebx,eax
mov brTestRows,1000
br_test_rounds:
mov bankroll,1000000
mov coin3d_choice,1
mov coin3d_state,COIN_IDLE
mov pressedKeys3D[20h],1
call HandleNativeCoin3D
invoke WriteFile,ebx,OFFSET coin3d_result,4,OFFSET brTestIo,0
mov dice3d_state,0
mov dice3d_choice,1
mov pressedKeys3D[20h],1
call HandleDiceRules3D
invoke WriteFile,ebx,OFFSET dice3d_val1,8,OFFSET brTestIo,0
mov slotSpinState,SLOT_READY
call StartNativeSlots3D
invoke WriteFile,ebx,OFFSET slotReel1,12,OFFSET brTestIo,0
mov clpState3D,SLOT_READY
call StartNativeClover3D
call CloverEvaluate3D
invoke WriteFile,ebx,OFFSET clpTotal3D,4,OFFSET brTestIo,0
mov cbBotCount3D,0
mov cbState3D,CB_IDLE
call StartNativeBlackjack3D
invoke WriteFile,ebx,OFFSET cbPlayerScore3D,4,OFFSET brTestIo,0
invoke WriteFile,ebx,OFFSET cbDeck3D,208,OFFSET brTestIo,0
mov pkState3D,PK_IDLE
call StartNativePoker3D
invoke WriteFile,ebx,OFFSET pkDeck3D,208,OFFSET brTestIo,0
invoke WriteFile,ebx,OFFSET pkButton3D,4,OFFSET brTestIo,0
dec brTestRows
jnz br_test_rounds
invoke CloseHandle,ebx
mov brTestStage,99
mov nativeProfilesDirPtr3D,OFFSET brTestProfileDir
invoke CopyCString,OFFSET saveFileName,OFFSET brTestProfileName
mov nativeProfileActive3D,1
mov brOwned3D,1
mov brEnabled3D,0
call SaveGame
mov brOwned3D,0
mov brEnabled3D,1
call LoadSave
cmp brOwned3D,1
jne br_test_fail
cmp brEnabled3D,0
jne br_test_fail
invoke ValidateGraphicProfile3D,OFFSET brTestProfilePath
cmp eax,1
jne br_test_fail
call RunBackroomsSaveTest3D
test eax,eax
jnz br_test_return
xor eax,eax
ret
br_test_fail:
mov eax,brTestStage
br_test_return:
ret
RunBackroomsTest3D ENDP
; Identical door/camera distance on every face, VHS off/on pairs.
RunBackroomsDoorFrames3D PROC uses ebx esi edi
local face:DWORD,effect:DWORD
invoke CreateFileA,OFFSET brTestFacesPath,40000000h,0,0,2,80h,0
mov ebx,eax
mov face,0
br_test_door_face:
lea edi,brMap3D
xor eax,eax
mov ecx,256
rep stosd
; Keep all rays inside the fixture map, including peripheral FOV rays.
xor esi,esi
br_test_door_border:
mov brMap3D[esi],BR_WALL
mov brMap3D[esi+31*32],BR_WALL
mov eax,esi
shl eax,5
mov brMap3D[eax],BR_WALL
mov brMap3D[eax+31],BR_WALL
inc esi
cmp esi,32
jb br_test_door_border
mov brExitCell3D,16*32+16
mov esi,face
mov eax,brTestFaceNeighbors[esi*4]
mov brMap3D[eax+16*32+16],BR_WALL
mov playerX,16*FP_1+FP_HALF
mov playerY,16*FP_1+FP_HALF
mov eax,brTestFaceAngles[esi*4]
mov playerAngle,eax
mov viewEye3D,HALL_STANDING_EYE
mov viewHorizon3D,HALF_H
mov brDialogue3D,10000
mov effect,0
br_test_door_effect:
mov eax,effect
mov brVhs3D,eax
call RenderBackrooms3D
invoke WriteFile,ebx,OFFSET screenBuffer,SCREEN_SIZE*4,OFFSET brTestIo,0
inc effect
cmp effect,2
jb br_test_door_effect
inc face
cmp face,4
jb br_test_door_face
invoke CloseHandle,ebx
ret
RunBackroomsDoorFrames3D ENDP

View file

@ -0,0 +1,128 @@
"""Execute shipping MASM maze/entry/collision/render, inspect native output."""
from collections import deque
from pathlib import Path
import struct
import subprocess
from PIL import Image
from check_artifacts import toolchain
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'build/scratch'
def check_exit_faces():
raw = (OUT/'backrooms-exit-faces.bin').read_bytes()
assert len(raw) == 8*640*480*4
frames = [Image.frombytes('RGBA', (640, 480), raw[i:i+640*480*4],
'raw', 'BGRA').convert('RGB')
for i in range(0, len(raw), 640*480*4)]
# Source glyph at runtime's exact nearest-neighbor UV scale. Compare both
# orientations rather than merely counting green pixels (mirrors pass that).
pack = (ROOT/'assets/sprites/backrooms/materials.bin').read_bytes()
art = Image.frombytes('RGBA', (128, 128), pack[20+4*65536:], 'raw', 'BGRA')
glyph = [(x, y) for y in range(48, 70) for x in range(27, 101)
if min(art.getpixel((x, y))[:3]) > 240]
for face, name in enumerate(('north', 'south', 'west', 'east')):
clean, vhs = frames[2*face:2*face+2]
clean.save(OUT/f'backrooms-exit-{name}.png')
vhs.save(OUT/f'backrooms-exit-{name}-vhs.png')
white = [(x, y) for y in range(0, 300) for x in range(60, 580)
if min(clean.getpixel((x, y))) > 240]
assert white, f'{name}: EXIT missing'
x0, x1 = min(x for x, y in white), max(x for x, y in white)
y0, y1 = min(y for x, y in white), max(y for x, y in white)
gx0, gx1 = min(x for x, y in glyph), max(x for x, y in glyph)
gy0, gy1 = min(y for x, y in glyph), max(y for x, y in glyph)
def score(mirror):
hits = 0
for x, y in glyph:
u = (x-gx0)/(gx1-gx0)
if mirror:
u = 1-u
px = round(x0+u*(x1-x0))
py = round(y0+(y-gy0)/(gy1-gy0)*(y1-y0))
hits += min(clean.getpixel((px, py))) > 240
return hits/len(glyph)
normal, mirrored = score(False), score(True)
assert normal > .85 and normal > mirrored+.12, (name, normal, mirrored)
# White glyphs may darken on scanlines, never wrap into black/color noise.
corrupt = sum(min(vhs.getpixel(p)) < 180 or
max(vhs.getpixel(p))-min(vhs.getpixel(p)) > 12 for p in white)
assert corrupt == 0, (name, 'VHS corrupt white pixels', corrupt, len(white))
def main():
OUT.mkdir(parents=True, exist_ok=True)
ml, link = toolchain()
sdk = Path('C:/Program Files (x86)/Windows Kits/10/Lib')
lib = next(p/'um/x86' for p in sorted(sdk.iterdir(), reverse=True)
if (p/'um/x86/kernel32.lib').exists())
for args in ([ml, '/c', '/coff', '/Cp', '/nologo', '/I.', '/DBACKROOMS_TEST=1',
'/Fobuild/scratch/backrooms.obj', 'poker.asm'],
[link, '/nologo', '/SUBSYSTEM:CONSOLE', '/LIBPATH:'+str(lib),
'/OUT:build/scratch/backrooms-test.exe', 'build/scratch/backrooms.obj',
'kernel32.lib', 'winmm.lib', 'user32.lib', 'ws2_32.lib', 'gdi32.lib']):
subprocess.run(list(map(str, args)), cwd=ROOT, check=True, timeout=120)
subprocess.run([str(OUT/'backrooms-test.exe')], cwd=ROOT, check=True, timeout=90)
raw = (OUT/'backrooms-native.bin').read_bytes()
assert len(raw) == 256 * 1052
fingerprints = set()
minimum = 10000
for offset in range(0, len(raw), 1052):
grid = raw[offset:offset+1024]
exit_cell, *games = struct.unpack_from('<7I', raw, offset+1024)
assert all(grid[x] for x in range(32))
assert all(grid[31*32+x] for x in range(32))
assert all(grid[y*32] and grid[y*32+31] for y in range(32))
distance, queue = {33: 0}, deque([33])
while queue:
cell = queue.popleft()
for neighbor in (cell-32, cell+1, cell+32, cell-1):
if grid[neighbor] == 0 and neighbor not in distance:
distance[neighbor] = distance[cell]+1
queue.append(neighbor)
assert len(distance) == grid.count(0), 'Disconnected open component'
assert distance[exit_cell] == max(distance.values())
assert len(set(games)) == 6 and exit_cell not in games and 33 not in games
assert all(cell in distance for cell in games)
minimum = min(minimum, distance[exit_cell])
fingerprints.add(grid)
assert len(fingerprints) == 256
assert minimum >= 40, minimum
luck = (OUT/'backrooms-luck.bin').read_bytes()
assert len(luck) == 1000*452
wins = dict.fromkeys(('coin', 'dice', '777', 'clover', 'blackjack', 'poker'), 0)
for row in struct.iter_unpack('<113I', luck):
coin, d1, d2, s1, s2, s3, clover, bj = row[:8]
assert coin in (1, 2) and 1 <= d1 <= 6 and 1 <= d2 <= 6
wins['coin'] += coin == 1
wins['dice'] += (d1+d2) % 2 == 0
assert (s1, s2, s3) in ((1, 1, 1), (2, 5, 3))
wins['777'] += s1 == s2 == s3
assert clover in (0, 78)
wins['clover'] += clover > 0
deck, poker, button = row[8:60], row[60:112], row[112]
assert sorted(deck) == sorted(poker) == list(range(52)), 'Duplicate/missing card'
assert bj in (19, 21)
assert bj == (21 if deck[0] == 12 else 19)
wins['blackjack'] += bj == 21
# Actual non-button-first deal order, plus actual burn/flop/turn/river offsets.
seat = button ^ 1
hand = [poker[seat], poker[seat+2], *(poker[i] for i in (5, 6, 7, 9, 11))]
wins['poker'] += set(range(8, 13)).issubset(hand)
assert all(860 <= n <= 940 for n in wins.values()), wins
image = Image.frombytes('RGBA', (640, 480), (OUT/'backrooms-frame.bin').read_bytes(), 'raw', 'BGRA').convert('RGB')
image.save(OUT/'backrooms-frame.png')
assert len(image.getcolors(640*480)) > 100
image = Image.frombytes('RGBA', (640, 480), (OUT/'backrooms-exit.bin').read_bytes(), 'raw', 'BGRA').convert('RGB')
image.save(OUT/'backrooms-exit.png')
assert sum(g > 1.4*r and g > 1.2*b for r, g, b in image.get_flattened_data()) > 100, 'Exit not visibly green'
check_exit_faces()
print('PASS: readable non-mirrored EXIT on all four wall faces; VHS preserves white glyphs')
print(f'PASS: 256 native seeds; sealed connected mazes; farthest exit >= {minimum} steps; unique reachable games; entry hold/reset; collision; isolation; profile roundtrip; render')
print(f'PASS: 1000 production rounds per game, valid unique decks, wins: {wins}')
print('PASS: all blackjack party sizes; 7 scratch designs; roulette; Durak conservation/trump; 9 Russian party/bullet setups; Buckshot counts; preview saves; repeat reward')
if __name__ == '__main__':
main()