# Conflicts:
#	build/graphics/poker.exe
#	poker.exe
This commit is contained in:
Игорь Федосеев 2026-10-28 10:00:47 +03:00
commit 434cc40799
81 changed files with 26121 additions and 110 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View file

@ -78,3 +78,7 @@ outside the image.
Deterministically generated exact 20-sector sports geometry, not AI artwork.
The sector order and scoring ring radii are shared with native scoring tests.
`board-v2.png` is the deterministic readability revision: larger, brighter
numerals and cleaner wires, drawn at 2x then reduced to the same 512px canvas.
It uses no image-generation model and does not change the board's world size.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,105 @@
{
"canvas": [
192,
192
],
"font": "Cambria Regular, rasterized at 4x; no font needed at runtime",
"frames": [
{
"cx": 107.0,
"cy": 85.60757294157031,
"rx": 72.68997989989911,
"ry": 74.69516621989159
},
{
"cx": 95.5,
"cy": 85.76519319889574,
"rx": 72.51196201438557,
"ry": 74.89806065214609
},
{
"cx": 95.5,
"cy": 85.71241583054908,
"rx": 72.69942242784161,
"ry": 74.83168393851604
},
{
"cx": 84.0,
"cy": 85.72005975738783,
"rx": 72.63601648302654,
"ry": 74.68751926061978
},
{
"cx": 106.5,
"cy": 82.20860810943547,
"rx": 72.87681495588859,
"ry": 74.48180234847793
},
{
"cx": 95.0,
"cy": 82.2684885845598,
"rx": 73.25393481375772,
"ry": 74.62519501114646
},
{
"cx": 95.0,
"cy": 82.35337687892542,
"rx": 73.02811219141157,
"ry": 74.66203440932597
},
{
"cx": 84.0,
"cy": 82.42039157417511,
"rx": 72.80858646484103,
"ry": 74.62208876727115
},
{
"cx": 106.5,
"cy": 81.72901899211435,
"rx": 74.34475334746573,
"ry": 73.78037632992753
},
{
"cx": 95.5,
"cy": 81.63668560751485,
"rx": 75.06807641776177,
"ry": 73.48706340594615
},
{
"cx": 95.5,
"cy": 81.67982777738753,
"rx": 75.12648691865994,
"ry": 73.49674962974022
},
{
"cx": 84.5,
"cy": 81.57452731727243,
"rx": 74.48750227516855,
"ry": 73.69652904057105
},
{
"cx": 106.0,
"cy": 72.23134647099609,
"rx": 75.09606781800167,
"ry": 75.52834001598418
},
{
"cx": 96.0,
"cy": 72.31864303902333,
"rx": 74.79155274675983,
"ry": 75.30939853900956
},
{
"cx": 94.5,
"cy": 72.10115208001082,
"rx": 74.92026723097523,
"ry": 75.46485386910284
},
{
"cx": 85.0,
"cy": 72.21051327527425,
"rx": 74.61593591942703,
"ry": 75.5076844695196
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -0,0 +1,45 @@
{
"panorama": [
512,
512,
4
],
"materials": [
256,
256,
8
],
"details": [
256,
256,
15
],
"city": [
512,
512,
22
],
"city-night": [
512,
512,
22
],
"night": [
512,
512,
4
],
"rail-hands": [
640,
320,
32
],
"lighting": [
512,
512,
3
],
"city_renderer": "world-space MASM geometry; sky only is raster",
"source": "Built-in image generation; runtime geometry and interaction are assembly-only",
"reference": "https://www.loftprojectetagi.ru/visitors/krysha/"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -0,0 +1,27 @@
# Rooftop City Source Art
Generated with the built-in image generation tool. The selected PNG originals
are preserved in this directory; the offline packer prepares runtime atlases.
The city itself is MASM world-space geometry, not a generated city background.
## Prompt Set
- `rail-hands-v1.png`: transparent eight-pose sprite sheet, two anatomical hands
and complete forearms reaching forward and resting naturally on a low rooftop
handrail, first-person view, dark sleeves, consistent camera and anatomy.
- `city-facades-v1.png`: eight detailed, straight-on Petersburg-inspired
classical building facades, varied pastel masonry, carved window surrounds,
cornices and realistic windows; no perspective, sky or ground.
- `modern-facades-v1.png`: varied straight-on contemporary glass, brick,
townhouse and civic building facades, suitable for wrapping real geometry.
- `roof-zinc-v1.png`: detailed weathered blue-grey standing-seam zinc roof
surface, evenly lit, seamless material texture, no building silhouette.
- `sunset-sky-v1.png`: panoramic sunset sky with fine pink and warm cloud
streaks fading into cool blue; no buildings, landscape or city silhouette.
- `night-sky-v1.png`: panoramic deep night sky with subtle clouds and stars;
no buildings, landscape or city silhouette.
These record the asset specifications. The generated images are originals,
not an exact reconstruction of any real venue or skyline. Alpha cleanup,
optical-flow in-betweens, window masks, light maps and mesh generation are
deterministic offline processing; no image service is used by the game.

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

View file

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

Binary file not shown.

View file

@ -5350,10 +5350,14 @@ check_walk_mode:
call UpdateInteriorTravel3D
call UpdateInteriorEye3D
call UpdateCamera3D
; The roof owns all planes/depth; an indoor prepass would be discarded.
cmp hallLevel3D,HALL_ROOF
je walk_world_ready
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderSprites
walk_world_ready:
call RenderHall3D
call RenderHUD
call RenderMinimap

Binary file not shown.

View file

@ -31,8 +31,12 @@ required by the game.
The throwing line is 2700 board millimetres from the target. The initial lens
is adjusted so the board is only about 7 percent smaller on screen than the
previous 2370mm/900px view. Its original 512px texture and scoring geometry are
unchanged. Max watches the same moving aim point for a short release window;
previous 2370mm/900px view. Texture resolution and scoring geometry are preserved.
The v2 board keeps the same physical bounds and 512px pack dimensions,
but uses larger high-contrast numerals, supersampled wires and lower grain.
The reticle is a 7x7 footprint containing only nine one-pixel marks, with no thick
outline. Individual marks adapt to bright/dark sectors for readability.
Max watches the same moving aim point for a short release window;
he does not reroll a hit or bypass the sway.
The independent

View file

@ -25,22 +25,41 @@ steps after stalls. No neural worker or algorithmic reading generator is called.
## Crystal Ball
The second selection raises a crystal ball and animates gloved hands with swirling
blue mist. It ends with an explicit unavailable-result line. There is no real
student grading, inference of merit, identity form or automated academic decision.
Again replays the visual sequence. This limitation is deliberate, not a hidden
random grade presented as a judgement.
The second selection is a fictional, theatrical student-themed lottery, separate
from tarot and not an assessment of actual academic merit. The form asks for a
first name, surname, birth date (DD.MM.YYYY), group and inclusive score bounds
from 0 to 100. Tab cycles fields, clicking focuses one, Backspace edits, and
Enter or the summon button submits. Empty fields, invalid calendar dates
(including leap years), reversed bounds and scores above 100 are rejected.
Identity fields only dress the scene: they never seed or weight the draw. One
native random draw is committed on valid submission. The ball rises for 1.4 s,
hands circle through 32 poses with 16 mist frames for 5 s, then a glowing number
appears inside the ball. Short typewriter speech accompanies the stages. The
result fades into view and stays readable while the identity and chosen bounds
remain at the bottom. Renders, duplicate submits and clicks during the ritual
cannot reroll. After the reveal, Edit returns to the filled form for another
session. No network, inference worker, profile or bankroll changes are involved.
Fields are kept only in session memory and cleared on exit; nothing is saved.
The number uses baked, antialiased Cambria numeral sprites with translucent blue
glow, not enlarged 3x5 block digits. Each of the 16 mist frames has its own measured
glass-centre anchor, excluding the pedestal. One assembly resolver chooses both
the ball frame and the matching number position, including the rising movement.
Numerals stay at a constant size and the fog remains visible through their glow.
The random draw and its probabilities are unchanged; identity never biases it.
## Assets
Generated source art and production prompts: `assets/sprites/fortune/`.
Offline packer: `tools/generators/pack_fortune_session.py`.
Offline packers: `tools/generators/pack_fortune_session.py` and then
`tools/generators/pack_fortune_orb.py` (tracking metadata and numeral sprites).
Runtime packs: `assets/animations/fortune/` (ANM1 format).
Forty authored poses are arranged on one shared anchored 8x5 sheet. Shared-canvas
in-betweens yield 64 shuffle, 32 deal, 32 gesture and 32 orb-hand frames. Sixteen
mist frames, 44 oriented card faces, a back and a 640x480 room complete the scene.
Art loads only when seated and all seven packs are freed on exit/shutdown. The
Art loads only when seated and all eight packs are freed on exit/shutdown. The
seated view is independent of the walking room's existing asset packs and map.
## Verification
@ -48,11 +67,17 @@ seated view is independent of the walking room's existing asset packs and map.
Run `C:/Python314/python.exe tools/tests/check_hall_expansion.py` after packing.
The fresh native x86 harness exercises real WM_CHAR input, length/backspace,
empty/double submission, all stage transitions, card uniqueness, speech growth,
question persistence, crystal-ball stages, exit during animation, asset cleanup
question persistence, crystal-ball form validation, inclusive draws, fixed bounds,
input locking, duplicate submission, crystal-ball stages, exit during animation, asset cleanup
and unchanged player position/bank. It writes actual framebuffer BMP/PNG captures
and a 64-frame shuffle GIF under `build/hall-expansion/`. Python additionally checks
native-frame differences, question pixels, pack integrity and reversed artwork.
`--parity` checks these tested procedures against the shipping game object.
The orb regression captures each of the 16 native frames twice, with and without
the number. Pixel differences must stay centred on that frame's glass sphere;
this also checks that opacity is restored and the numeral pack is freed on exit.
`--prove-fate-anchor-regression` deliberately restores the old fixed coordinates
in a test-only module and must fail the native tracking assertions.
The old multi-gigabyte neural ZIP is not rebuilt by this feature. Distributions
must include the new `assets/animations/fortune/` directory with the updated game.

138
docs/ROOFTOP.md Normal file
View file

@ -0,0 +1,138 @@
# Rooftop Lounge "Vysota"
The terrace is an assembly-only walkable hall level (`HALL_ROOF = 4`), reached
by a real elevator cabin near the north wall to the right of the cabaret.
Existing games, basement stairs, scratch tickets and fortune room stay in place.
## Travel
Use E at the exterior call panel, then walk into the open cabin using the normal
movement keys. The inside panel offers the roof or the main hall depending on
the current landing. E starts a trip only when the player is fully inside.
Doors physically slide apart into projecting pockets. Movement is blocked during
closing/travel, while looking around remains possible. After one second of
closing and 3.2 seconds of travel the destination changes, doors open, and the
player walks out. Position, angle and standing height do not drift during travel.
An idle open door closes after 2.8 seconds away from the elevator. It reverses
if the player re-enters its approach, rather than shutting through them. Arrival
keeps the cabin coordinates and opens the other landing; there is no forced
camera path or automatic walking. The illuminated physical indicator changes
during travel and shows `1` or `R` at the landings.
## Terrace
- A fully volumetric Petersburg-inspired city, seen from a sixty-metre tower.
Only the sky is a painted texture; buildings, streets, roofs, parks, canal,
bridges, monuments, cars and boats are real world-space geometry.
- Finite HD decking, low railings and an open sky, rather than an indoor ceiling
or a floor extending to infinity beyond the roof edge.
- Three three-seat sofas, four armchairs, three bar stools and three stools at
a rail-side standing counter: 13 selectable furniture positions, 19 seat places.
- Seven coffee tables, eight potted trees, flowering troughs, eight varied guests,
two supported strings of lanterns, and a slatted pergola over a working bar.
- Sofas and chairs use closed native volumes with separate padded cushions.
Planter boxes contain soil and multiple foliage clusters at different depths.
Whole-object photographs are not pasted onto every furniture face.
- E at a railing eases into a leaning pose, with thirty-two hand/forearm frames
anchored to the actual rail height. E or movement releases the rail.
- While leaning, mouse, arrows and A/D can turn up to 73 degrees either way. The
rail contact is projected in world space so the hands do not follow head yaw.
- Guests walk on the terrace with collision checks, pauses and walking frames.
- In the open first-floor elevator, 1 selects sunset and 2 selects night.
- E sits/stands; walking forward/backward also stands and restores the approach
position. Stool views use bar height and rail-side stools face the city.
- The bar reuses the existing mixing/drinking system and prices (100/200/free
water). Its camera is temporarily positioned at the new counter and completely
restored after rendering. Drinks are the only new-location actions that spend
or save bankroll, through the existing purchase routine.
## Assets And Build
Generated sources are under `assets/sprites/rooftop/`; runtime ANM1 packs are under
`assets/animations/rooftop/`. Run `tools/generators/pack_rooftop.py` offline to pack
the sky, materials, semantic night-window textures, ground lighting, hands,
district layout and collision map. Run `pack_rooftop_gl.py` afterwards to build
the static city and moving vehicle triangle meshes. Python, Pillow, NumPy and
OpenCV are offline developer tools, not game dependencies.
The native MASM HD city renderer uses the Windows system OpenGL library, a
private hidden rendering context and an offscreen framebuffer up to 1280x960. It uses
textured mesh display lists, mipmaps, normals, directional dusk lighting,
distance fog and a separate emissive-window pass at night. The existing terrace,
hands and HUD are composited by direct native pixel copying into one DIB. Other games retain
their existing renderer. If the required GL capabilities are unavailable, the
assembly software city renderer remains the fallback. No C/C++ renderer, model
engine, browser runtime, cloud service or runtime downloader is introduced.
The eastern avenue is now a broad river with raised embankments, promenades,
trees, piers, three full-span bridges and further riverfront quarters. Water
texture flow and six moving boats keep it from being a static blue plane.
Street rectangles are clipped into disjoint junctions rather than coplanar,
overlapping strips. The terrace deck uses mipmapped HD geometry rather than
the unfiltered software floor when HD rendering is available. City, foreground
and HUD are composed into one offscreen DIB and copied to the window once;
there is no intermediate bare-city frame. glReadPixels supplies the required
readback synchronisation, without a redundant preceding glFinish.
## Rooftop Performance
- F8 cycles automatic, eco (640x480) and detailed (1280x960) city rendering.
Automatic starts at 960x720 and drops to eco after sustained frames over 34 ms.
Switching back to automatic retries 960x720. The scene, view distance, collisions
and gameplay do not change with the render profile; other rooms are unaffected.
- The offline mesh packer discards only triangles that face away from every
possible rooftop eye position. Runtime back-face culling handles head turns.
The static mesh, including driver-safe ground and canal frontage, stays under 48,000
triangles. Never-visible faces are pruned before shipping.
- Terrain and light-map triangles crossing the near district are at most 64 m
wide, with finer cells near the terrace. This avoids relying on per-fragment
fog for multi-kilometre polygons on integrated OpenGL drivers. Water animation
uses its own display list from the same tessellated mesh, not two giant quads.
GL_NICEST requests accurate fog, but the geometry remains safe if ignored.
- CPU foreground surfaces use projected screen bounds and camera-facing box
sides. The final frame avoids an extra GDI scaling/alpha-blending pass.
- Mipmap generation is optional on older OpenGL drivers. If hardware rendering
cannot initialise, the software ground renderer still draws roads and water,
including solid-colour fallback surfaces when a texture pack is absent.
- `runtime/roof-renderer.txt` records the local GL vendor, renderer, version,
initialisation stage, city-pack availability and render width. It contains no
personal data and is not sent anywhere. Stage 0 means successful initialisation.
Integrated Intel graphics use shared system memory; the small dedicated-memory
figure alone is not a useful rendering budget. Actual laptop performance must be
checked on that machine, not inferred from the desktop benchmark.
The western canal has two rows of varied waterfront facades, raised stone quays,
bridge openings, trees and benches. These replace the empty cleared block rows;
they use the same world records in the GL and software renderers. The offline
`pack_rooftop.py --district-only` option rebuilds layout and lighting without
repacking unchanged sky, hand-animation or material assets.
The art direction takes atmospheric inspiration from the rooftop reference:
https://www.loftprojectetagi.ru/visitors/krysha/ . It is an original game backdrop,
not an exact survey or reproduction of that venue. Generated source images are
preserved alongside the packer metadata. Older city panoramas and furniture
reference photographs remain source art, not the active city/furniture renderer.
Runtime sources: `src_graphics/rooftop*.inc` and the shared MASM city primitives.
The normal MASM build and root/bin publisher ship the feature. Rebuilding the
existing installer is separate from updating the ordinary game executable.
## Verification
`tools/tests/check_hall_expansion.py` compiles the real current MASM routines and
tests closed/open door collision, manual cabin entry/exit, both trips, timing,
camera/position/bankroll preservation, furniture/rail bounds, seating, automatic
closing, thirty repeated bar renders, asset loading/freeing and native screenshots.
It exports a 70x78 walk grid from actual `CanWalk3D` calls. Reachability checks
verify a connected route from the cabin to the bar, both sides of the terrace,
and every seating approach after the denser layout. Framebuffer captures are
written under `build/hall-expansion/roof-*.png`. `--parity` verifies tested
procedures against the full shipping object before publishing.
The native harness also checks all three resolution profiles, automatic downgrade,
F8 switching, and software roads/water with both present and missing city textures.
It also captures GL_FASTEST fog, checks ground-cell bounds and water tessellation.
These are compatibility checks on the test machine, not a substitute for confirming
the result on the laptop's actual Intel driver.

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -5,12 +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 eax,161
cmp eax,MATERIAL_JACKBOX_FRONT
jb @F
cmp eax,163
cmp eax,MATERIAL_JACKBOX_BACK
ja @F
invoke ResolveJackboxSurface3D,material
ret
@@:
cmp eax,ROOF_DECK
jb @F
cmp eax,ROOF_LAST_MATERIAL
ja @F
invoke ResolveRoofSurface3D,material
ret
@@:
cmp eax,160
jne @F
@ -116,6 +123,7 @@ city_prepare_ray:
PrepareCityRays3D ENDP
CityRayPlane3D PROC coordinate:DWORD,origin:DWORD,direction:DWORD
local farDelta:DWORD
mov ecx, direction
mov eax, ecx
test eax, eax
@ -124,6 +132,17 @@ CityRayPlane3D PROC coordinate:DWORD,origin:DWORD,direction:DWORD
@@:
cmp eax, 256
jb city_ray_parallel
; A long city ray can overflow IDIV before its depth is clipped. Reject
; intersections beyond the exterior far plane before forming the dividend.
shl eax,10
mov farDelta,eax
mov eax,coordinate
sub eax,origin
cdq
xor eax,edx
sub eax,edx
cmp eax,farDelta
jae city_ray_parallel
mov eax, coordinate
sub eax, origin
cdq
@ -140,14 +159,78 @@ CityRayPlane3D ENDP
DrawCityVertical3D PROC uses ebx esi edi axis:DWORD,coordinate:DWORD,loBound:DWORD,hiBound:DWORD,zLow:DWORD,zHigh:DWORD,material:DWORD
local depth:DWORD,top:DWORD,bottom:DWORD,span:DWORD,column:DWORD
local sourceX:DWORD,stepY:DWORD,sourceY:DWORD,destination:DWORD,row:DWORD,texture:DWORD
local textureW:DWORD,textureH:DWORD
local textureW:DWORD,textureH:DWORD,faceLight:DWORD
local endColumn:DWORD,firstX:DWORD,firstDepth:DWORD,clipX:DWORD,clipY:DWORD
invoke ResolveCitySurface3D,material
test eax, eax
jz city_vertical_done
mov texture, eax
mov textureW, ecx
mov textureH, edx
mov faceLight,230
cmp roofCityPass3D,0
je city_vertical_setup
cmp axis,0
jne city_vertical_light_y
mov eax,playerX
cmp eax,coordinate
jl city_vertical_setup
mov faceLight,142
jmp city_vertical_setup
city_vertical_light_y:
mov faceLight,165
mov eax,playerY
cmp eax,coordinate
jl city_vertical_setup
mov faceLight,218
city_vertical_setup:
mov column, 0
mov endColumn,SCREEN_W
cmp hallLevel3D,HALL_ROOF
jne city_vertical_column
mov eax,coordinate
mov edx,loBound
cmp axis,0
je @F
xchg eax,edx
@@:
invoke ProjectCityPoint3D,eax,edx
mov firstX,eax
mov firstDepth,edx
mov eax,coordinate
mov edx,hiBound
cmp axis,0
je @F
xchg eax,edx
@@:
invoke ProjectCityPoint3D,eax,edx
cmp edx,4096
jg city_vertical_clip_second
cmp firstDepth,4096
jle city_vertical_done
jmp city_vertical_column
city_vertical_clip_second:
cmp firstDepth,4096
jle city_vertical_column
mov edx,firstX
cmp eax,edx
jge @F
xchg eax,edx
@@:
sub edx,2
add eax,3
test edx,edx
jns @F
xor edx,edx
@@:
cmp eax,SCREEN_W
jle @F
mov eax,SCREEN_W
@@:
cmp edx,eax
jge city_vertical_done
mov column,edx
mov endColumn,eax
city_vertical_column:
mov ebx, column
cmp axis, 0
@ -200,15 +283,15 @@ city_vertical_along:
mov sourceX, eax
mov eax, viewEye3D
sub eax, zHigh
imul eax, SCREEN_H
cdq
mov ecx,SCREEN_H
imul ecx
idiv depth
add eax, viewHorizon3D
mov top, eax
mov eax, viewEye3D
sub eax, zLow
imul eax, SCREEN_H
cdq
mov ecx,SCREEN_H
imul ecx
idiv depth
add eax, viewHorizon3D
mov bottom, eax
@ -274,7 +357,10 @@ city_vertical_pixel:
@@:
mov edx, depth
mov cityDepthBuffer3D[edi], edx
cmp roofCityPass3D,0
jne city_vertical_scene_color
and eax, 00FFFFFFh
city_vertical_scene_color:
cmp cityBackdrop3D, 0
je @F
push eax
@ -288,7 +374,21 @@ city_vertical_pixel:
shr eax, 1
add eax, 00101828h
@@:
cmp roofCityPass3D,0
je city_vertical_color_ready
invoke ShadeRoofCityPixel3D,eax,depth,faceLight,material
mov ebx,edi
shr ebx,2
mov ecx,roofPixelGlow3D
mov roofGlowMask3D[ebx],cl
city_vertical_color_ready:
mov screenBuffer[edi], eax
cmp hallLevel3D,HALL_ROOF
jne city_vertical_next_pixel
mov ebx,edi
shr ebx,2
mov ecx,roofCityPass3D
mov roofCityMask3D[ebx],cl
city_vertical_next_pixel:
mov eax, stepY
add sourceY, eax
@ -299,7 +399,8 @@ city_vertical_next_pixel:
jb city_vertical_pixel
city_vertical_next_column:
inc column
cmp column, SCREEN_W
mov eax,endColumn
cmp column,eax
jb city_vertical_column
city_vertical_done:
ret
@ -308,7 +409,7 @@ DrawCityVertical3D ENDP
DrawCityHorizontal3D PROC uses ebx esi edi xMin:DWORD,xMax:DWORD,yMin:DWORD,yMax:DWORD,z:DWORD,material:DWORD
local left:DWORD,right:DWORD,top:DWORD,bottom:DWORD,corner:DWORD,pointX:DWORD,pointY:DWORD
local depth:DWORD,row:DWORD,column:DWORD,stepX:DWORD,stepY:DWORD,scaleX:DWORD,scaleY:DWORD,texture:DWORD
local textureW:DWORD,textureH:DWORD,nearClipped:DWORD
local textureW:DWORD,textureH:DWORD,nearClipped:DWORD,visibleCorners:DWORD
invoke ResolveCitySurface3D,material
test eax, eax
jz city_horizontal_done
@ -316,6 +417,7 @@ DrawCityHorizontal3D PROC uses ebx esi edi xMin:DWORD,xMax:DWORD,yMin:DWORD,yMax
mov textureW, ecx
mov textureH, edx
mov nearClipped, 0
mov visibleCorners,0
mov left, SCREEN_W
mov right, 0
mov top, SCREEN_H
@ -338,6 +440,7 @@ city_horizontal_corner:
mov nearClipped, 1
jmp city_horizontal_next_corner
city_horizontal_visible_corner:
inc visibleCorners
mov esi, edx
cmp eax, left
jge @F
@ -349,8 +452,8 @@ city_horizontal_visible_corner:
@@:
mov eax, viewEye3D
sub eax, z
imul eax, SCREEN_H
cdq
mov ecx,SCREEN_H
imul ecx
idiv esi
add eax, viewHorizon3D
cmp eax, top
@ -365,6 +468,11 @@ city_horizontal_next_corner:
inc corner
cmp corner, 4
jb city_horizontal_corner
cmp hallLevel3D,HALL_ROOF
jne @F
cmp visibleCorners,0
je city_horizontal_done
@@:
cmp nearClipped, 0
je @F
; Ray bounds/depth below clip the polygon; a behind-camera corner cannot discard it.
@ -414,8 +522,8 @@ city_horizontal_row:
jz city_horizontal_next_row
mov eax, viewEye3D
sub eax, z
imul eax, SCREEN_H
cdq
mov ebx,SCREEN_H
imul ebx
idiv ecx
cmp eax, 4096
jle city_horizontal_next_row
@ -508,7 +616,25 @@ city_horizontal_alpha:
mov ecx, depth
mov cityDepthBuffer3D[edi], ecx
and eax, 00FFFFFFh
cmp roofCityPass3D,0
je city_horizontal_color_ready
cmp material,ROOF_CITY_ROAD
jne @F
invoke SampleRoofGroundLight3D,eax,pointX,pointY
@@:
invoke ShadeRoofCityPixel3D,eax,depth,244,material
mov ebx,edi
shr ebx,2
mov ecx,roofPixelGlow3D
mov roofGlowMask3D[ebx],cl
city_horizontal_color_ready:
mov screenBuffer[edi], eax
cmp hallLevel3D,HALL_ROOF
jne city_horizontal_advance
mov ebx,edi
shr ebx,2
mov ecx,roofCityPass3D
mov roofCityMask3D[ebx],cl
city_horizontal_advance:
mov eax, stepX
add pointX, eax
@ -529,6 +655,30 @@ city_horizontal_done:
DrawCityHorizontal3D ENDP
DrawCityBoxSides3D PROC xMin:DWORD,xMax:DWORD,yMin:DWORD,yMax:DWORD,zLow:DWORD,zHigh:DWORD,material:DWORD
cmp hallLevel3D,HALL_ROOF
jne city_box_all_sides
mov eax,playerX
cmp eax,xMin
jge @F
invoke DrawCityVertical3D,0,xMin,yMin,yMax,zLow,zHigh,material
@@:
mov eax,playerX
cmp eax,xMax
jle @F
invoke DrawCityVertical3D,0,xMax,yMin,yMax,zLow,zHigh,material
@@:
mov eax,playerY
cmp eax,yMin
jge @F
invoke DrawCityVertical3D,1,yMin,xMin,xMax,zLow,zHigh,material
@@:
mov eax,playerY
cmp eax,yMax
jle @F
invoke DrawCityVertical3D,1,yMax,xMin,xMax,zLow,zHigh,material
@@:
ret
city_box_all_sides:
invoke DrawCityVertical3D, 0, xMin, yMin, yMax, zLow, zHigh, material
invoke DrawCityVertical3D, 0, xMax, yMin, yMax, zLow, zHigh, material
invoke DrawCityVertical3D, 1, yMin, xMin, xMax, zLow, zHigh, material

View file

@ -30,6 +30,7 @@ DartsWallText3D PROTO :DWORD,:DWORD,:DWORD
DrawDartsScoreboard3D PROTO
DrawDartsProps3D PROTO
DrawDartsArm3D PROTO :DWORD,:SDWORD,:SDWORD
DrawDartsReticle3D PROTO :SDWORD,:SDWORD
DartsScore3D PROTO :SDWORD,:SDWORD
SettleDarts3D PROTO
DrawDartsRoom3D PROTO
@ -45,7 +46,7 @@ dtWall3D AnimPack3D <>
dtChair3D AnimPack3D <>
dtTable3D AnimPack3D <>
dtRoom3D AnimPack3D <>
dtBoardPath db "assets\animations\darts\board-v1.bin",0
dtBoardPath db "assets\animations\darts\board-v2.bin",0
dtHandsPath db "assets\animations\darts\arm-throw-v3.bin",0
dtDartPath db "assets\animations\darts\dart-views-v1.bin",0
dtWallPath db "assets\animations\darts\lounge-wall-v1.bin",0
@ -134,6 +135,7 @@ dtDouble db 68,0
dtTriple db 84,0
dtSingle db 83,0
dtTarget db 214,197,203,220,0
dtReticleOffsets3D dd -3,0,-2,0,2,0,3,0,0,-3,0,-2,0,2,0,3,0,0
.data?
dtRayX3D dd 640 dup(?)
dtRayZ3D dd 640 dup(?)

View file

@ -1,3 +1,39 @@
; Nine physical pixels, with contrast chosen from the surface beneath each one.
DrawDartsReticle3D PROC uses ebx esi edi targetX:SDWORD,targetY:SDWORD
lea esi,dtReticleOffsets3D
mov ebx,9
dt_reticle_point:
mov eax,targetX
add eax,[esi]
cmp eax,640
jae dt_reticle_next
mov edx,targetY
add edx,[esi+4]
cmp edx,32
jl dt_reticle_next
cmp edx,400
jge dt_reticle_next
imul edx,640
add edx,eax
lea edi,screenBuffer[edx*4]
movzx eax,byte ptr [edi]
movzx ecx,byte ptr [edi+1]
lea eax,[eax+ecx*2]
movzx ecx,byte ptr [edi+2]
add eax,ecx
mov ecx,00F6E6ADh
cmp eax,560
jb @F
mov ecx,00152021h
@@:
mov [edi],ecx
dt_reticle_next:
add esi,8
dec ebx
jnz dt_reticle_point
ret
DrawDartsReticle3D ENDP
; A single rigid sprite transform aligns the dart axis with the live target.
DrawDartsArm3D PROC uses ebx esi edi pose:DWORD,targetX:SDWORD,targetY:SDWORD
local originX:DWORD,originY:DWORD,left:DWORD,right:DWORD,bottom:DWORD,row:DWORD,column:DWORD

View file

@ -545,22 +545,7 @@ dt_render_pose:
invoke DrawDartsArm3D,pose,bx0,by0
cmp dtState3D,DT_AIM
jne dt_render_hud
mov eax,bx0
sub eax,7
mov edx,by0
sub edx,2
invoke DrawBuckshotPanel3D,eax,edx,15,5,00101112h
mov eax,by0
sub eax,7
mov edx,bx0
sub edx,2
invoke DrawBuckshotPanel3D,edx,eax,5,15,00101112h
mov eax,bx0
sub eax,6
invoke DrawBuckshotPanel3D,eax,by0,13,2,00F6E6ADh
mov eax,by0
sub eax,6
invoke DrawBuckshotPanel3D,bx0,eax,2,13,00F6E6ADh
invoke DrawDartsReticle3D,bx0,by0
dt_render_hud:
invoke DrawBox,0,0,320,16,00151D20h
invoke DrawString8x8,8,4,OFFSET dtTitle,00E8D7ADh

View file

@ -11,7 +11,7 @@ fortune_session_load:
invoke LoadAnimPack3D,esi,fortuneSessionPaths3D[ebx*4]
add esi,SIZEOF AnimPack3D
inc ebx
cmp ebx,7
cmp ebx,FORTUNE_SESSION_PACK_COUNT
jb fortune_session_load
mov engineMode,MODE_FORTUNE
mov paused3D,0
@ -24,6 +24,7 @@ fortune_session_load:
mov fortuneQuestionLength3D,0
mov fortuneQuestion3D,0
mov fortuneWorkerState3D,0
call ResetFortuneFate3D
mov eax,animationClock3D
mov fortuneSessionClock3D,eax
invoke SetFortuneSpeech3D,OFFSET fortuneWelcome3D
@ -35,13 +36,14 @@ BeginFortuneDialog3D ENDP
; Kept as the public exit/shutdown hook. No inference process is launched.
StopFortuneWorker3D PROC uses ebx esi
lea esi,fortuneSessionPacks3D
mov ebx,7
mov ebx,FORTUNE_SESSION_PACK_COUNT
fortune_session_free:
invoke FreeAnimPack3D,esi
add esi,SIZEOF AnimPack3D
dec ebx
jnz fortune_session_free
mov fortuneWorkerState3D,0
call ResetFortuneFate3D
ret
StopFortuneWorker3D ENDP
@ -129,11 +131,17 @@ fortune_char_tarot:
ret
fortune_char_orb:
mov fortuneSubMode3D,FORTUNE_SUBMODE_ORB
mov fortunePhase3D,1
call ResetFortuneFate3D
mov fortunePhase3D,0
mov fortunePhaseTime3D,0
invoke SetFortuneSpeech3D,OFFSET fortuneOrbStatus3D
invoke SetFortuneSpeech3D,OFFSET fortuneFateReady3D
ret
fortune_char_question:
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne @F
invoke FortuneFateChar3D,character
ret
@@:
cmp fortuneSubMode3D,FORTUNE_SUBMODE_TAROT
jne fortune_char_done
cmp fortunePhase3D,0
@ -277,6 +285,7 @@ fortune_update_orb:
jb fortune_update_done
mov fortunePhase3D,2
mov fortunePhaseTime3D,0
invoke SetFortuneSpeech3D,OFFSET fortuneFateSwirl3D
jmp fortune_update_done
fortune_orb_swirl:
cmp fortunePhase3D,2
@ -285,7 +294,7 @@ fortune_orb_swirl:
jb fortune_update_done
mov fortunePhase3D,3
mov fortunePhaseTime3D,0
invoke SetFortuneSpeech3D,OFFSET fortuneOrbText3D
invoke SetFortuneSpeech3D,OFFSET fortuneFateReveal3D
fortune_update_done:
ret
UpdateFortuneSession3D ENDP
@ -315,6 +324,37 @@ HandleFortuneDialog3D PROC
jae fortune_handle_reset
jmp fortune_handle_action
fortune_handle_menu_click:
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne fortune_handle_selection_click
cmp fortunePhase3D,0
jne fortune_handle_done
; Form uses physical pixels; mouse input here uses half-resolution units.
mov eax,mouseY3D
sub eax,162
jb fortune_handle_done
xor edx,edx
mov ecx,21
div ecx
cmp eax,2
ja fortune_handle_done
cmp edx,11
ja fortune_handle_done
shl eax,1
cmp mouseX3D,156
jb fortune_handle_form_left
cmp mouseX3D,168
jb fortune_handle_done
cmp mouseX3D,312
ja fortune_handle_done
inc eax
jmp fortune_handle_form_focus
fortune_handle_form_left:
cmp mouseX3D,8
jb fortune_handle_done
fortune_handle_form_focus:
mov fortuneFateField3D,eax
ret
fortune_handle_selection_click:
cmp fortuneSubMode3D,FORTUNE_SUBMODE_MENU
jne fortune_handle_done
cmp mouseY3D,196
@ -332,6 +372,11 @@ fortune_handle_orb:
fortune_handle_action:
cmp fortuneSubMode3D,FORTUNE_SUBMODE_MENU
je fortune_handle_tarot
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne @F
call SubmitFortuneFate3D
ret
@@:
cmp fortuneSubMode3D,FORTUNE_SUBMODE_TAROT
jne fortune_handle_done
cmp fortunePhase3D,0
@ -367,9 +412,14 @@ fortune_handle_menu:
fortune_handle_reset:
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne @F
mov fortunePhase3D,1
cmp fortunePhase3D,3
jne fortune_handle_done
cmp fortunePhaseTime3D,1200
jb fortune_handle_done
mov fortunePhase3D,0
mov fortunePhaseTime3D,0
invoke SetFortuneSpeech3D,OFFSET fortuneOrbStatus3D
mov fortuneFateResult3D,-1
invoke SetFortuneSpeech3D,OFFSET fortuneFateReady3D
ret
@@:
cmp fortunePhase3D,12
@ -692,6 +742,8 @@ fortune_render_hand_gesture:
mov esi,eax
jmp fortune_render_hands
fortune_render_orb_hands:
cmp fortunePhase3D,0
je fortune_render_hands
mov ebx,OFFSET fortuneSessionOrbHands3D
mov eax,fortunePhaseTime3D
shr eax,5
@ -737,28 +789,24 @@ fortune_render_menu:
invoke DrawBuckshotText3D,364,404,OFFSET fortuneFate3D,00D2E8F2h
jmp fortune_render_footer
fortune_render_orb:
mov eax,fortunePhaseTime3D
shr eax,6
and eax,15
mov esi,eax
mov ebx,195
cmp fortunePhase3D,1
cmp fortunePhase3D,0
jne fortune_render_orb_active
call DrawFortuneFateForm3D
jmp fortune_render_footer
fortune_render_orb_active:
call ResolveFortuneOrb3D
invoke DrawAnimSprite3D,OFFSET fortuneSessionMist3D,fortuneOrbFrame3D,fortuneOrbX3D,fortuneOrbY3D,128,128
cmp fortunePhase3D,3
jne @F
mov eax,fortunePhaseTime3D
imul eax,135
xor edx,edx
mov ecx,1400
div ecx
mov ebx,330
sub ebx,eax
call DrawFortuneFateResult3D
@@:
invoke DrawAnimSprite3D,OFFSET fortuneSessionMist3D,esi,214,ebx,128,128
call DrawFortuneFateIdentity3D
fortune_render_footer:
call DrawFortuneSpeech3D
cmp fortuneSubMode3D,FORTUNE_SUBMODE_MENU
je fortune_render_exit_button
cmp fortuneSubMode3D,FORTUNE_SUBMODE_TAROT
jne fortune_render_again_button
jne fortune_render_fate_button
invoke DrawBoxBorder,8,227,76,12,000B1E30h,00445F75h,1
mov esi,OFFSET fortuneAsk3D
cmp fortunePhase3D,0
@ -766,9 +814,20 @@ fortune_render_footer:
mov esi,OFFSET fortuneContinue3D
@@:
invoke DrawBuckshotText3D,24,458,esi,00CCE4F4h
jmp fortune_render_again_button
fortune_render_fate_button:
cmp fortunePhase3D,0
jne fortune_render_again_button
invoke DrawBoxBorder,8,227,76,12,000B1E30h,00445F75h,1
invoke DrawBuckshotText3D,24,458,OFFSET fortuneFateStart3D,00CCE4F4h
fortune_render_again_button:
invoke DrawBoxBorder,88,227,76,12,000B1E30h,00445F75h,1
invoke DrawBuckshotText3D,192,458,OFFSET fortuneAgain3D,0091A8BCh
mov esi,OFFSET fortuneAgain3D
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne @F
mov esi,OFFSET fortuneFateEdit3D
@@:
invoke DrawBuckshotText3D,192,458,esi,0091A8BCh
invoke DrawBoxBorder,172,227,64,12,000B1E30h,00445F75h,1
invoke DrawBuckshotText3D,368,458,OFFSET fortuneMenu3D,00CCE4F4h
fortune_render_exit_button:
@ -776,3 +835,425 @@ fortune_render_exit_button:
invoke DrawBuckshotText3D,516,458,OFFSET fortuneExit3D,00CCE4F4h
ret
RenderFortuneDialog3D ENDP
ResetFortuneFate3D PROC uses edi
lea edi,fortuneFateLengths3D
xor eax,eax
mov ecx,6
rep stosd
lea edi,fortuneFateFields3D
mov ecx,96
rep stosd
mov fortuneFateField3D,0
mov fortuneFateResult3D,-1
mov fortuneFateFields3D[256],'9'
mov fortuneFateFields3D[320],'1'
mov fortuneFateFields3D[321],'0'
mov fortuneFateLengths3D[16],1
mov fortuneFateLengths3D[20],2
ret
ResetFortuneFate3D ENDP
FortuneFateChar3D PROC uses ebx esi edi character:DWORD
cmp fortunePhase3D,0
jne fate_char_done
mov ebx,fortuneFateField3D
cmp ebx,6
jae fate_char_done
mov eax,character
cmp eax,9
je fate_char_next
mov esi,ebx
shl esi,6
lea esi,fortuneFateFields3D[esi]
mov ecx,fortuneFateLengths3D[ebx*4]
cmp eax,8
je fate_char_backspace
cmp eax,32
jb fate_char_done
cmp eax,255
ja fate_char_done
cmp eax,127
je fate_char_done
cmp ecx,fortuneFateLimits3D[ebx*4]
jae fate_char_done
cmp ebx,2
je fate_char_date
cmp ebx,4
jae fate_char_digit
cmp eax,32
jne fate_char_store
test ecx,ecx
jz fate_char_done
cmp BYTE PTR [esi+ecx-1],32
je fate_char_done
jmp fate_char_store
fate_char_date:
cmp eax,'/'
jne @F
mov eax,'.'
@@:
cmp eax,'.'
jne fate_char_date_digit
cmp ecx,2
je fate_char_store
cmp ecx,5
je fate_char_store
jmp fate_char_done
fate_char_date_digit:
cmp eax,'0'
jb fate_char_done
cmp eax,'9'
ja fate_char_done
cmp ecx,2
je fate_char_date_dot
cmp ecx,5
jne fate_char_store
fate_char_date_dot:
mov BYTE PTR [esi+ecx],'.'
inc ecx
jmp fate_char_store
fate_char_digit:
cmp eax,'0'
jb fate_char_done
cmp eax,'9'
ja fate_char_done
fate_char_store:
mov [esi+ecx],al
inc ecx
mov fortuneFateLengths3D[ebx*4],ecx
mov BYTE PTR [esi+ecx],0
ret
fate_char_backspace:
test ecx,ecx
jz fate_char_done
dec ecx
mov fortuneFateLengths3D[ebx*4],ecx
mov BYTE PTR [esi+ecx],0
ret
fate_char_next:
inc ebx
cmp ebx,6
jb @F
xor ebx,ebx
@@:
mov fortuneFateField3D,ebx
fate_char_done:
ret
FortuneFateChar3D ENDP
; Validation is calendar-based, including leap years, not a birth-date heuristic.
ValidateFortuneBirth3D PROC uses ebx esi edi
local dayValue:DWORD,monthValue:DWORD
cmp fortuneFateLengths3D[8],10
jne fate_birth_bad
lea esi,fortuneFateFields3D[128]
cmp BYTE PTR [esi+2],'.'
jne fate_birth_bad
cmp BYTE PTR [esi+5],'.'
jne fate_birth_bad
xor ebx,ebx
xor edi,edi
fate_birth_parse:
movzx eax,BYTE PTR [esi+edi]
cmp edi,2
je fate_birth_day
cmp edi,5
je fate_birth_month
sub eax,'0'
cmp eax,9
ja fate_birth_bad
imul ebx,10
add ebx,eax
jmp fate_birth_next
fate_birth_day:
mov dayValue,ebx
xor ebx,ebx
jmp fate_birth_next
fate_birth_month:
mov monthValue,ebx
xor ebx,ebx
fate_birth_next:
inc edi
cmp edi,10
jb fate_birth_parse
cmp ebx,1900
jb fate_birth_bad
cmp ebx,2099
ja fate_birth_bad
mov eax,monthValue
dec eax
cmp eax,11
ja fate_birth_bad
movzx ecx,fortuneFateMonthDays3D[eax]
cmp eax,1
jne fate_birth_check_day
test ebx,3
jnz fate_birth_check_day
cmp ebx,1900
je fate_birth_check_day
inc ecx
fate_birth_check_day:
cmp dayValue,1
jb fate_birth_bad
cmp dayValue,ecx
ja fate_birth_bad
mov eax,1
ret
fate_birth_bad:
xor eax,eax
ret
ValidateFortuneBirth3D ENDP
ParseFortuneFateNumber3D PROC uses ebx esi slot:DWORD
mov eax,slot
mov ecx,fortuneFateLengths3D[eax*4]
test ecx,ecx
jz fate_number_bad
cmp ecx,3
ja fate_number_bad
shl eax,6
lea esi,fortuneFateFields3D[eax]
xor eax,eax
fate_number_parse:
movzx ebx,BYTE PTR [esi]
sub ebx,'0'
cmp ebx,9
ja fate_number_bad
imul eax,10
add eax,ebx
inc esi
loop fate_number_parse
cmp eax,100
ja fate_number_bad
ret
fate_number_bad:
mov eax,-1
ret
ParseFortuneFateNumber3D ENDP
SubmitFortuneFate3D PROC uses ebx
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne fate_submit_done
cmp fortunePhase3D,0
jne fate_submit_done
xor ebx,ebx
fate_submit_required:
cmp fortuneFateLengths3D[ebx*4],0
je fate_submit_missing
inc ebx
cmp ebx,6
jb fate_submit_required
call ValidateFortuneBirth3D
test eax,eax
jz fate_submit_date
invoke ParseFortuneFateNumber3D,4
cmp eax,-1
je fate_submit_low
mov fortuneFateLowValue3D,eax
invoke ParseFortuneFateNumber3D,5
cmp eax,-1
je fate_submit_high
cmp eax,fortuneFateLowValue3D
jb fate_submit_high
mov fortuneFateHighValue3D,eax
sub eax,fortuneFateLowValue3D
inc eax
; Exactly one draw, independent of identity. Renders and double clicks never reroll.
invoke RandomRange3D,eax
add eax,fortuneFateLowValue3D
mov fortuneFateResult3D,eax
mov fortunePhase3D,1
mov fortunePhaseTime3D,0
mov eax,animationClock3D
mov fortuneSessionClock3D,eax
invoke SetFortuneSpeech3D,OFFSET fortuneFateRising3D
fate_submit_done:
ret
fate_submit_missing:
mov fortuneFateField3D,ebx
invoke SetFortuneSpeech3D,OFFSET fortuneFateRequired3D
ret
fate_submit_date:
mov fortuneFateField3D,2
invoke SetFortuneSpeech3D,OFFSET fortuneFateDateError3D
ret
fate_submit_low:
mov fortuneFateField3D,4
jmp fate_submit_range
fate_submit_high:
mov fortuneFateField3D,5
fate_submit_range:
invoke SetFortuneSpeech3D,OFFSET fortuneFateRangeError3D
ret
SubmitFortuneFate3D ENDP
DrawFortuneFateForm3D PROC uses ebx esi edi
local px:DWORD,py:DWORD,edgeInk:DWORD
invoke DrawBoxBorder,8,149,304,74,00081322h,0046637Fh,1
invoke DrawBuckshotText3D,24,304,OFFSET fortuneFatePlay3D,0086ABBCh
xor esi,esi
fate_form_field:
mov eax,fortuneFateXs3D[esi*4]
mov px,eax
shr eax,1
mov ebx,fortuneFateYs3D[esi*4]
mov py,ebx
shr ebx,1
mov edgeInk,00314D65h
cmp esi,fortuneFateField3D
jne @F
mov edgeInk,0088DDEAh
@@:
invoke DrawBoxBorder,eax,ebx,144,11,000B1D30h,edgeInk,1
mov eax,px
add eax,8
mov ebx,py
sub ebx,9
invoke DrawBuckshotText3D,eax,ebx,fortuneFateLabels3D[esi*4],00A9C8D8h
mov edi,esi
shl edi,6
add edi,OFFSET fortuneFateFields3D
mov eax,px
add eax,8
mov ebx,py
add ebx,7
cmp esi,2
jne fate_form_value
cmp fortuneFateLengths3D[8],0
jne fate_form_value
invoke DrawBuckshotText3D,eax,ebx,OFFSET fortuneFateHint3D,005A7A95h
jmp fate_form_caret
fate_form_value:
invoke DrawBuckshotText3D,eax,ebx,edi,00E8EFF4h
fate_form_caret:
cmp esi,fortuneFateField3D
jne fate_form_next
test animationClock3D,200h
jnz fate_form_next
mov eax,fortuneFateLengths3D[esi*4]
shl eax,3
add eax,px
add eax,8
mov ebx,py
add ebx,7
invoke DrawBuckshotText3D,eax,ebx,OFFSET fortuneCaret3D,00B8F3FFh
fate_form_next:
inc esi
cmp esi,6
jb fate_form_field
ret
DrawFortuneFateForm3D ENDP
DrawFortuneFateIdentity3D PROC uses ebx esi edi
invoke DrawBoxBorder,8,190,304,33,00081322h,0046637Fh,1
xor esi,esi
fate_identity_field:
mov eax,fortuneFateXs3D[esi*4]
add eax,8
mov ebx,esi
shr ebx,1
imul ebx,20
add ebx,388
mov edi,esi
shl edi,6
add edi,OFFSET fortuneFateFields3D
cmp esi,4
jb @F
invoke DrawBuckshotText3D,eax,ebx,fortuneFateLabels3D[esi*4],0086ABBCh
mov eax,fortuneFateXs3D[esi*4]
add eax,40
@@:
invoke DrawBuckshotText3D,eax,ebx,edi,00E8EFF4h
inc esi
cmp esi,6
jb fate_identity_field
ret
DrawFortuneFateIdentity3D ENDP
; One resolver supplies the frame, sprite placement and its measured glass centre.
ResolveFortuneOrb3D PROC uses ebx
mov eax,fortunePhaseTime3D
shr eax,6
and eax,15
mov fortuneOrbFrame3D,eax
mov ebx,eax
mov fortuneOrbX3D,214
mov fortuneOrbY3D,195
cmp fortunePhase3D,1
jne fate_orb_anchor
mov eax,fortunePhaseTime3D
cmp eax,1400
jbe @F
mov eax,1400
@@:
imul eax,135
xor edx,edx
mov ecx,1400
div ecx
mov ecx,330
sub ecx,eax
mov fortuneOrbY3D,ecx
fate_orb_anchor:
mov eax,fortuneOrbAnchorX3D[ebx*4]
add eax,128
shr eax,8
add eax,fortuneOrbX3D
mov fortuneOrbCenterX3D,eax
mov eax,fortuneOrbAnchorY3D[ebx*4]
add eax,128
shr eax,8
add eax,fortuneOrbY3D
mov fortuneOrbCenterY3D,eax
ret
ResolveFortuneOrb3D ENDP
DrawFortuneFateResult3D PROC uses ebx esi edi
local numberX:DWORD,numberY:DWORD
cmp fortuneFateResult3D,100
ja fate_result_done
invoke FormatNumber,fortuneFateResult3D,OFFSET fortuneFateNumber3D
lea edi,fortuneFateNumber3D
xor ebx,ebx
fate_result_length:
cmp BYTE PTR [edi+ebx],0
je fate_result_place
inc ebx
jmp fate_result_length
fate_result_place:
dec ebx
imul ebx,20
add ebx,32
shr ebx,1
neg ebx
add ebx,fortuneOrbCenterX3D
mov numberX,ebx
mov eax,fortuneOrbCenterY3D
sub eax,24
mov numberY,eax
; Fade into the mist at a constant size; preserve opacity for later draws.
push animOpacity3D
mov eax,fortunePhaseTime3D
cmp eax,900
jbe @F
mov eax,900
@@:
imul eax,255
xor edx,edx
mov ecx,900
div ecx
mov animOpacity3D,eax
fate_result_digit:
movzx esi,BYTE PTR [edi]
test esi,esi
jz fate_result_restore
sub esi,'0'
invoke DrawAnimSprite3D,OFFSET fortuneSessionNumerals3D,esi,numberX,numberY,32,48
add numberX,20
inc edi
jmp fate_result_digit
fate_result_restore:
pop animOpacity3D
fate_result_done:
ret
DrawFortuneFateResult3D ENDP

View file

@ -2,6 +2,7 @@ MODE_FORTUNE equ 25
FORTUNE_SUBMODE_MENU equ 0
FORTUNE_SUBMODE_TAROT equ 1
FORTUNE_SUBMODE_ORB equ 2
FORTUNE_SESSION_PACK_COUNT equ 8
BeginFortuneDialog3D PROTO
HandleFortuneDialog3D PROTO
RenderFortuneDialog3D PROTO
@ -15,6 +16,15 @@ SetFortuneSpeech3D PROTO :DWORD
FortuneCardSpeech3D PROTO :DWORD
DrawFortuneSpeech3D PROTO
DrawFortuneSessionCard3D PROTO :DWORD
ResetFortuneFate3D PROTO
FortuneFateChar3D PROTO :DWORD
SubmitFortuneFate3D PROTO
ValidateFortuneBirth3D PROTO
ParseFortuneFateNumber3D PROTO :DWORD
DrawFortuneFateForm3D PROTO
DrawFortuneFateIdentity3D PROTO
DrawFortuneFateResult3D PROTO
ResolveFortuneOrb3D PROTO
.data
fortuneSessionPacks3D LABEL BYTE
fortuneSessionRoom3D AnimPack3D <>
@ -24,6 +34,7 @@ fortuneSessionGesture3D AnimPack3D <>
fortuneSessionOrbHands3D AnimPack3D <>
fortuneSessionMist3D AnimPack3D <>
fortuneSessionCards3D AnimPack3D <>
fortuneSessionNumerals3D AnimPack3D <>
fortuneSessionPath0 db "assets\animations\fortune\room-v1.bin",0
fortuneSessionPath1 db "assets\animations\fortune\shuffle-v1.bin",0
fortuneSessionPath2 db "assets\animations\fortune\deal-v1.bin",0
@ -31,8 +42,10 @@ fortuneSessionPath3 db "assets\animations\fortune\gesture-v1.bin",0
fortuneSessionPath4 db "assets\animations\fortune\orb-hands-v1.bin",0
fortuneSessionPath5 db "assets\animations\fortune\orb-mist-v1.bin",0
fortuneSessionPath6 db "assets\animations\fortune\cards-v1.bin",0
fortuneSessionPath7 db "assets\animations\fortune\orb-numerals-v1.bin",0
fortuneSessionPaths3D dd OFFSET fortuneSessionPath0,OFFSET fortuneSessionPath1,OFFSET fortuneSessionPath2,OFFSET fortuneSessionPath3
dd OFFSET fortuneSessionPath4,OFFSET fortuneSessionPath5,OFFSET fortuneSessionPath6
dd OFFSET fortuneSessionPath7
fortuneSubMode3D dd 0
fortunePhase3D dd 0
fortunePhaseTime3D dd 0
@ -120,6 +133,46 @@ fortuneCardNames3D dd OFFSET fortuneCardName0,OFFSET fortuneCardName1,OFFSET for
dd OFFSET fortuneCardName12,OFFSET fortuneCardName13,OFFSET fortuneCardName14,OFFSET fortuneCardName15
dd OFFSET fortuneCardName16,OFFSET fortuneCardName17,OFFSET fortuneCardName18,OFFSET fortuneCardName19
dd OFFSET fortuneCardName20,OFFSET fortuneCardName21
; Local, ephemeral identity fields dress a fictional crystal-ball lottery.
fortuneFateField3D dd 0
fortuneFateLengths3D dd 6 dup(0)
fortuneFateFields3D db 384 dup(0)
fortuneFateLowValue3D dd 9
fortuneFateHighValue3D dd 10
fortuneFateResult3D dd -1
fortuneFateLimits3D dd 30,30,10,20,3,3
fortuneFateXs3D dd 16,336,16,336,16,336
fortuneFateYs3D dd 324,324,366,366,408,408
fortuneFateLabels3D dd OFFSET fortuneFateName3D,OFFSET fortuneFateSurname3D,OFFSET fortuneFateBirth3D,OFFSET fortuneFateGroup3D
dd OFFSET fortuneFateLow3D,OFFSET fortuneFateHigh3D
fortuneFateMonthDays3D db 31,28,31,30,31,30,31,31,30,31,30,31
fortuneFateNumber3D db 12 dup(0)
fortuneOrbFrame3D dd 0
fortuneOrbX3D dd 214
fortuneOrbY3D dd 195
fortuneOrbCenterX3D dd 0
fortuneOrbCenterY3D dd 0
include src_graphics\fortune_orb_art.inc
fortuneFateName3D db 200,204,223,0
fortuneFateSurname3D db 212,192,204,200,203,200,223,0
fortuneFateBirth3D db 196,192,210,192,32,208,206,198,196,197,205,200,223,0
fortuneFateGroup3D db 195,208,211,207,207,192,0
fortuneFateLow3D db 206,210,0
fortuneFateHigh3D db 196,206,0
fortuneFateStart3D db 207,208,200,199,194,192,210,220,0
fortuneFateEdit3D db 200,199,204,197,205,200,210,220,0
fortuneFateHint3D db 196,196,46,204,204,46,195,195,195,195,0
fortuneFateReady3D db 205,224,231,238,226,232,32,242,238,227,238,44,10,247,252,254,32,241,243,228,252,225,243,32,240,229,248,232,236,46,0
fortuneFateRising3D db 210,232,248,229,46,10,216,224,240,32,239,240,238,225,243,230,228,224,229,242,241,255,46,0
fortuneFateSwirl3D db 210,243,236,224,237,32,241,238,225,232,240,224,229,242,241,255,46,46,46,10,207,243,241,242,252,32,241,243,228,252,225,224,10,237,224,231,238,226,184,242,32,247,232,241,235,238,46,0
fortuneFateReveal3D db 208,229,248,229,237,232,229,32,239,240,232,237,255,242,238,46,10,216,224,240,32,237,224,231,226,224,235,32,247,232,241,235,238,46,0
fortuneFateRequired3D db 205,229,32,226,241,229,32,232,236,229,237,224,10,237,224,231,226,224,237,251,46,0
fortuneFateDateError3D db 210,224,234,238,233,32,228,224,242,251,32,237,229,242,46,10,205,224,231,238,226,232,32,228,229,237,252,32,240,238,230,228,229,237,232,255,46,0
fortuneFateRangeError3D db 205,224,231,238,226,232,32,227,240,224,237,232,246,251,58,10
db 238,242,32,236,229,237,252,248,229,227,238,32,234,32,225,238,235,252,248,229,236,243,44,10
db 238,242,32,48,32,228,238,32,49,48,48,46,0
fortuneFateCaption3D db 208,197,216,197,205,200,197,32,209,211,196,220,193,219,0
fortuneFatePlay3D db 200,195,208,206,194,206,201,32,208,200,210,211,192,203,0
.data?
fortuneBackground3D dd SCREEN_SIZE dup(?)
.data

View file

@ -0,0 +1,3 @@
; Generated by tools/generators/pack_fortune_orb.py; glass only, pedestal excluded.
fortuneOrbAnchorX3D dd 18261,16299,16299,14336,18176,16213,16213,14336,18176,16299,16299,14421,18091,16384,16128,14507
fortuneOrbAnchorY3D dd 14610,14637,14628,14630,14030,14040,14055,14066,13948,13933,13940,13922,12327,12342,12305,12324

View file

@ -169,4 +169,5 @@ include src_graphics\hall_expansion_data.inc
include src_graphics\fortune_room_data.inc
include src_graphics\fortune_dialog_data.inc
include src_graphics\cabaret_data.inc
include src_graphics\rooftop_data.inc
.code

View file

@ -194,6 +194,9 @@ interior_begin_finish:
BeginWalkableStairs3D ENDP
UpdateInteriorTravel3D PROC
call UpdateRoof3D
cmp hallLevel3D,HALL_ROOF
je interior_travel_done
call UpdateUpperEffects3D
call UpdateBasementDoor3D
call UpdateFortunePassage3D
@ -410,6 +413,11 @@ UpdateInteriorEye3D PROC
je UpdateStairHeight3D
cmp engineMode,MODE_WALK
jne interior_eye_done
cmp hallLevel3D,HALL_ROOF
jne @F
call UpdateRoofEye3D
ret
@@:
mov viewHorizon3D,HALF_H
mov viewEye3D,HALL_STANDING_EYE
mov viewCeiling3D,FP_1*5/2

View file

@ -2,11 +2,17 @@
RenderCeilingAndFloor PROC uses ebx esi edi
local row:DWORD,distance:DWORD,sx:DWORD,sy:DWORD,wx:DWORD,wy:DWORD
local material:DWORD,shade:DWORD
cmp hallLevel3D,HALL_ROOF
jne @F
call DrawRoofSky3D
@@:
mov row, 0
hall_plane_row:
mov eax, row
cmp eax, viewHorizon3D
jge hall_plane_floor
cmp hallLevel3D,HALL_ROOF
je hall_plane_next_row
cmp outdoorRender3D, 0
jne hall_plane_next_row
mov ecx, viewHorizon3D
@ -96,7 +102,18 @@ hall_plane_distance:
imul eax, SCREEN_W*4
lea edi, screenBuffer[eax]
mov esi, SCREEN_W
hall_plane_pixel:
hall_plane_pixel:
cmp hallLevel3D,HALL_ROOF
jne roof_floor_inside
cmp wx,FP_1*109/10
jl roof_floor_outside
cmp wx,FP_1*291/10
jg roof_floor_outside
cmp wy,FP_1*9/10
jl roof_floor_outside
cmp wy,FP_1*211/10
jg roof_floor_outside
roof_floor_inside:
mov ebx, material
cmp outdoorRender3D, 0
je hall_plane_sample
@ -129,7 +146,13 @@ hall_plane_sample:
jl hall_plane_choose_hd
hall_plane_wood:
mov ebx, 85
hall_plane_choose_hd:
hall_plane_choose_hd:
cmp ebx,ROOF_DECK
jne hall_plane_not_roof
mov ecx,roofMaterialArt3D.pixels
test ecx,ecx
jnz hall_plane_hd_coordinates
hall_plane_not_roof:
cmp ebx,132
jb hall_plane_other_hd
cmp ebx,135
@ -237,6 +260,9 @@ hall_plane_next_row:
cmp row, SCREEN_H
jb hall_plane_row
ret
roof_floor_outside:
mov eax,[edi]
jmp hall_plane_store
RenderCeilingAndFloor ENDP
RenderSprites PROC uses ebx esi edi
@ -330,7 +356,8 @@ hall_sprites_done:
ret
RenderSprites ENDP
InitHall3D PROC uses esi edi
InitHall3D PROC uses esi edi
call InitRoof3D
lea esi, hallLayout3D
lea edi, casinoMap
mov ecx, 256
@ -370,7 +397,8 @@ init_plane_done:
ret
InitHall3D ENDP
LoadHallAssets3D PROC
LoadHallAssets3D PROC
call LoadRoofAssets3D
call InitUpperSurfaces3D
call LoadFortuneArt3D
invoke LoadAnimPack3D, OFFSET upperMaterialArt3D, OFFSET upperMaterialPath3D
@ -469,7 +497,8 @@ hall_load_sign:
ret
LoadHallAssets3D ENDP
FreeHallAssets3D PROC
FreeHallAssets3D PROC
call FreeRoofAssets3D
call FreeFortuneArt3D
invoke FreeAnimPack3D, OFFSET upperMaterialArt3D
invoke FreeAnimPack3D,OFFSET upperGamesArt3D
@ -574,6 +603,11 @@ surface_missing:
ResolveWorldSurface3D ENDP
RenderHall3D PROC uses ebx esi
cmp hallLevel3D,HALL_ROOF
jne @F
call RenderRoof3D
ret
@@:
cmp engineMode,MODE_STAIRS
je fortune_hall_stairs
cmp hallLevel3D,3
@ -694,6 +728,7 @@ hall_render_lounge:
call RenderHallDressing3D
call RenderHallPortals3D
call RenderFortuneEntrance3D
call RenderLift3D
call ApplyHallLighting3D
call RenderInteriorViews3D
call DrawBasementLeaf3D
@ -759,7 +794,12 @@ hall_target_done:
ret
HallTarget3D ENDP
CheckHallInteractions3D PROC
CheckHallInteractions3D PROC
cmp hallLevel3D,HALL_ROOF
jne @F
call CheckRoofInteractions3D
ret
@@:
cmp hallLevel3D,3
jne @F
call CheckFortuneInteractions3D
@ -790,6 +830,7 @@ hall_check_other:
invoke HallTarget3D,FP_1*305/10,21*FP_1,0,OFFSET upperStairPrompt3D
call CheckNpcInteractions3D
call CheckFortuneInteractions3D
call CheckRoofInteractions3D
ret
hall_check_bunker:
mov interactTargetId, 0
@ -805,6 +846,9 @@ TriggerHallInteraction3D PROC
mov eax,1
ret
@@:
call TryRoofInteraction3D
test eax,eax
jnz hall_trigger_handled
cmp interactTargetId,INTERACT_DARTS
jne hall_not_darts
call BeginDarts3D
@ -978,6 +1022,11 @@ RenderBar3D PROC uses ebx
jne @F
mov playerX,FP_1*25/2
mov playerY,16*FP_1+FP_1/3
@@:
cmp hallLevel3D,HALL_ROOF
jne @F
mov playerX,15*FP_1
mov playerY,FP_1*63/10
@@:
mov viewHorizon3D, HALF_H
mov viewEye3D, HALL_STANDING_EYE
@ -1007,6 +1056,10 @@ bar_draw_title:
cmp hallLevel3D,2
jne @F
mov eax,OFFSET upperBarName3D
@@:
cmp hallLevel3D,HALL_ROOF
jne @F
mov eax,OFFSET roofBarName3D
@@:
invoke DrawString8x8, 18,15,eax,00E7CA88h
cmp barPhase3D, 2
@ -1094,7 +1147,15 @@ city_world_guard_right:
ret
RenderCityGuards3D ENDP
CanWalkHall3D PROC uses esi ebx px:DWORD,py:DWORD
CanWalkHall3D PROC uses esi ebx px:DWORD,py:DWORD
cmp hallLevel3D,HALL_ROOF
jne @F
invoke RoofCanWalk3D,px,py
ret
@@:
invoke LiftCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
cmp engineMode,MODE_STAIRS
je fortune_collision_stairs
cmp hallLevel3D,3
@ -1404,6 +1465,7 @@ RenderPatrons3D ENDP
include src_graphics\world_dressing.inc
include src_graphics\terminal.inc
include src_graphics\hall_expansion.inc
include src_graphics\rooftop.inc
include src_graphics\steel_door.inc
include src_graphics\interior_portals.inc
include src_graphics\fortune_room.inc

View file

@ -3,15 +3,15 @@
; ==============================================================================
MODE_JACKBOX equ 28
INTERACT_JACKBOX equ 48
INTERACT_JACKBOX equ 52
JACKBOX_X equ FP_1*306/10
JACKBOX_Y equ FP_1*235/10
JACKBOX_TRACK_COUNT equ 3
MATERIAL_JACKBOX_FRONT equ 161
MATERIAL_JACKBOX_FRONT equ 211
MATERIAL_JACKBOX_SIDE equ 121
MATERIAL_JACKBOX_BACK equ 163
MATERIAL_JACKBOX_BACK equ 213
LoadJackboxArt3D PROTO
FreeJackboxArt3D PROTO

View file

@ -431,6 +431,9 @@ present_geometry_ready:
jz @F
invoke PatBlt, h3DDC, eax, 0, ecx, edi, 42h
@@:
invoke PresentRoofHD3D,h3DDC,offsetX,offsetY,viewW,viewH
test eax,eax
jnz present_done
invoke StretchDIBits, h3DDC, offsetX, offsetY, viewW, viewH, 0, 0, SCREEN_W, SCREEN_H, OFFSET screenBuffer, OFFSET bmi3D, 0, SRCCOPY
present_done:
ret

1178
src_graphics/rooftop.inc Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,382 @@
; Directional dusk lighting and aerial perspective are applied to actual geometry.
ShadeRoofCityPixel3D PROC uses ebx esi edi color:DWORD,depth:DWORD,light:DWORD,material:DWORD
local fog:DWORD,inverseFog:DWORD,red:DWORD,green:DWORD,blue:DWORD,level:DWORD
local targetR:DWORD,targetG:DWORD,targetB:DWORD
mov roofPixelGlow3D,0
mov eax,depth
sar eax,16
test eax,eax
jns @F
xor eax,eax
@@:
cmp eax,1023
jbe @F
mov eax,1023
@@:
movzx eax,roofFog3D[eax]
mov fog,eax
neg eax
add eax,256
mov inverseFog,eax
mov eax,color
and eax,255
mov blue,eax
mov eax,color
shr eax,8
and eax,255
mov green,eax
mov eax,color
shr eax,16
and eax,255
mov red,eax
mov targetR,171
mov targetG,157
mov targetB,177
mov eax,light
mov level,eax
cmp roofNight3D,0
je roof_lighting_day
mov targetR,13
mov targetG,24
mov targetB,44
mov level,72
cmp material,ROOF_CITY_ROAD
jne @F
mov level,256
@@:
cmp material,ROOF_LIGHT
jne @F
mov level,256
mov roofPixelGlow3D,96
@@:
cmp roofLandmarkLight3D,0
je @F
mov level,148
@@:
cmp material,ROOF_BRASS
jne @F
mov level,228
cmp roofLandmarkLight3D,0
je @F
mov roofPixelGlow3D,12
@@:
; Emission is tagged in the actual window pixels, never inferred from gold trim.
cmp material,ROOF_FACADE
jb roof_lighting_apply
cmp material,ROOF_FACADE+7
jbe roof_lighting_window
cmp material,ROOF_MODERN_FACADE
jb roof_lighting_apply
cmp material,ROOF_MODERN_FACADE+7
ja roof_lighting_apply
roof_lighting_window:
mov eax,color
shr eax,24
cmp eax,255
jne roof_lighting_apply
mov level,310
mov roofPixelGlow3D,64
shr fog,1
mov eax,fog
neg eax
add eax,256
mov inverseFog,eax
jmp roof_lighting_apply
roof_lighting_day:
cmp material,ROOF_CITY_ROOF
jne @F
imul eax,red,180
shr eax,8
mov red,eax
imul eax,green,220
shr eax,8
mov green,eax
@@:
mov eax,red
imul eax,263
shr eax,8
cmp eax,255
jbe @F
mov eax,255
@@:
mov red,eax
imul eax,blue,235
shr eax,8
mov blue,eax
roof_lighting_apply:
mov eax,red
imul eax,level
shr eax,8
cmp eax,255
jbe @F
mov eax,255
@@:
imul eax,inverseFog
mov ecx,targetR
imul ecx,fog
add eax,ecx
shr eax,8
mov ebx,eax
shl ebx,16
mov eax,green
imul eax,level
shr eax,8
cmp eax,255
jbe @F
mov eax,255
@@:
imul eax,inverseFog
mov ecx,targetG
imul ecx,fog
add eax,ecx
shr eax,8
shl eax,8
or ebx,eax
mov eax,blue
imul eax,level
shr eax,8
cmp eax,255
jbe @F
mov eax,255
@@:
imul eax,inverseFog
mov ecx,targetB
imul ecx,fog
add eax,ecx
shr eax,8
or eax,ebx
ret
ShadeRoofCityPixel3D ENDP
SampleRoofGroundLight3D PROC uses ebx esi edi color:DWORD,wx:DWORD,wy:DWORD
mov eax,color
cmp roofLightingArt3D.pixels,0
je roof_ground_light_done
mov eax,wx
sar eax,17
add eax,256
cmp eax,511
ja roof_ground_light_original
mov ebx,eax
mov eax,wy
sar eax,17
add eax,256
cmp eax,511
ja roof_ground_light_original
shl eax,9
add eax,ebx
mov esi,roofLightingArt3D.pixels
cmp roofNight3D,0
je @F
add esi,512*512*4
@@:
mov edx,[esi+eax*4]
mov edi,edx
and edx,255
mov eax,color
and eax,255
imul eax,edx
shr eax,8
mov ebx,eax
mov edx,edi
shr edx,8
and edx,255
mov eax,color
shr eax,8
and eax,255
imul eax,edx
and eax,0FF00h
or ebx,eax
mov edx,edi
shr edx,16
and edx,255
mov eax,color
shr eax,16
and eax,255
imul eax,edx
and eax,0FF00h
shl eax,8
or eax,ebx
ret
roof_ground_light_original:
mov eax,color
roof_ground_light_done:
ret
SampleRoofGroundLight3D ENDP
PrepareRoofCityAA3D PROC uses ebx esi edi
mov roofAAStill3D,0
cmp roofAAValid3D,0
je roof_aa_moving
mov eax,playerX
cmp eax,roofAAX3D
jne roof_aa_moving
mov eax,playerY
cmp eax,roofAAY3D
jne roof_aa_moving
mov eax,playerAngle
cmp eax,roofAAAngle3D
jne roof_aa_moving
mov eax,viewEye3D
cmp eax,roofAAEye3D
jne roof_aa_moving
mov eax,viewHorizon3D
cmp eax,roofAAHorizon3D
jne roof_aa_moving
mov eax,roofNight3D
cmp eax,roofAATheme3D
jne roof_aa_moving
mov roofAAStill3D,1
inc roofAAFrame3D
and roofAAFrame3D,3
jmp roof_aa_save_view
roof_aa_moving:
mov roofAAFrame3D,0
roof_aa_save_view:
mov eax,playerX
mov roofAAX3D,eax
mov eax,playerY
mov roofAAY3D,eax
mov eax,playerAngle
mov roofAAAngle3D,eax
mov eax,viewEye3D
mov roofAAEye3D,eax
mov eax,viewHorizon3D
mov roofAAHorizon3D,eax
mov eax,roofNight3D
mov roofAATheme3D,eax
mov eax,roofAAFrame3D
shl eax,1
sub eax,3
mov ebx,eax
mov eax,playerPlaneX
imul ebx
cdq
mov ecx,SCREEN_W*4
idiv ecx
mov esi,eax
mov eax,playerPlaneY
imul ebx
cdq
idiv ecx
mov edi,eax
xor ebx,ebx
roof_aa_shift_ray:
add cityRayX3D[ebx*4],esi
add cityRayY3D[ebx*4],edi
inc ebx
cmp ebx,SCREEN_W
jb roof_aa_shift_ray
ret
PrepareRoofCityAA3D ENDP
FinishRoofCityAA3D PROC uses ebx esi edi
xor edi,edi
roof_aa_pixel:
mov eax,screenBuffer[edi*4]
cmp roofAAStill3D,0
je roof_aa_store
cmp roofCityMask3D[edi],1
jne roof_aa_store
and eax,00FCFCFCh
shr eax,2
mov edx,roofAAHistory3D[edi*4]
and edx,00FCFCFCh
shr edx,2
imul edx,3
add eax,edx
mov screenBuffer[edi*4],eax
roof_aa_store:
mov roofAAHistory3D[edi*4],eax
inc edi
cmp edi,SCREEN_SIZE
jb roof_aa_pixel
mov roofAAValid3D,1
ret
FinishRoofCityAA3D ENDP
RenderRoofCityBloom3D PROC uses ebx esi edi
local pixel:DWORD,color:DWORD,amount:DWORD,rowOffset:DWORD,columnOffset:DWORD
local target:DWORD,red:DWORD,green:DWORD,blue:DWORD
cmp roofNight3D,0
je roof_bloom_done
mov pixel,SCREEN_W+1
roof_bloom_source:
mov esi,pixel
movzx eax,roofGlowMask3D[esi]
test eax,eax
jz roof_bloom_next_source
shr eax,2
mov amount,eax
mov eax,screenBuffer[esi*4]
mov color,eax
mov rowOffset,-1
roof_bloom_row:
mov columnOffset,-1
roof_bloom_column:
mov eax,rowOffset
imul eax,SCREEN_W
add eax,columnOffset
add eax,pixel
cmp eax,SCREEN_SIZE
jae roof_bloom_next_column
mov target,eax
lea edi,screenBuffer[eax*4]
mov ebx,[edi]
mov eax,color
and eax,255
imul eax,amount
shr eax,8
movzx ecx,bl
add eax,ecx
cmp eax,255
jbe @F
mov eax,255
@@:
mov blue,eax
mov eax,color
shr eax,8
and eax,255
imul eax,amount
shr eax,8
mov ecx,ebx
shr ecx,8
and ecx,255
add eax,ecx
cmp eax,255
jbe @F
mov eax,255
@@:
mov green,eax
mov eax,color
shr eax,16
and eax,255
imul eax,amount
shr eax,8
shr ebx,16
and ebx,255
add eax,ebx
cmp eax,255
jbe @F
mov eax,255
@@:
shl eax,16
mov ecx,green
shl ecx,8
or eax,ecx
or eax,blue
mov [edi],eax
roof_bloom_next_column:
inc columnOffset
cmp columnOffset,1
jle roof_bloom_column
inc rowOffset
cmp rowOffset,1
jle roof_bloom_row
roof_bloom_next_source:
inc pixel
cmp pixel,SCREEN_SIZE-SCREEN_W-1
jb roof_bloom_source
roof_bloom_done:
ret
RenderRoofCityBloom3D ENDP

View file

@ -0,0 +1,156 @@
RenderRoofCityLamps3D PROC uses ebx esi edi
local px:DWORD,py:DWORD,x1:DWORD,y1:DWORD
xor esi,esi
roof_city_lamp:
mov eax,roofCityLamps3D[esi*8]
mov px,eax
mov edx,roofCityLamps3D[esi*8+4]
mov py,edx
invoke ProjectCityPoint3D,eax,edx
cmp edx,FP_1
jle roof_city_lamp_next
cmp edx,180*FP_1
ja roof_city_lamp_next
cmp eax,-8
jl roof_city_lamp_next
cmp eax,SCREEN_W+8
jg roof_city_lamp_next
mov eax,px
add eax,FP_1/12
mov x1,eax
mov eax,py
add eax,FP_1/12
mov y1,eax
invoke DrawRoofSolid3D,px,x1,py,y1,ROOF_CITY_STREET_Z,ROOF_CITY_STREET_Z+5*FP_1,ROOF_METAL
mov eax,px
sub eax,FP_1/5
mov edx,px
add edx,FP_1/5
mov ecx,py
sub ecx,FP_1/5
mov edi,py
add edi,FP_1/5
invoke DrawRoofSolid3D,eax,edx,ecx,edi,ROOF_CITY_STREET_Z+FP_1*49/10,ROOF_CITY_STREET_Z+FP_1*51/10,ROOF_LIGHT
roof_city_lamp_next:
inc esi
cmp esi,ROOF_CITY_LAMP_COUNT
jb roof_city_lamp
ret
RenderRoofCityLamps3D ENDP
DrawRoofCar3D PROC uses ebx esi edi centreX:DWORD,centreY:DWORD,axis:DWORD,style:DWORD
local x0:DWORD,x1:DWORD,y0:DWORD,y1:DWORD,bodyMat:DWORD,cabinX0:DWORD,cabinX1:DWORD
local cabinY0:DWORD,cabinY1:DWORD
invoke ProjectCityPoint3D,centreX,centreY
cmp edx,FP_1
jle roof_car_done
cmp eax,-30
jl roof_car_done
cmp eax,SCREEN_W+30
jg roof_car_done
mov ebx,FP_1*19/10
mov ecx,FP_1*8/10
cmp axis,0
je @F
xchg ebx,ecx
@@:
mov eax,centreX
sub eax,ebx
mov x0,eax
mov eax,centreX
add eax,ebx
mov x1,eax
mov eax,centreY
sub eax,ecx
mov y0,eax
mov eax,centreY
add eax,ecx
mov y1,eax
mov bodyMat,ROOF_CITY_ROOF
test style,1
jz @F
mov bodyMat,ROOF_BRASS
@@:
invoke DrawRoofSolid3D,x0,x1,y0,y1,ROOF_CITY_STREET_Z+FP_1/4,ROOF_CITY_STREET_Z+FP_1*85/100,bodyMat
mov ebx,FP_1*9/10
mov ecx,FP_1*65/100
cmp axis,0
je @F
xchg ebx,ecx
@@:
mov eax,centreX
sub eax,ebx
mov cabinX0,eax
mov eax,centreX
add eax,ebx
mov cabinX1,eax
mov eax,centreY
sub eax,ecx
mov cabinY0,eax
mov eax,centreY
add eax,ecx
mov cabinY1,eax
invoke DrawRoofSolid3D,cabinX0,cabinX1,cabinY0,cabinY1,ROOF_CITY_STREET_Z+FP_1*85/100,ROOF_CITY_STREET_Z+FP_1*135/100,ROOF_MODERN_FACADE+1
roof_car_done:
ret
DrawRoofCar3D ENDP
RenderRoofTraffic3D PROC uses ebx esi edi
local px:DWORD,py:DWORD,material:DWORD
xor esi,esi
roof_city_car:
mov eax,esi
imul eax,17000
add eax,animationClock3D
mov ecx,393
mul ecx
mov ecx,340*FP_1
div ecx
sub edx,160*FP_1
mov px,edx
mov py,-FP_1*59/10
test esi,1
jz @F
neg edx
mov px,edx
mov py,-FP_1*81/10
@@:
cmp esi,4
jb roof_city_car_x
mov eax,px
mov py,eax
mov px,FP_1*391/10
test esi,1
jz @F
mov px,FP_1*369/10
@@:
invoke DrawRoofCar3D,px,py,1,esi
jmp roof_city_car_next
roof_city_car_x:
invoke DrawRoofCar3D,px,py,0,esi
roof_city_car_next:
inc esi
cmp esi,8
jb roof_city_car
xor esi,esi
roof_city_boat:
imul eax,esi,47000
add eax,animationClock3D
mov ecx,98
mul ecx
mov ecx,340*FP_1
div ecx
sub edx,160*FP_1
mov px,edx
mov py,68*FP_1
invoke DrawRoofEllipsoid3D,px,py,ROOF_CITY_STREET_Z-FP_1*16/10,3*FP_1,FP_1,FP_1/2,ROOF_CITY_ROOF
mov eax,px
sub eax,FP_1
mov ebx,px
add ebx,FP_1
invoke DrawRoofSolid3D,eax,ebx,FP_1*674/10,FP_1*686/10,ROOF_CITY_STREET_Z-FP_1*14/10,ROOF_CITY_STREET_Z-FP_1*7/10,ROOF_PLASTER
inc esi
cmp esi,3
jb roof_city_boat
ret
RenderRoofTraffic3D ENDP

View file

@ -0,0 +1,202 @@
; Native walkable rooftop and elevator share the existing hall engine.
HALL_ROOF equ 4
INTERACT_LIFT_CALL equ 48
INTERACT_LIFT_TRAVEL equ 49
INTERACT_ROOF_SEAT equ 50
INTERACT_ROOF_RAIL equ 51
ROOF_STANDING_EYE equ FP_1*165/100
ROOF_SEATED_EYE equ FP_1*118/100
ROOF_RAIL_HEIGHT equ FP_1*102/100
ROOF_FAR_DEPTH equ FP_1*1024
ROOF_FACADE equ 186
ROOF_FOLIAGE equ 194
ROOF_EARTH equ 195
ROOF_CITY_ROOF equ 196
ROOF_CITY_ROAD equ 197
ROOF_CITY_WATER equ 198
ROOF_MODERN_FACADE equ 199
ROOF_CITY_LAWN equ 207
ROOF_LAST_MATERIAL equ 207
ROOF_CITY_STREET_Z equ -60*FP_1
ROOF_DECK equ 161
ROOF_STONE equ 162
ROOF_TEAL equ 163
ROOF_IVORY equ 164
ROOF_METAL equ 165
ROOF_BRASS equ 166
ROOF_MARBLE equ 167
ROOF_PLASTER equ 168
ROOF_DOOR equ 169
ROOF_BUTTON equ 170
ROOF_BAR_FRONT equ 171
ROOF_BOTTLES equ 172
ROOF_TREE equ 173
ROOF_GRASS equ 174
ROOF_LANTERN equ 175
ROOF_TABLE equ 176
ROOF_INDICATOR equ 177
ROOF_LIGHT equ 178
ROOF_SOFA_FRONT equ 179
ROOF_SOFA_SIDE equ 180
ROOF_SOFA_BACK equ 181
ROOF_CHAIR_FRONT equ 182
ROOF_CHAIR_SIDE equ 183
ROOF_CHAIR_BACK equ 184
ROOF_BAR_STOOL equ 185
ROOF_SEAT_COUNT equ 13
ROOF_TREE_COUNT equ 8
ROOF_GUEST_COUNT equ 8
LIFT_CENTRE_X equ FP_1*244/10
LIFT_FRONT_Y equ FP_1*345/100
InitRoof3D PROTO
LoadRoofAssets3D PROTO
FreeRoofAssets3D PROTO
ResolveRoofSurface3D PROTO :DWORD
DrawRoofSky3D PROTO
RenderRoof3D PROTO
RenderLift3D PROTO
UpdateRoof3D PROTO
CheckRoofInteractions3D PROTO
TryRoofInteraction3D PROTO
LiftCanWalk3D PROTO :DWORD,:DWORD
RoofCanWalk3D PROTO :DWORD,:DWORD
UpdateRoofEye3D PROTO
UpdateRoofLook3D PROTO
StandFromRoofSeat3D PROTO
DrawRoofSofa3D PROTO :DWORD,:DWORD
DrawRoofChair3D PROTO :DWORD,:DWORD,:DWORD
DrawRoofCoffee3D PROTO :DWORD,:DWORD
DrawRoofTree3D PROTO :DWORD,:DWORD,:DWORD
DrawRoofBulb3D PROTO :DWORD,:DWORD,:DWORD
UpdateRoofIndicator3D PROTO
DrawRoofSolid3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
DrawRoofUpholstery3D PROTO :DWORD,:DWORD,:DWORD
DrawRoofPlanter3D PROTO :DWORD,:DWORD,:DWORD,:DWORD
RenderRoofDistrict3D PROTO
UpdateRoofGuests3D PROTO :DWORD
CheckRoofRail3D PROTO
StartRoofLean3D PROTO
RenderRoofLean3D PROTO
DrawRoofHandsPlane3D PROTO :DWORD,:DWORD,:DWORD
ShadeRoofNight3D PROTO
PrepareRoofFloorDepth3D PROTO
DrawRoofEllipsoid3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
DrawRoofPadding3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
RenderRoofCityGround3D PROTO
RenderRoofLandmarks3D PROTO
RenderRoofCityParks3D PROTO
ShadeRoofCityPixel3D PROTO :DWORD,:DWORD,:DWORD,:DWORD
DrawRoofSlope3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
DrawRoofPyramid3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
SampleRoofGroundLight3D PROTO :DWORD,:DWORD,:DWORD
RenderRoofCityLamps3D PROTO
RenderRoofTraffic3D PROTO
DrawRoofCar3D PROTO :DWORD,:DWORD,:DWORD,:DWORD
PrepareRoofCityAA3D PROTO
FinishRoofCityAA3D PROTO
RenderRoofCityBloom3D PROTO
include src_graphics\rooftop_district_data.inc
include src_graphics\rooftop_map.inc
include src_graphics\rooftop_gl_data.inc
.data
roofSkyArt3D AnimPack3D <>
roofMaterialArt3D AnimPack3D <>
roofDetailArt3D AnimPack3D <>
roofCityArt3D AnimPack3D <>
roofCityNightArt3D AnimPack3D <>
roofHandsArt3D AnimPack3D <>
roofNightArt3D AnimPack3D <>
roofLightingArt3D AnimPack3D <>
roofLightingPath3D db "assets\animations\rooftop\lighting-v1.bin",0
roofCityPath3D db "assets\animations\rooftop\city-v1.bin",0
roofCityNightPath3D db "assets\animations\rooftop\city-night-v1.bin",0
roofHandsPath3D db "assets\animations\rooftop\rail-hands-v1.bin",0
roofNightPath3D db "assets\animations\rooftop\night-v1.bin",0
roofNight3D dd 0
roofCityPass3D dd 0
roofLandmarkLight3D dd 0
roofPixelGlow3D dd 0
roofAAValid3D dd 0
roofAAStill3D dd 0
roofAAFrame3D dd 0
roofAAX3D dd 0
roofAAY3D dd 0
roofAAAngle3D dd 0
roofAAEye3D dd 0
roofAAHorizon3D dd 0
roofAATheme3D dd 0
roofSlopeMask3D dd 0
roofSlopeCentreX3D dd 0
roofSlopeCentreY3D dd 0
roofSlopeHalfX3D dd 0
roofSlopeHalfY3D dd 0
roofLean3D dd 0
roofLeanTime3D dd 0
roofLeanAngle3D dd 0
roofLeanSide3D dd 0
roofHandsContactX3D dd 320
roofHandsWidth3D dd 640
roofGuestClock3D dd 0
roofGroundRoadPixels3D dd 0
roofGroundWaterPixels3D dd 0
roofGuestStep3D dd ROOF_GUEST_COUNT dup(0)
roofGuestWait3D dd 1200,2400,3600,1800,3000,4200,2000,3300
roofGuestMoving3D dd ROOF_GUEST_COUNT dup(0)
roofGuestDistance3D dd ROOF_GUEST_COUNT dup(0)
roofGuestDir3D dd ROOF_GUEST_COUNT dup(0)
roofFloatFP3D REAL4 65536.0
roofFloatHalfTex3D REAL4 127.5
roofFloatShade3D REAL4 44.0
roofFloatAmbient3D REAL4 205.0
roofFloat4803D REAL4 480.0
roofSkyPath3D db "assets\animations\rooftop\panorama-v1.bin",0
roofMaterialPath3D db "assets\animations\rooftop\materials-v1.bin",0
roofDetailPath3D db "assets\animations\rooftop\details-v1.bin",0
liftPhase3D dd 0
liftDoor3D dd 0
liftTrip3D dd 0
liftDestination3D dd 0
liftRideTime3D dd 0
liftAwayTime3D dd 0
roofUpdateClock3D dd -1
roofIndicatorPixel3D dd 0FFFFE9BDh
roofSeat3D dd 0
roofReturnX3D dd 0
roofReturnY3D dd 0
roofReturnAngle3D dd 0
roofSeatX3D dd FP_1*145/10,FP_1*245/10,FP_1*195/10,FP_1*177/10,FP_1*206/10,FP_1*145/10,FP_1*245/10
dd 13*FP_1,15*FP_1,17*FP_1,13*FP_1,13*FP_1,13*FP_1
roofSeatY3D dd FP_1*95/10,FP_1*95/10,18*FP_1,FP_1*95/10,FP_1*95/10,15*FP_1,15*FP_1
dd FP_1*61/10,FP_1*61/10,FP_1*61/10,FP_1*124/10,FP_1*139/10,FP_1*154/10
roofSeatKind3D dd 0,0,0,1,1,1,1,2,2,2,3,3,3
roofTargetSeat3D dd 0
roofTreeX3D dd 12*FP_1,28*FP_1,12*FP_1,28*FP_1,FP_1*194/10,FP_1*215/10,FP_1*175/10,FP_1*278/10
roofTreeY3D dd FP_1*68/10,8*FP_1,FP_1*189/10,16*FP_1,FP_1*134/10,FP_1*195/10,FP_1*195/10,FP_1*45/10
roofPostX3D dd 12*FP_1,18*FP_1,12*FP_1,18*FP_1,FP_1*117/10,FP_1*283/10,FP_1*117/10,FP_1*283/10
roofPostY3D dd FP_1*15/10,FP_1*15/10,FP_1*62/10,FP_1*62/10,7*FP_1,7*FP_1,15*FP_1,15*FP_1
roofGuestX3D dd FP_1*188/10,FP_1*165/10,FP_1*265/10,FP_1*163/10,FP_1*221/10,FP_1*263/10,FP_1*155/10,FP_1*205/10
roofGuestY3D dd FP_1*65/10,FP_1*75/10,13*FP_1,FP_1*119/10,FP_1*122/10,FP_1*188/10,FP_1*188/10,FP_1*16/10+15*FP_1
roofGuestFrame3D dd 0,24,12,6,18,30,0,24
roofActor3D CityProp3D <0,0,FP_1*75/100,FP_1*175/100,0,OFFSET npcGuestArt3D>
roofTender3D CityProp3D <15*FP_1,FP_1*34/10,FP_1,FP_1,0,OFFSET hallBartender3D,FP_1*9/20>
roofBarName3D db 203,192,211,205,198,45,193,192,208,32,34,194,219,209,206,210,192,34,0
roofLiftCall3D db 69,58,32,194,219,199,194,192,210,220,32,203,200,212,210,0
roofLiftUp3D db 49,58,32,199,192,202,192,210,32,47,32,50,58,32,205,206,215,220,32,47,32,69,58,32,197,213,192,210,220,0
roofLeanPrompt3D db 69,58,32,206,193,203,206,202,206,210,200,210,220,209,223,0
roofLeanLeave3D db 69,58,32,206,210,206,201,210,200,0
roofSunsetName3D db 199,192,202,192,210,0
roofNightName3D db 205,206,215,220,0
roofLiftDown3D db 69,58,32,194,32,195,203,192,194,205,219,201,32,199,192,203,0
roofLiftBusy3D db 203,200,212,210,32,207,206,196,205,200,204,192,197,210,209,223,0
roofLiftBusyDown3D db 203,200,212,210,32,209,207,211,209,202,192,197,210,209,223,0
roofSeatPrompt3D db 69,58,32,207,208,200,209,197,209,210,220,0
roofStandPrompt3D db 69,58,32,194,209,210,192,210,220,0
roofBarPrompt3D db 69,58,32,203,192,211,205,198,45,193,192,208,32,34,194,219,209,206,210,192,34,0
roofArrival3D db 202,208,219,216,192,32,47,32,210,197,208,208,192,209,192,32,34,194,219,209,206,210,192,34,0
.data?
roofIndicatorPixels3D dd 4096 dup(?)
roofCityMask3D db SCREEN_SIZE dup(?)
roofGlowMask3D db SCREEN_SIZE dup(?)
roofSlopeRay3D dd SCREEN_W dup(?)
roofAAHistory3D dd SCREEN_SIZE dup(?)
.code

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1122
src_graphics/rooftop_gl.inc Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,92 @@
; Windows' system OpenGL entry points. All scene logic and calls are native MASM.
includelib opengl32.lib
includelib msimg32.lib
glBegin PROTO STDCALL :DWORD
glEnd PROTO STDCALL
glVertex3f PROTO STDCALL :DWORD,:DWORD,:DWORD
glNormal3f PROTO STDCALL :DWORD,:DWORD,:DWORD
glTexCoord2f PROTO STDCALL :DWORD,:DWORD
glColor4f PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD
glEnable PROTO STDCALL :DWORD
glDisable PROTO STDCALL :DWORD
glClear PROTO STDCALL :DWORD
glClearColor PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD
glViewport PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD
glMatrixMode PROTO STDCALL :DWORD
glLoadIdentity PROTO STDCALL
glPushMatrix PROTO STDCALL
glPopMatrix PROTO STDCALL
glTranslatef PROTO STDCALL :DWORD,:DWORD,:DWORD
glRotatef PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD
glFrustum PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
glOrtho PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
glGenTextures PROTO STDCALL :DWORD,:DWORD
glDeleteTextures PROTO STDCALL :DWORD,:DWORD
glBindTexture PROTO STDCALL :DWORD,:DWORD
glTexParameteri PROTO STDCALL :DWORD,:DWORD,:DWORD
glTexImage2D PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
glGenLists PROTO STDCALL :DWORD
glNewList PROTO STDCALL :DWORD,:DWORD
glEndList PROTO STDCALL
glCallList PROTO STDCALL :DWORD
glDeleteLists PROTO STDCALL :DWORD,:DWORD
glLightfv PROTO STDCALL :DWORD,:DWORD,:DWORD
glLightModelfv PROTO STDCALL :DWORD,:DWORD
glMaterialfv PROTO STDCALL :DWORD,:DWORD,:DWORD
glColorMaterial PROTO STDCALL :DWORD,:DWORD
glFogfv PROTO STDCALL :DWORD,:DWORD
glFogf PROTO STDCALL :DWORD,:DWORD
glFogi PROTO STDCALL :DWORD,:DWORD
glHint PROTO STDCALL :DWORD,:DWORD
glShadeModel PROTO STDCALL :DWORD
glBlendFunc PROTO STDCALL :DWORD,:DWORD
glDepthMask PROTO STDCALL :DWORD
glDepthFunc PROTO STDCALL :DWORD
glPolygonOffset PROTO STDCALL :DWORD,:DWORD
glReadPixels PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
glReadBuffer PROTO STDCALL :DWORD
glDrawBuffer PROTO STDCALL :DWORD
glGetError PROTO STDCALL
glGetString PROTO STDCALL :DWORD
glGetIntegerv PROTO STDCALL :DWORD,:DWORD
glFlush PROTO STDCALL
glFinish PROTO STDCALL
glCullFace PROTO STDCALL :DWORD
glFrontFace PROTO STDCALL :DWORD
wglCreateContext PROTO STDCALL :DWORD
wglDeleteContext PROTO STDCALL :DWORD
wglMakeCurrent PROTO STDCALL :DWORD,:DWORD
wglGetProcAddress PROTO STDCALL :DWORD
IFNDEF RegisterClassA
RegisterClassA PROTO STDCALL :DWORD
ENDIF
IFNDEF ChoosePixelFormat
ChoosePixelFormat PROTO STDCALL :DWORD,:DWORD
ENDIF
IFNDEF SetPixelFormat
SetPixelFormat PROTO STDCALL :DWORD,:DWORD,:DWORD
ENDIF
IFNDEF CreateDIBSection
CreateDIBSection PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
ENDIF
IFNDEF CreateCompatibleDC
CreateCompatibleDC PROTO STDCALL :DWORD
ENDIF
IFNDEF DeleteDC
DeleteDC PROTO STDCALL :DWORD
ENDIF
IFNDEF SelectObject
SelectObject PROTO STDCALL :DWORD,:DWORD
ENDIF
IFNDEF DeleteObject
DeleteObject PROTO STDCALL :DWORD
ENDIF
IFNDEF AlphaBlend
AlphaBlend PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
ENDIF
IFNDEF GdiFlush
GdiFlush PROTO STDCALL
ENDIF
IFNDEF StretchBlt
StretchBlt PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
ENDIF

View file

@ -0,0 +1,186 @@
include src_graphics\rooftop_gl_api.inc
ROOF_HD_W equ 1280
ROOF_HD_H equ 960
ROOF_GL_TEXTURES equ 49
ROOF_GL_LIST_COUNT equ 10
ROOF_GL_WATER_LIST equ 9
InitRoofHD3D PROTO
FreeRoofHD3D PROTO
RenderRoofHD3D PROTO
PresentRoofHD3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
LoadRoofMesh3D PROTO :DWORD,:DWORD
FreeRoofMesh3D PROTO :DWORD
RoofGLProc3D PROTO :DWORD,:DWORD
UploadRoofGLTexture3D PROTO :DWORD,:DWORD,:DWORD,:DWORD
BuildRoofGLList3D PROTO :DWORD,:DWORD,:DWORD,:DWORD
DrawRoofGLSky3D PROTO
DrawRoofGLTraffic3D PROTO
DrawRoofGLWater3D PROTO
UploadRoofGLSky3D PROTO :DWORD,:DWORD
UpdateRoofGLFog3D PROTO
UpdateRoofQuality3D PROTO
ComposeRoofFrame3D PROTO
RenderRoofQualityToast3D PROTO
WriteRoofRendererInfo3D PROTO
WriteRoofLogString3D PROTO :DWORD,:DWORD
.data
roofHDReady3D dd 0
roofHDStage3D dd 0
roofHDWindow3D dd 0
roofHDDC3D dd 0
roofHDRC3D dd 0
roofHDModule3D dd 0
roofHDBuffer3D dd 0
roofHDFBO3D dd 0
roofHDColorTex3D dd 0
roofHDDepth3D dd 0
roofHDLists3D dd 0
roofGLFogHint3D dd 1102h
roofHDSkyTextures3D dd 2 dup(0)
roofHDDayTextures3D dd ROOF_GL_TEXTURES dup(0)
roofHDNightTextures3D dd ROOF_GL_TEXTURES dup(0)
roofHDEmissionTextures3D dd ROOF_GL_TEXTURES dup(0)
roofHDMeshScene3D dd 4 dup(0)
roofHDMeshCar3D dd 4 dup(0)
roofHDMeshBoat3D dd 4 dup(0)
roofHDScenePath3D db "assets\animations\rooftop\mesh-city-v1.bin",0
roofHDCarPath3D db "assets\animations\rooftop\mesh-car-v1.bin",0
roofHDBoatPath3D db "assets\animations\rooftop\mesh-boat-v1.bin",0
roofHDMeshPtrs3D dd OFFSET roofHDMeshScene3D,OFFSET roofHDMeshCar3D,OFFSET roofHDMeshBoat3D
roofHDClassName3D db "AssemblerCasinoRoofOffscreen",0
roofHDTitle3D db "Roof offscreen renderer",0
roofHDClass3D dd 20h,OFFSET DefWindowProcA,0,0,0,0,0,0,0,OFFSET roofHDClassName3D
; PIXELFORMATDESCRIPTOR: RGBA32, depth24, draw-to-window/OpenGL/double-buffer.
roofHDPixelFormat3D dw 40,1
dd 25h
db 0,32,0,0,0,0,0,0,8,0,0,0,0,0,0,24,8,0,0,0
dd 0,0,0
roofHDOverlayDC3D dd 0
roofHDOverlayBmp3D dd 0
roofHDOverlayOld3D dd 0
roofHDFinalDC3D dd 0
roofHDFinalBmp3D dd 0
roofHDFinalOld3D dd 0
roofHDFinalPixels3D dd 0
roofHDOverlayPixels3D dd 0
roofHDOverlayBMI3D dd 40,640,-480
dw 1,32
dd 0,640*480*4,0,0,0,0
roofHDBMI3D dd 40,ROOF_HD_W,ROOF_HD_H
dw 1,32
dd 0,ROOF_HD_W*ROOF_HD_H*4,0,0,0,0
roofGLGenFBO3D dd 0
roofGLBindFBO3D dd 0
roofGLAttachTexture3D dd 0
roofGLCheckFBO3D dd 0
roofGLDeleteFBO3D dd 0
roofGLGenRBO3D dd 0
roofGLBindRBO3D dd 0
roofGLStoreRBO3D dd 0
roofGLAttachRBO3D dd 0
roofGLDeleteRBO3D dd 0
roofGLMipmap3D dd 0
roofGLProcSlots3D dd OFFSET roofGLGenFBO3D,OFFSET roofGLBindFBO3D,OFFSET roofGLAttachTexture3D
dd OFFSET roofGLCheckFBO3D,OFFSET roofGLDeleteFBO3D,OFFSET roofGLGenRBO3D
dd OFFSET roofGLBindRBO3D,OFFSET roofGLStoreRBO3D,OFFSET roofGLAttachRBO3D
dd OFFSET roofGLDeleteRBO3D,OFFSET roofGLMipmap3D
roofGLProcNames3D dd OFFSET roofGLName0,OFFSET roofGLName1,OFFSET roofGLName2,OFFSET roofGLName3
dd OFFSET roofGLName4,OFFSET roofGLName5,OFFSET roofGLName6,OFFSET roofGLName7
dd OFFSET roofGLName8,OFFSET roofGLName9,OFFSET roofGLName10
roofGLProcExtNames3D dd OFFSET roofGLExt0,OFFSET roofGLExt1,OFFSET roofGLExt2,OFFSET roofGLExt3
dd OFFSET roofGLExt4,OFFSET roofGLExt5,OFFSET roofGLExt6,OFFSET roofGLExt7
dd OFFSET roofGLExt8,OFFSET roofGLExt9,OFFSET roofGLExt10
roofGLName0 db "glGenFramebuffers",0
roofGLName1 db "glBindFramebuffer",0
roofGLName2 db "glFramebufferTexture2D",0
roofGLName3 db "glCheckFramebufferStatus",0
roofGLName4 db "glDeleteFramebuffers",0
roofGLName5 db "glGenRenderbuffers",0
roofGLName6 db "glBindRenderbuffer",0
roofGLName7 db "glRenderbufferStorage",0
roofGLName8 db "glFramebufferRenderbuffer",0
roofGLName9 db "glDeleteRenderbuffers",0
roofGLName10 db "glGenerateMipmap",0
roofGLExt0 db "glGenFramebuffersEXT",0
roofGLExt1 db "glBindFramebufferEXT",0
roofGLExt2 db "glFramebufferTexture2DEXT",0
roofGLExt3 db "glCheckFramebufferStatusEXT",0
roofGLExt4 db "glDeleteFramebuffersEXT",0
roofGLExt5 db "glGenRenderbuffersEXT",0
roofGLExt6 db "glBindRenderbufferEXT",0
roofGLExt7 db "glRenderbufferStorageEXT",0
roofGLExt8 db "glFramebufferRenderbufferEXT",0
roofGLExt9 db "glDeleteRenderbuffersEXT",0
roofGLExt10 db "glGenerateMipmapEXT",0
roofGLFogDay3D REAL4 0.67,0.61,0.69,1.0
roofGLFogNight3D REAL4 0.05,0.09,0.17,1.0
roofGLFogCurrent3D REAL4 0.67,0.61,0.69,1.0
roofGLFogWhite3D REAL4 1.0,1.0,1.0,1.0
roofGLFogBlack3D REAL4 0.0,0.0,0.0,1.0
roofGLColorScale3D REAL4 255.0
roofGLWaveDiv3D REAL4 100000.0
roofGLHalf3D REAL4 0.5
roofGLAmbientDay3D REAL4 0.43,0.43,0.48,1.0
roofGLAmbientNight3D REAL4 0.20,0.23,0.32,1.0
roofGLSunColorDay3D REAL4 0.75,0.57,0.39,1.0
roofGLSunColorNight3D REAL4 0.12,0.15,0.25,1.0
roofGLSunDirection3D REAL4 -0.8,0.8,0.4,0.0
roofGLEmissionZero3D REAL4 0.0,0.0,0.0,1.0
roofGLEmissionGold3D REAL4 0.42,0.28,0.08,1.0
roofGLEmissionLamp3D REAL4 0.9,0.67,0.3,1.0
roofGLInvFP3D REAL4 0.0000152587890625
roofGLDegrees3D REAL4 0.703125
roofGLNinety3D REAL4 90.0
roofGLHalfWidth3D REAL8 0.66
roofGLLeft3D REAL8 -0.66
roofGLRight3D REAL8 0.66
roofGLNear3D REAL8 1.0
roofGLFar3D REAL8 2048.0
roofGLZeroD3D REAL8 0.0
roofGLOneD3D REAL8 1.0
roofGLMinusOneD3D REAL8 -1.0
roofGLScreenWidthD3D REAL8 1280.0
roofGLScreenHeightD3D REAL8 960.0
roofGLCameraH3D dd 480
roofHDRenderX3D dd -1
roofHDRenderY3D dd -1
roofHDRenderAngle3D dd -1
roofHDRenderEye3D dd -1
roofHDRenderHorizon3D dd -1
roofHDRenderTheme3D dd -1
roofHDRenderClock3D dd -1
roofRenderW3D dd 960
roofRenderH3D dd 720
roofQualityMode3D dd 0
roofAutoWidth3D dd 960
roofQualityMapW3D dd 0
roofQualityClock3D dd -1
roofQualitySlow3D dd 0
roofQualityToast3D dd 0
roofQualityAutoText3D db 202,208,219,216,192,58,32,192,194,210,206,0
roofQualityEcoText3D db 202,208,219,216,192,58,32,221,202,206,0
roofQualityHighText3D db 202,208,219,216,192,58,32,196,197,210,192,203,200,0
roofLogPath3D db "runtime\roof-renderer.txt",0
roofLogHeader3D db "Native MASM rooftop renderer",13,10,0
roofLogTerrain3D db "Terrain path: tiled ground and animated mesh water v2",13,10,0
roofLogTriangles3D db "City mesh triangles: ",0
roofLogVendor3D db "Vendor: ",0
roofLogRenderer3D db "Renderer: ",0
roofLogVersion3D db "OpenGL: ",0
roofLogStage3D db "Init stage (0=ready): ",0
roofLogAssets3D db "City texture pack present: ",0
roofLogWidth3D db "Render width: ",0
roofLogSmall3D db "640",0
roofLogMedium3D db "960",0
roofLogLarge3D db "1280",0
roofLogUnknown3D db "unavailable",0
roofLogNL3D db 13,10,0
roofLogDigit3D db "0",0
.data?
roofHDBackdrop3D dd SCREEN_SIZE dup(?)
roofHDUsedMaterials3D db ROOF_GL_TEXTURES dup(?)
roofQualityX3D dd SCREEN_W dup(?)
roofQualityXCount3D dd SCREEN_W dup(?)
roofQualityY3D dd SCREEN_H dup(?)
roofQualityYCount3D dd SCREEN_H dup(?)
.code

View file

@ -0,0 +1,74 @@
; Generated by tools/generators/pack_rooftop.py.
.data
roofMap3D db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
roofSkyColumns3D dd -190,-190,-189,-189,-188,-188,-187,-187,-186,-186,-185,-185,-184,-184,-184,-183
dd -183,-182,-182,-181,-181,-180,-180,-179,-179,-178,-178,-177,-177,-176,-176,-175
dd -175,-174,-174,-173,-173,-172,-172,-171,-171,-170,-170,-169,-169,-168,-168,-167
dd -167,-166,-166,-165,-165,-164,-164,-163,-163,-162,-161,-161,-160,-160,-159,-159
dd -158,-158,-157,-157,-156,-156,-155,-155,-154,-154,-153,-153,-152,-151,-151,-150
dd -150,-149,-149,-148,-148,-147,-147,-146,-145,-145,-144,-144,-143,-143,-142,-142
dd -141,-141,-140,-139,-139,-138,-138,-137,-137,-136,-135,-135,-134,-134,-133,-133
dd -132,-132,-131,-130,-130,-129,-129,-128,-128,-127,-126,-126,-125,-125,-124,-123
dd -123,-122,-122,-121,-121,-120,-119,-119,-118,-118,-117,-116,-116,-115,-115,-114
dd -113,-113,-112,-112,-111,-111,-110,-109,-109,-108,-108,-107,-106,-106,-105,-105
dd -104,-103,-103,-102,-101,-101,-100,-100,-99,-98,-98,-97,-97,-96,-95,-95
dd -94,-93,-93,-92,-92,-91,-90,-90,-89,-89,-88,-87,-87,-86,-85,-85
dd -84,-84,-83,-82,-82,-81,-80,-80,-79,-78,-78,-77,-77,-76,-75,-75
dd -74,-73,-73,-72,-71,-71,-70,-70,-69,-68,-68,-67,-66,-66,-65,-64
dd -64,-63,-62,-62,-61,-60,-60,-59,-59,-58,-57,-57,-56,-55,-55,-54
dd -53,-53,-52,-51,-51,-50,-49,-49,-48,-47,-47,-46,-45,-45,-44,-43
dd -43,-42,-41,-41,-40,-39,-39,-38,-37,-37,-36,-35,-35,-34,-33,-33
dd -32,-31,-31,-30,-29,-29,-28,-27,-27,-26,-25,-25,-24,-23,-23,-22
dd -21,-21,-20,-19,-19,-18,-17,-17,-16,-15,-15,-14,-13,-13,-12,-11
dd -11,-10,-9,-9,-8,-7,-7,-6,-5,-5,-4,-3,-3,-2,-1,-1
dd 0,1,1,2,3,3,4,5,5,6,7,7,8,9,9,10
dd 11,11,12,13,13,14,15,15,16,17,17,18,19,19,20,21
dd 21,22,23,23,24,25,25,26,27,27,28,29,29,30,31,31
dd 32,33,33,34,35,35,36,37,37,38,39,39,40,41,41,42
dd 43,43,44,45,45,46,47,47,48,49,49,50,51,51,52,53
dd 53,54,55,55,56,57,57,58,59,59,60,60,61,62,62,63
dd 64,64,65,66,66,67,68,68,69,70,70,71,71,72,73,73
dd 74,75,75,76,77,77,78,78,79,80,80,81,82,82,83,84
dd 84,85,85,86,87,87,88,89,89,90,90,91,92,92,93,93
dd 94,95,95,96,97,97,98,98,99,100,100,101,101,102,103,103
dd 104,105,105,106,106,107,108,108,109,109,110,111,111,112,112,113
dd 113,114,115,115,116,116,117,118,118,119,119,120,121,121,122,122
dd 123,123,124,125,125,126,126,127,128,128,129,129,130,130,131,132
dd 132,133,133,134,134,135,135,136,137,137,138,138,139,139,140,141
dd 141,142,142,143,143,144,144,145,145,146,147,147,148,148,149,149
dd 150,150,151,151,152,153,153,154,154,155,155,156,156,157,157,158
dd 158,159,159,160,160,161,161,162,163,163,164,164,165,165,166,166
dd 167,167,168,168,169,169,170,170,171,171,172,172,173,173,174,174
dd 175,175,176,176,177,177,178,178,179,179,180,180,181,181,182,182
dd 183,183,184,184,184,185,185,186,186,187,187,188,188,189,189,190

View file

@ -0,0 +1,280 @@
; Quality changes only roof rendering. Simulation, collisions and other games stay native.
UpdateRoofQuality3D PROC uses ebx esi edi
cmp hallLevel3D,HALL_ROOF
jne roof_quality_apply
cmp engineMode,MODE_WALK
jne roof_quality_apply
mov eax,animationClock3D
cmp eax,roofQualityClock3D
je roof_quality_apply
mov roofQualityClock3D,eax
invoke Pressed3D,77h
test eax,eax
jz roof_quality_auto
inc roofQualityMode3D
cmp roofQualityMode3D,3
jb @F
mov roofQualityMode3D,0
mov roofAutoWidth3D,960
@@:
mov roofQualityToast3D,1800
mov roofQualitySlow3D,0
roof_quality_auto:
mov eax,deltaTime3D
cmp eax,100
jbe @F
mov eax,100
@@:
sub roofQualityToast3D,eax
jns @F
mov roofQualityToast3D,0
@@:
cmp roofQualityMode3D,0
jne roof_quality_apply
cmp roofAutoWidth3D,640
je roof_quality_apply
cmp deltaTime3D,34
jbe roof_quality_cool
add roofQualitySlow3D,eax
cmp roofQualitySlow3D,1000
jb roof_quality_apply
mov roofAutoWidth3D,640
mov roofQualitySlow3D,0
mov roofQualityToast3D,1800
jmp roof_quality_apply
roof_quality_cool:
sub roofQualitySlow3D,8
jns roof_quality_apply
mov roofQualitySlow3D,0
roof_quality_apply:
mov eax,roofAutoWidth3D
cmp roofQualityMode3D,0
je roof_quality_size
mov eax,640
cmp roofQualityMode3D,1
je roof_quality_size
mov eax,1280
roof_quality_size:
mov roofRenderW3D,eax
imul eax,3
shr eax,2
mov roofRenderH3D,eax
mov eax,roofRenderW3D
cmp eax,roofQualityMapW3D
je roof_quality_done
mov roofQualityMapW3D,eax
mov roofHDRenderClock3D,-1
xor esi,esi
roof_quality_x:
mov eax,esi
imul eax,roofRenderW3D
xor edx,edx
mov ecx,SCREEN_W
div ecx
mov ebx,eax
shl eax,2
mov roofQualityX3D[esi*4],eax
mov eax,esi
inc eax
imul eax,roofRenderW3D
xor edx,edx
div ecx
sub eax,ebx
mov roofQualityXCount3D[esi*4],eax
inc esi
cmp esi,SCREEN_W
jb roof_quality_x
xor esi,esi
roof_quality_y:
mov eax,esi
imul eax,roofRenderH3D
xor edx,edx
mov ecx,SCREEN_H
div ecx
mov ebx,eax
mov edi,ROOF_HD_H-1
sub edi,eax
imul edi,ROOF_HD_W*4
mov roofQualityY3D[esi*4],edi
mov eax,esi
inc eax
imul eax,roofRenderH3D
xor edx,edx
div ecx
sub eax,ebx
mov roofQualityYCount3D[esi*4],eax
inc esi
cmp esi,SCREEN_H
jb roof_quality_y
cmp roofHDReady3D,1
jne roof_quality_done
call WriteRoofRendererInfo3D
roof_quality_done:
ret
UpdateRoofQuality3D ENDP
ComposeRoofFrame3D PROC uses ebx esi edi
local rows:DWORD,y:DWORD,x:DWORD,targetRow:DWORD,repeatRow:DWORD
invoke GdiFlush
mov esi,roofHDBuffer3D
mov eax,ROOF_HD_H
sub eax,roofRenderH3D
imul eax,ROOF_HD_W*4
add eax,roofHDFinalPixels3D
mov edi,eax
mov eax,roofRenderH3D
mov rows,eax
roof_compose_background:
mov ecx,roofRenderW3D
rep movsd
mov eax,ROOF_HD_W
sub eax,roofRenderW3D
lea edi,[edi+eax*4]
dec rows
jnz roof_compose_background
xor esi,esi
mov y,0
roof_compose_row:
mov ebx,y
mov eax,roofQualityY3D[ebx*4]
add eax,roofHDFinalPixels3D
mov targetRow,eax
mov eax,roofQualityYCount3D[ebx*4]
mov repeatRow,eax
mov x,0
roof_compose_pixel:
mov eax,screenBuffer[esi*4]
cmp roofCityMask3D[esi],0
je roof_compose_opaque
cmp eax,roofHDBackdrop3D[esi*4]
je roof_compose_next
roof_compose_opaque:
mov ebx,x
mov edi,roofQualityX3D[ebx*4]
add edi,targetRow
mov [edi],eax
cmp roofQualityXCount3D[ebx*4],2
jne @F
mov [edi+4],eax
@@:
cmp repeatRow,2
jne roof_compose_next
mov [edi-ROOF_HD_W*4],eax
cmp roofQualityXCount3D[ebx*4],2
jne roof_compose_next
mov [edi-ROOF_HD_W*4+4],eax
roof_compose_next:
inc esi
inc x
cmp x,SCREEN_W
jb roof_compose_pixel
inc y
cmp y,SCREEN_H
jb roof_compose_row
ret
ComposeRoofFrame3D ENDP
RenderRoofQualityToast3D PROC
cmp roofQualityToast3D,0
je roof_quality_toast_done
mov eax,OFFSET roofQualityAutoText3D
cmp roofRenderW3D,640
jne @F
mov eax,OFFSET roofQualityEcoText3D
@@:
cmp roofQualityMode3D,2
jne @F
mov eax,OFFSET roofQualityHighText3D
@@:
push eax
invoke DrawBox,12,40,160,18,00151B1Eh
pop eax
invoke DrawString8x8,20,45,eax,00E8D7ADh
roof_quality_toast_done:
ret
RenderRoofQualityToast3D ENDP
WriteRoofLogString3D PROC uses ebx esi handle:DWORD,value:DWORD
local written:DWORD
mov ebx,handle
mov esi,value
test esi,esi
jnz @F
mov esi,OFFSET roofLogUnknown3D
@@:
xor ecx,ecx
roof_log_length:
cmp BYTE PTR [esi+ecx],0
je roof_log_write
inc ecx
jmp roof_log_length
roof_log_write:
invoke WriteFile,ebx,esi,ecx,ADDR written,0
ret
WriteRoofLogString3D ENDP
WriteRoofRendererInfo3D PROC uses ebx esi
local number[16]:BYTE
invoke CreateDirectoryA,OFFSET runtime3D,0
invoke CreateFileA,OFFSET roofLogPath3D,40000000h,1,0,2,80h,0
cmp eax,-1
je roof_log_done
mov ebx,eax
invoke WriteRoofLogString3D,ebx,OFFSET roofLogHeader3D
invoke WriteRoofLogString3D,ebx,OFFSET roofLogTerrain3D
invoke WriteRoofLogString3D,ebx,OFFSET roofLogTriangles3D
invoke FormatNumber,roofHDMeshScene3D[4],ADDR number
invoke WriteRoofLogString3D,ebx,ADDR number
invoke WriteRoofLogString3D,ebx,OFFSET roofLogNL3D
mov esi,0
roof_log_gl_string:
mov eax,OFFSET roofLogVendor3D
cmp esi,0
je @F
mov eax,OFFSET roofLogRenderer3D
cmp esi,1
je @F
mov eax,OFFSET roofLogVersion3D
@@:
invoke WriteRoofLogString3D,ebx,eax
xor eax,eax
cmp roofHDRC3D,0
je @F
mov eax,esi
add eax,1F00h
invoke glGetString,eax
@@:
invoke WriteRoofLogString3D,ebx,eax
invoke WriteRoofLogString3D,ebx,OFFSET roofLogNL3D
inc esi
cmp esi,3
jb roof_log_gl_string
invoke WriteRoofLogString3D,ebx,OFFSET roofLogStage3D
mov eax,roofHDStage3D
add al,'0'
mov roofLogDigit3D,al
invoke WriteRoofLogString3D,ebx,OFFSET roofLogDigit3D
invoke WriteRoofLogString3D,ebx,OFFSET roofLogNL3D
invoke WriteRoofLogString3D,ebx,OFFSET roofLogAssets3D
mov roofLogDigit3D,'0'
cmp roofCityArt3D.pixels,0
je @F
mov roofLogDigit3D,'1'
@@:
invoke WriteRoofLogString3D,ebx,OFFSET roofLogDigit3D
invoke WriteRoofLogString3D,ebx,OFFSET roofLogNL3D
invoke WriteRoofLogString3D,ebx,OFFSET roofLogWidth3D
mov eax,OFFSET roofLogMedium3D
cmp roofRenderW3D,960
je @F
mov eax,OFFSET roofLogSmall3D
cmp roofRenderW3D,640
je @F
mov eax,OFFSET roofLogLarge3D
@@:
invoke WriteRoofLogString3D,ebx,eax
invoke WriteRoofLogString3D,ebx,OFFSET roofLogNL3D
invoke CloseHandle,ebx
roof_log_done:
ret
WriteRoofRendererInfo3D ENDP

View file

@ -0,0 +1,317 @@
; Ray/plane intersection for real pitched roofs, including perspective depth.
DrawRoofSlope3D PROC uses ebx esi edi axis:DWORD,x0:DWORD,x1:DWORD,y0:DWORD,y1:DWORD,baseZ:DWORD,rise:DWORD,material:DWORD
local slope:DWORD,numerator:DWORD,leftPx:DWORD,rightPx:DWORD,topPx:DWORD,bottomPx:DWORD
local corner:DWORD,wx:DWORD,wy:DWORD,wz:DWORD,visible:DWORD,nearClip:DWORD
local row:DWORD,column:DWORD,rayZ:DWORD,depth:DWORD,along:DWORD,denominator:DWORD
local texture:DWORD,textureW:DWORD,textureH:DWORD,light:DWORD,sourceU:DWORD
invoke ResolveRoofSurface3D,material
test eax,eax
jz roof_slope_done
mov texture,eax
mov textureW,ecx
mov textureH,edx
mov eax,rise
cdq
shld edx,eax,16
shl eax,16
mov ecx,x1
sub ecx,x0
cmp axis,0
je @F
mov ecx,y1
sub ecx,y0
@@:
idiv ecx
mov slope,eax
mov light,242
test eax,eax
jns @F
mov light,184
@@:
mov eax,playerX
sub eax,x0
cmp axis,0
je @F
mov eax,playerY
sub eax,y0
@@:
imul slope
shrd eax,edx,16
neg eax
add eax,viewEye3D
sub eax,baseZ
mov numerator,eax
mov leftPx,SCREEN_W
mov rightPx,0
mov topPx,SCREEN_H
mov bottomPx,0
mov visible,0
mov nearClip,0
mov corner,0
roof_slope_corner:
mov eax,x0
test corner,1
jz @F
mov eax,x1
@@:
mov wx,eax
mov ebx,y0
test corner,2
jz @F
mov ebx,y1
@@:
mov wy,ebx
mov ecx,baseZ
mov eax,corner
cmp axis,0
jne @F
and eax,1
jmp roof_slope_corner_height
@@:
and eax,2
roof_slope_corner_height:
test eax,eax
jz @F
add ecx,rise
@@:
mov wz,ecx
invoke ProjectCityPoint3D,wx,wy
cmp edx,4096
jg roof_slope_projected_corner
mov nearClip,1
jmp roof_slope_corner_next
roof_slope_projected_corner:
inc visible
mov esi,edx
cmp eax,leftPx
jge @F
mov leftPx,eax
@@:
cmp eax,rightPx
jle @F
mov rightPx,eax
@@:
mov eax,viewEye3D
sub eax,wz
mov ecx,SCREEN_H
imul ecx
idiv esi
add eax,viewHorizon3D
cmp eax,topPx
jge @F
mov topPx,eax
@@:
cmp eax,bottomPx
jle @F
mov bottomPx,eax
@@:
roof_slope_corner_next:
inc corner
cmp corner,4
jb roof_slope_corner
cmp visible,0
je roof_slope_done
cmp nearClip,0
je @F
mov leftPx,0
mov rightPx,SCREEN_W-1
mov topPx,0
mov bottomPx,SCREEN_H-1
@@:
cmp leftPx,0
jge @F
mov leftPx,0
@@:
cmp rightPx,SCREEN_W-1
jle @F
mov rightPx,SCREEN_W-1
@@:
cmp topPx,0
jge @F
mov topPx,0
@@:
cmp bottomPx,SCREEN_H-1
jle @F
mov bottomPx,SCREEN_H-1
@@:
mov eax,leftPx
cmp eax,rightPx
jge roof_slope_done
mov eax,topPx
cmp eax,bottomPx
jge roof_slope_done
mov row,eax
mov ebx,leftPx
roof_slope_prepare:
mov eax,cityRayX3D[ebx*4]
cmp axis,0
je @F
mov eax,cityRayY3D[ebx*4]
@@:
imul slope
shrd eax,edx,16
mov roofSlopeRay3D[ebx*4],eax
inc ebx
cmp ebx,rightPx
jbe roof_slope_prepare
roof_slope_row:
mov eax,viewHorizon3D
sub eax,row
shl eax,16
cdq
mov ecx,SCREEN_H
idiv ecx
mov rayZ,eax
mov eax,row
imul eax,SCREEN_W*4
mov ebx,leftPx
lea edi,[eax+ebx*4]
mov column,ebx
roof_slope_pixel:
mov ebx,column
mov ecx,roofSlopeRay3D[ebx*4]
sub ecx,rayZ
mov denominator,ecx
mov eax,ecx
cdq
xor eax,edx
sub eax,edx
cmp eax,64
jb roof_slope_advance
shl eax,10
mov along,eax
mov eax,numerator
cdq
xor eax,edx
sub eax,edx
cmp eax,along
jae roof_slope_advance
mov eax,numerator
cdq
shld edx,eax,16
shl eax,16
idiv denominator
cmp eax,4096
jle roof_slope_advance
cmp eax,cityDepthBuffer3D[edi]
jae roof_slope_advance
mov depth,eax
imul cityRayX3D[ebx*4]
shrd eax,edx,16
add eax,playerX
cmp eax,x0
jl roof_slope_advance
cmp eax,x1
jge roof_slope_advance
mov wx,eax
sub eax,x0
sar eax,9
mov ecx,textureW
dec ecx
and eax,ecx
mov sourceU,eax
mov eax,depth
imul cityRayY3D[ebx*4]
shrd eax,edx,16
add eax,playerY
cmp eax,y0
jl roof_slope_advance
cmp eax,y1
jge roof_slope_advance
mov wy,eax
cmp roofSlopeMask3D,0
je roof_slope_sample
mov eax,wx
sub eax,roofSlopeCentreX3D
cdq
xor eax,edx
sub eax,edx
imul roofSlopeHalfY3D
shrd eax,edx,16
mov along,eax
mov eax,wy
sub eax,roofSlopeCentreY3D
cdq
xor eax,edx
sub eax,edx
imul roofSlopeHalfX3D
shrd eax,edx,16
cmp roofSlopeMask3D,1
jne @F
cmp eax,along
ja roof_slope_advance
jmp roof_slope_sample
@@:
cmp eax,along
jb roof_slope_advance
roof_slope_sample:
mov eax,wy
sub eax,y0
sar eax,9
mov ecx,textureH
dec ecx
and eax,ecx
imul eax,textureW
add eax,sourceU
mov esi,texture
mov eax,[esi+eax*4]
invoke ShadeRoofCityPixel3D,eax,depth,light,material
mov ecx,edi
shr ecx,2
mov edx,roofPixelGlow3D
mov roofGlowMask3D[ecx],dl
mov screenBuffer[edi],eax
mov eax,depth
mov cityDepthBuffer3D[edi],eax
mov eax,edi
shr eax,2
mov roofCityMask3D[eax],1
roof_slope_advance:
add edi,4
inc column
mov eax,column
cmp eax,rightPx
jbe roof_slope_pixel
inc row
mov eax,row
cmp eax,bottomPx
jbe roof_slope_row
roof_slope_done:
ret
DrawRoofSlope3D ENDP
DrawRoofPyramid3D PROC uses ebx esi centreX:DWORD,centreY:DWORD,baseZ:DWORD,halfX:DWORD,halfY:DWORD,height:DWORD,mat:DWORD
local x0:DWORD,x1:DWORD,y0:DWORD,y1:DWORD,topZ:DWORD,down:DWORD
mov eax,centreX
mov roofSlopeCentreX3D,eax
sub eax,halfX
mov x0,eax
mov eax,centreX
add eax,halfX
mov x1,eax
mov eax,centreY
mov roofSlopeCentreY3D,eax
sub eax,halfY
mov y0,eax
mov eax,centreY
add eax,halfY
mov y1,eax
mov eax,halfX
mov roofSlopeHalfX3D,eax
mov eax,halfY
mov roofSlopeHalfY3D,eax
mov eax,baseZ
add eax,height
mov topZ,eax
mov eax,height
neg eax
mov down,eax
mov roofSlopeMask3D,1
invoke DrawRoofSlope3D,0,x0,centreX,y0,y1,baseZ,height,mat
invoke DrawRoofSlope3D,0,centreX,x1,y0,y1,topZ,down,mat
mov roofSlopeMask3D,2
invoke DrawRoofSlope3D,1,x0,x1,y0,centreY,baseZ,height,mat
invoke DrawRoofSlope3D,1,x0,x1,centreY,y1,topZ,down,mat
mov roofSlopeMask3D,0
ret
DrawRoofPyramid3D ENDP

View file

@ -1,5 +1,6 @@
"""Exact clock-board geometry plus fixed-grid full-arm animation packing."""
from pathlib import Path
import argparse
import math
import random
import struct
@ -19,58 +20,76 @@ def pack(name, frames):
def board():
rng = random.Random(301)
image = Image.new("RGBA", (512, 512))
scale = 2
center = 256*scale
image = Image.new("RGBA", (512*scale, 512*scale))
pixels = image.load()
for y in range(512):
for x in range(512):
dx, dy = x-256, 256-y
for y in range(image.height):
for x in range(image.width):
dx, dy = (x-center)/scale, (center-y)/scale
radius = math.hypot(dx, dy)
angle = math.atan2(dx, dy)
sector = int(((angle + math.pi/20) % math.tau) / (math.tau/20))
red = sector % 2 == 0
color = (30, 36, 39)
if radius < 170:
color = (35, 38, 37) if red else (214, 206, 178)
color = (23, 29, 31) if red else (232, 225, 201)
if 99 <= radius <= 107 or 162 <= radius:
color = (174, 37, 47) if red else (28, 112, 87)
color = (191, 42, 55) if red else (24, 145, 103)
if radius <= 15.9:
color = (28, 112, 87)
color = (24, 145, 103)
if radius <= 6.35:
color = (187, 37, 47)
elif radius < 210:
color = (20, 24, 25)
color = (191, 42, 55)
elif radius < 220:
color = (10, 14, 17)
elif radius < 240:
color = (72, 29, 35)
elif radius < 248:
color = (31, 37, 41)
else:
continue
grain = rng.randrange(-11, 12) if radius < 170 else rng.randrange(-3, 4)
grain = rng.randrange(-2, 3)
pixels[x, y] = tuple(max(0, min(255, c+grain)) for c in color) + (255,)
draw = ImageDraw.Draw(image)
wire = (171, 178, 173, 255)
wire = (139, 158, 161, 255)
for r in (6.35, 15.9, 99, 107, 162, 170):
draw.ellipse((256-r, 256-r, 256+r, 256+r), outline=wire, width=1)
bounds = tuple(round(v*scale) for v in (256-r, 256-r, 256+r, 256+r))
draw.ellipse(bounds, outline=wire, width=2*scale)
for index in range(20):
theta = (index*18-9)*math.pi/180
draw.line([(256+math.sin(theta)*16, 256-math.cos(theta)*16),
(256+math.sin(theta)*170, 256-math.cos(theta)*170)], fill=wire)
font = ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf", 23)
ends = [(round((256+math.sin(theta)*r)*scale),
round((256-math.cos(theta)*r)*scale)) for r in (16,170)]
draw.line(ends, fill=wire, width=2*scale)
font = ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf", 32*scale)
labels = []
for index, value in enumerate(SECTORS):
theta = index*math.tau/20
draw.text((256+math.sin(theta)*190, 256-math.cos(theta)*190), str(value),
font=font, fill=(227, 230, 220), anchor="mm", stroke_width=1)
draw.ellipse((12, 12, 500, 500), outline=(104, 114, 120), width=3)
image.save(ART / "board-v1.png")
point = ((256+math.sin(theta)*198)*scale, (256-math.cos(theta)*198)*scale)
labels.append(draw.textbbox(point, str(value), font=font, anchor="mm"))
draw.text(point, str(value),
font=font, fill=(249, 247, 232), anchor="mm")
for i, a in enumerate(labels):
for b in labels[i+1:]:
assert a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1], "Overlapping board numbers"
draw.ellipse((12*scale, 12*scale, 500*scale, 500*scale),
outline=(104, 114, 120), width=3*scale)
image = image.resize((512,512), Image.Resampling.LANCZOS)
image.save(ART / "board-v2.png")
return image
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--board-only", action="store_true")
args = parser.parse_args()
ART.mkdir(parents=True, exist_ok=True)
OUT.mkdir(parents=True, exist_ok=True)
pack("board-v1.bin", [board()])
pack("board-v2.bin", [board()])
if args.board_only:
print("Packed sharper 512px board: unchanged footprint and scoring radii.")
return
wall = Image.open(ART / "lounge-wall-v1.png").convert("RGBA")
pack("lounge-wall-v1.bin", [wall.resize((512,512), Image.Resampling.LANCZOS)])
anchors = []

View file

@ -0,0 +1,102 @@
"""Offline orb tracking metadata and antialiased numeral sprites; no runtime Python."""
from pathlib import Path
import json
import struct
import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "assets/animations/fortune"
ART = ROOT / "assets/sprites/fortune"
PREVIEW = ROOT / "build/hall-expansion"
def load_orbs():
raw = (OUT / "orb-mist-v1.bin").read_bytes()
magic, width, height, count, length = struct.unpack_from("<4s4I", raw)
assert magic == b"ANM1" and count == 16
assert length == width * height * count * 4 == len(raw) - 20
stride = width * height * 4
return [Image.frombytes("RGBA", (width, height), raw[20+i*stride:20+(i+1)*stride],
"raw", "BGRA") for i in range(count)]
def sphere_geometry(frame):
# Fit the upper glass silhouette, explicitly excluding the metal pedestal.
alpha = np.asarray(frame.getchannel("A"))
rows, midpoints, half_widths = [], [], []
for y in range(12, round(frame.height * .70)):
xs = np.flatnonzero(alpha[y] >= 170)
if len(xs) < frame.width * .35:
continue
left, right = int(xs[0]), int(xs[-1])
rows.append(y)
midpoints.append((left + right) / 2)
half_widths.append((right - left) / 2)
a, b, c = np.polyfit(rows, np.square(half_widths), 2)
assert a < 0
cy = -b / (2*a)
cx = float(np.median(midpoints))
rx = float(np.sqrt(c - b*b/(4*a)))
ry = float(rx / np.sqrt(-a))
assert 70 < cx < 125 and 60 < cy < 115
assert 55 < rx < 95 and 55 < ry < 95
return {"cx": cx, "cy": cy, "rx": rx, "ry": ry}
def numeral(digit, font):
factor = 4
mask = Image.new("L", (32*factor, 48*factor))
draw = ImageDraw.Draw(mask)
box = draw.textbbox((0, 0), str(digit), font=font)
x = (mask.width-(box[2]-box[0]))/2-box[0]
y = (mask.height-(box[3]-box[1]))/2-box[1]
draw.text((round(x), round(y)), str(digit), font=font, fill=255)
mask = mask.resize((32, 48), Image.Resampling.LANCZOS)
shadow = Image.new("RGBA", mask.size, (8, 30, 66, 0))
shadow.putalpha(mask.filter(ImageFilter.MaxFilter(3)).point(lambda a: round(a*.65)))
halo = Image.new("RGBA", mask.size, (65, 168, 243, 0))
halo.putalpha(mask.filter(ImageFilter.GaussianBlur(2.0)).point(lambda a: round(a*.65)))
ink = Image.new("RGBA", mask.size, (200, 235, 255, 0))
ink.putalpha(mask.point(lambda a: round(a*.90)))
return Image.alpha_composite(Image.alpha_composite(halo, shadow), ink)
def main():
PREVIEW.mkdir(parents=True, exist_ok=True)
orbs = load_orbs()
geometry = [sphere_geometry(frame) for frame in orbs]
# Q8 offsets in the exact 128px runtime drawing rectangle, not its centre.
lines = ["; Generated by tools/generators/pack_fortune_orb.py; glass only, pedestal excluded."]
for name, key in (("fortuneOrbAnchorX3D", "cx"), ("fortuneOrbAnchorY3D", "cy")):
values = [round(item[key] * 128 / 192 * 256) for item in geometry]
lines.append(name + " dd " + ",".join(map(str, values)))
(ROOT / "src_graphics/fortune_orb_art.inc").write_text("\n".join(lines)+"\n", encoding="ascii")
font = ImageFont.truetype("C:/Windows/Fonts/cambria.ttc", 34*4)
digits = [numeral(i, font) for i in range(10)]
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in digits)
(OUT / "orb-numerals-v1.bin").write_bytes(struct.pack("<4s4I", b"ANM1", 32, 48, 10,
len(payload)) + payload)
sheet = Image.new("RGBA", (320, 48))
for i, digit in enumerate(digits):
sheet.alpha_composite(digit, (32*i, 0))
sheet.save(ART / "orb-numerals-v1.png")
(ART / "orb-tracking-v1.json").write_text(json.dumps({"canvas": [192, 192],
"font": "Cambria Regular, rasterized at 4x; no font needed at runtime",
"frames": geometry}, indent=2)+"\n", encoding="ascii")
sheet = Image.new("RGB", (192*4, 192*4), (5, 15, 28))
for i, (frame, item) in enumerate(zip(orbs, geometry)):
debug = frame.copy()
draw = ImageDraw.Draw(debug)
x, y = item["cx"], item["cy"]
draw.ellipse((x-item["rx"], y-item["ry"], x+item["rx"], y+item["ry"]), outline="yellow")
draw.line((x-5, y, x+5, y), fill="yellow")
draw.line((x, y-5, x, y+5), fill="yellow")
sheet.paste(debug, ((i%4)*192, (i//4)*192), debug)
sheet.save(PREVIEW / "fortune-orb-tracking.png")
print("Packed 10 antialiased Cambria numerals and 16 measured glass-centre anchors.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,419 @@
"""Offline rooftop atlas/map compiler. Game rendering and travel remain MASM."""
from pathlib import Path
import json
import math
import struct
import numpy as np
import cv2
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
ART = ROOT / "assets/sprites/rooftop"
OUT = ROOT / "assets/animations/rooftop"
def grid(path, columns, rows):
source = Image.open(ART/path).convert("RGBA")
return [source.crop((round(x*source.width/columns), round(y*source.height/rows),
round((x+1)*source.width/columns), round((y+1)*source.height/rows)))
.resize((256, 256), Image.Resampling.LANCZOS)
for y in range(rows) for x in range(columns)]
def pack(name, frames):
w, h = frames[0].size
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
(OUT/name).write_bytes(struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(payload))+payload)
def clean_hands(frame):
pixels = np.asarray(frame).copy()
mask = (pixels[:,:,3] >= 128).astype(np.uint8)
mask = cv2.morphologyEx(mask,cv2.MORPH_OPEN,np.ones((3,3),np.uint8))
count,labels,stats,_ = cv2.connectedComponentsWithStats(mask)
keep = [i for i in range(1,count) if stats[i,cv2.CC_STAT_AREA] > 2500]
support = np.isin(labels,keep).astype(np.uint8)
support = cv2.erode(support,np.ones((3,3),np.uint8))
pixels[:,:,3] = support*255
pixels[support == 0,:3] = 0
return Image.fromarray(pixels)
def night_facade(frame, seed):
"""Identify window apertures; mark lit glass with FF alpha and unlit walls FE."""
pixels = np.asarray(frame).copy()
rgb = pixels[:,:,:3].astype(np.int16)
mask = ((rgb.max(axis=2)<145) & (rgb[:,:,2]>rgb[:,:,0]-10)).astype(np.uint8)*255
mask = cv2.morphologyEx(mask,cv2.MORPH_CLOSE,np.ones((7,5),np.uint8))
contours,_ = cv2.findContours(mask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
pixels[:,:,3] = 254
rng = np.random.default_rng(seed)
count = 0
for contour in contours:
x,y,w,h = cv2.boundingRect(contour)
if not (8 <= w <= 110 and 14 <= h <= 180 and .8 <= h/w <= 4):
continue
if rng.random() > .48:
continue
region = pixels[y+2:y+h-2,x+2:x+w-2]
source = region[:,:,:3].astype(np.int16)
glass = (source[:,:,2]>source[:,:,0]+4) & (source.max(axis=2)<155)
if glass.sum() < 15:
continue
yy,xx = np.mgrid[:h-4,:w-4]
glow = np.clip(1-((xx-(w-4)/2)/(w/2))**2*.22, .5, 1)
warmth = np.array([210,148,76])*rng.uniform(.7,1.1)
lit = np.clip(glow[:,:,None]*warmth,0,255).astype(np.uint8)
region[:,:,:3][glass] = lit[glass]
region[:,:,3][glass] = 255
count += 1
return Image.fromarray(pixels),count
def main():
OUT.mkdir(parents=True, exist_ok=True)
sky = Image.open(ART/"sunset-sky-v1.png").convert("RGBA").resize((2048,512),Image.Resampling.LANCZOS)
# A short symmetric overlap makes the cylindrical wrap continuous.
original = sky.copy()
for x in range(32):
mixed = Image.blend(original.crop((x,0,x+1,512)),
original.crop((2047-x,0,2048-x,512)), (32-x)/64)
sky.paste(mixed, (x,0))
mixed = Image.blend(original.crop((2047-x,0,2048-x,512)),
original.crop((x,0,x+1,512)), (32-x)/64)
sky.paste(mixed, (2047-x,0))
pack("panorama-v1.bin", [sky.crop((i*512,0,(i+1)*512,512)) for i in range(4)])
night = Image.open(ART/"night-sky-v1.png").convert("RGBA").resize((2048,512),Image.Resampling.LANCZOS)
pack("night-v1.bin",[night.crop((i*512,0,(i+1)*512,512)) for i in range(4)])
hands = Image.open(ART/"rail-hands-v1.png").convert("RGBA")
poses = [hands.crop((round(x*hands.width/4),round(y*hands.height/2),
round((x+1)*hands.width/4),round((y+1)*hands.height/2)))
.resize((640,320),Image.Resampling.LANCZOS)
for y in range(2) for x in range(4)]
# Offline bidirectional flow supplies in-between poses; playback is pure MASM.
coordinates = np.mgrid[:320,:640][::-1].transpose(1,2,0).astype(np.float32)
smooth = []
for first,second in zip(poses,poses[1:]):
a,b = np.asarray(first),np.asarray(second)
ag = cv2.cvtColor(a,cv2.COLOR_RGBA2GRAY)
bg = cv2.cvtColor(b,cv2.COLOR_RGBA2GRAY)
forward = cv2.calcOpticalFlowFarneback(ag,bg,None,.5,5,35,6,7,1.5,0)
backward = cv2.calcOpticalFlowFarneback(bg,ag,None,.5,5,35,6,7,1.5,0)
for index in range(4):
q = index/4
aw = cv2.remap(a,coordinates-forward*q,None,cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT)
bw = cv2.remap(b,coordinates-backward*(1-q),None,cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT)
smooth.append(Image.fromarray(cv2.addWeighted(aw,1-q,bw,q,0)))
smooth += [poses[-1]]*4
assert len(smooth) == 32
pack("rail-hands-v1.bin",[clean_hands(frame) for frame in smooth])
materials = grid("materials-v1.png",4,2)
pack("materials-v1.bin",materials)
panels = grid("panels-v1.png",4,2)[:4]
# One moving leaf, not a photograph of a whole elevator on each leaf.
panels[0] = panels[0].crop((72,16,126,235)).resize((256,256),Image.Resampling.LANCZOS)
# Use only the fluted facade, not the reference's pictured skyline above it.
panels[2] = panels[2].crop((0,116,256,240)).resize((256,256),Image.Resampling.LANCZOS)
cutouts = grid("cutouts-v1.png",2,2)
for frame in cutouts:
assert frame.getchannel("A").getextrema()[0] == 0, "cutouts require actual alpha"
sofas = Image.open(ART/"sofa-views-v1.png").convert("RGBA")
occupied = np.sum(np.asarray(sofas.getchannel("A")) > 128,axis=0) > 10
edges = np.flatnonzero(np.diff(np.r_[False,occupied,False]))
spans = [(int(a),int(b)) for a,b in zip(edges[::2],edges[1::2]) if b-a > 80]
assert len(spans) == 3, spans
boxes = []
for lo,hi in spans:
box = sofas.crop((lo,0,hi,sofas.height)).getchannel("A").point(lambda a:255 if a>128 else 0).getbbox()
boxes.append((lo,box[1],hi,box[3]))
top = min(box[1] for box in boxes)
bottom = max(box[3] for box in boxes)
views = [sofas.crop((box[0],top,box[2],bottom)).resize((256,256),Image.Resampling.LANCZOS)
for box in boxes]
chairs = Image.open(ART/"chair-views-v1.png").convert("RGBA")
split = round(chairs.width*.55)
boxes = [(0,0,split,chairs.height//2),(split,0,chairs.width,chairs.height//2),
(0,chairs.height//2,split,chairs.height),(split,chairs.height//2,chairs.width,chairs.height)]
chair_views = []
for box in boxes:
frame = chairs.crop(box)
tight = frame.getchannel("A").point(lambda a:255 if a>128 else 0).getbbox()
chair_views.append(frame.crop(tight).resize((256,256),Image.Resampling.LANCZOS))
pack("details-v1.bin",panels+cutouts+views+chair_views)
facade = Image.open(ART/"city-facades-v1.png").convert("RGBA")
city = [facade.crop((round(x*facade.width/4),round(y*facade.height/2),
round((x+1)*facade.width/4),round((y+1)*facade.height/2)))
.resize((512,512),Image.Resampling.LANCZOS)
for y in range(2) for x in range(4)]
# Foliage-only tuft, not a planter photographed on a vertical card.
city.append(cutouts[1].crop((0,0,256,160)).resize((512,512),Image.Resampling.LANCZOS))
earth = np.asarray(materials[1].resize((512,512))).copy()
earth[:,:,:3] = (earth[:,:,:3].astype(np.uint16)*np.array([37,31,24])//100).astype(np.uint8)
city.append(Image.fromarray(earth))
city.append(Image.open(ART/"roof-zinc-v1.png").convert("RGBA").resize((512,512),Image.Resampling.LANCZOS))
# Existing high-detail stone is also a road aggregate; water is a generated
# native texture below. Neither is a photograph of a whole city object.
road = np.asarray(materials[1].resize((512,512))).copy()
road[:,:,:3] = (road[:,:,:3].astype(np.uint16)*45//100).astype(np.uint8)
city.append(Image.fromarray(road))
yy,xx = np.mgrid[:512,:512]
ripple = (np.sin(xx*.17+np.sin(yy*.09)*2)+np.sin(yy*.31))*4
water = np.zeros((512,512,4),dtype=np.uint8)
for channel,base in enumerate((24,55,72)):
water[:,:,channel] = np.clip(base+ripple,0,255).astype(np.uint8)
water[:,:,3] = 255
city.append(Image.fromarray(water))
modern = Image.open(ART/"modern-facades-v1.png").convert("RGBA")
city += [modern.crop((round(x*modern.width/4),round(y*modern.height/2),
round((x+1)*modern.width/4),round((y+1)*modern.height/2)))
.resize((512,512),Image.Resampling.LANCZOS)
for y in range(2) for x in range(4)]
noise = np.random.default_rng(701).integers(-8,9,size=(512,512,1))
lawn = np.empty((512,512,4),dtype=np.uint8)
lawn[:,:,:3] = np.clip(np.array([39,68,43])+noise,0,255)
lawn[:,:,3] = 255
city.append(Image.fromarray(lawn))
pack("city-v1.bin",city)
night_city,lit_windows = [],0
for index,frame in enumerate(city):
if index < 8 or 13 <= index < 21:
frame,count = night_facade(frame,2000+index)
lit_windows += count
night_city.append(frame)
pack("city-night-v1.bin",night_city)
count = pack_city_district()
pack_roof_map()
print(f"Packed roof: sunset/night, {lit_windows} distinct lit window apertures, 32 hand frames, {count} buildings.")
def pack_city_district():
# A connected street/courtyard district, not a ring of independent props.
blocks = []
lots,parks = [],[]
for gy in range(-5,6):
for gx in range(-5,6):
x,y = 20+gx*36,11+gy*36
if gx == 0 and gy == 0:
continue
# The cross-city canal has continuous quays, not houses over water.
if y+14 > 54 and y-14 < 82:
continue
# Two open squares reserve space around the dome and golden spire.
if (gx,gy) in ((0,3),(4,0)):
continue
key = (gx*17+gy*31)%37
if key%11 == 0 or (gx,gy) in ((1,0),(-1,0)):
parks.append((x,y))
continue
facade = 186+(gx*3+gy)%8
if max(abs(gx),abs(gy)) <= 1:
top = -60+6+key%6
facade = 203
wings = [(x-6,x+5,y-5,y+5)]
elif key%7 == 0:
top = -60+24+key%17
facade = 199+(key%2)*5
wings = [(x-8,x+8,y-10,y+10)]
else:
top = -60+12+key%16
wings = [(x-14,x+14,y-14,y-7),(x-14,x+14,y+7,y+14),
(x-14,x-7,y-7,y+7),(x+7,x+14,y-7,y+7)]
if key%3 == 0:
wings = wings[:3]
elif key%5 == 0:
wings = wings[1:3]
lots.append({"centre":[x,y],"courtyard":[x-7,x+7,y-7,y+7],
"facade":facade,"height":top+60})
for index,wing in enumerate(wings):
blocks.append((*wing,top+(index%2)*.3,facade,index))
# Distant quarters are aggregated geometry, not a scaled city photograph.
for gy in range(-8,9):
for gx in range(-8,9):
if max(abs(gx),abs(gy)) <= 3:
continue
x,y = 20+gx*64,11+gy*64
if y+24 > 54 and y-24 < 82:
continue
key = (gx*5+gy*11)%29
top = -60+10+key
half = 17+key%10
blocks.append((x-half,x+half,y-24,y+24,top,186+(gx+gy)%8,4))
towers = [(320,-130,90),(355,-165,126),(390,-125,108),(425,-195,155),
(460,-145,102),(345,-225,115),(405,-260,132),(475,-240,88)]
for index,(x,y,height) in enumerate(towers):
blocks.append((x-8,x+8,y-10,y+10,-60+height,200+index%3,2))
blocks.append((x-5,x+5,y-7,y+7,-60+height+7,200+index%3,2))
# The eastern avenue opens into a broad river, not an endless paved corridor.
blocks = [b for b in blocks if not (b[1] > 68 and b[2] < 81 and b[3] > 26)]
# Far riverfront quarters break the empty, perfectly straight horizon.
for side in (-1,1):
for i in range(10):
x = 570+i*34
y = 6-(i%3)*8 if side < 0 else 106+(i%4)*9
h = 16+(i*7)%24
blocks.append((x-11,x+11,y-10,y+10,-60+h,186+i%8,4))
# Canal clearance removes water footprints, not entire empty city blocks.
waterfront = []
for gx in range(-13,2):
x = 20+gx*36
for bank in (0,1):
key = (gx*19+bank*7)%31
front,back = (53,39-key%4) if bank == 0 else (82,94+key%3)
y0,y1 = sorted((front,back))
mid = x-2+key%5
for index,(x0,x1) in enumerate(((x-13,mid-.45),(mid+.45,x+13))):
top = -60+12+(key+index*3)%11
waterfront.append((x0,x1,y0,y1,top,186+(gx+bank*3+index)%8,5))
blocks.extend(waterfront)
blocks.sort(key=lambda b:(((b[0]+b[1])/2-20)**2+((b[2]+b[3])/2-11)**2))
# Ground-only baked illumination follows the same real building footprints.
elevation = np.zeros((512,512),dtype=np.float32)
for x0,x1,y0,y1,z,_,_ in blocks:
loX,hiX = max(0,math.floor(x0/2)+256),min(512,math.ceil(x1/2)+256)
loY,hiY = max(0,math.floor(y0/2)+256),min(512,math.ceil(y1/2)+256)
if loX < hiX and loY < hiY:
elevation[loY:hiY,loX:hiX] = np.maximum(elevation[loY:hiY,loX:hiX],z+60)
occlusion = np.zeros_like(elevation)
padded = np.pad(elevation,128)
for step in range(1,100):
sx,sy = round(-step*.89),round(step*.45)
ray_height = step*2*math.tan(math.radians(18))
blocked = padded[128+sy:640+sy,128+sx:640+sx]-ray_height
occlusion = np.maximum(occlusion,blocked)
shadow = cv2.GaussianBlur(np.where(occlusion>0,155,250).astype(np.uint8),(5,5),1)
day = np.empty((512,512,4),dtype=np.uint8)
day[:,:,:3] = shadow[:,:,None]
day[:,:,3] = 255
lamps = []
for gx in range(-5,6):
for gy in range(-6,7):
lamps.append((34.8+36*gx,11+30*gy))
lamps += [(x,y) for x in range(-170,211,18) for y in (58.5,77.5)]
lamps = [(x,y) for x,y in lamps if not (60 < y < 76 or (x > 68 and 29 < y < 76))]
lamps += [(x,y) for x in range(86,650,18) for y in (27,78)]
yy,xx = np.mgrid[:512,:512]
world_x,world_y = (xx-256)*2,(yy-256)*2
illumination = np.zeros((512,512,3),dtype=np.float32)+[24,30,46]
for lx,ly in lamps:
energy = np.exp(-((world_x-lx)**2+(world_y-ly)**2)/(2*5.5**2))
illumination += energy[:,:,None]*np.array([215,150,64])
if ly in (27,78,58.5,77.5):
reflected = np.exp(-((world_x-lx)**2/(2*1.6**2)+(world_y-ly)**2/(2*13**2)))
reflected *= ((world_y>=60)&(world_y<=76)) | ((world_x>=70)&(world_y>=29)&(world_y<=76))
illumination += reflected[:,:,None]*np.array([180,125,60])
night_light = np.empty((512,512,4),dtype=np.uint8)
night_light[:,:,:3] = np.clip(illumination,0,255).astype(np.uint8)
night_light[:,:,3] = 255
glow = np.empty_like(night_light)
glow[:,:,:3] = np.clip((illumination-np.array([24,30,46]))*.38,0,255).astype(np.uint8)
glow[:,:,3] = 255
pack("lighting-v1.bin",[Image.fromarray(day),Image.fromarray(night_light),Image.fromarray(glow)])
district = ["; Generated city block footprints and rooftop-relative heights.",
f"ROOF_BUILDING_COUNT equ {len(blocks)}", ".data", "roofDistrict3D LABEL DWORD"]
for block in blocks:
district.append(" dd "+",".join(str(round(v*65536)) if i<5 else str(v)
for i,v in enumerate(block)))
district += [f"ROOF_PARK_COUNT equ {len(parks)}","roofCityParks3D LABEL DWORD"]
district += [f" dd {round(x*65536)},{round(y*65536)}" for x,y in parks]
district += [f"ROOF_CITY_LAMP_COUNT equ {len(lamps)}","roofCityLamps3D LABEL DWORD"]
district += [f" dd {round(x*65536)},{round(y*65536)}" for x,y in lamps]
pieces = []
def landmark_piece(kind,coords,material,pivot,target,scale=2):
ox,oy = pivot
tx,ty = target
if kind == 0:
a,b,c,d,e,f = coords
coords = ((a-ox)*scale+tx,(b-ox)*scale+tx,(c-oy)*scale+ty,(d-oy)*scale+ty,
(e+17)*scale-60,(f+17)*scale-60)
else:
a,b,c,d,e,f = coords
coords = ((a-ox)*scale+tx,(b-oy)*scale+ty,(c+17)*scale-60,d*scale,e*scale,f*scale)
pieces.append((kind,*[round(v*65536) for v in coords],material))
dome = lambda kind,coords,mat: landmark_piece(kind,coords,mat,(56,47),(20,119))
dome(0,(47,65,38,56,-17,2),193)
dome(0,(50,62,41,53,2,5),168)
for index in range(16):
angle = index*math.tau/16
x,y = 56+5*math.cos(angle),47+5*math.sin(angle)
dome(0,(x,x+.35,y,y+.35,5,8),168)
dome(0,(50,62,41,53,8,8.4),196)
dome(1,(56,47,8.3,5.9,5.9,5),196)
dome(0,(55.4,56.6,46.4,47.6,13,15),166)
dome(0,(55.9,56.1,46.9,47.1,15,18),166)
spire = lambda kind,coords,mat: landmark_piece(kind,coords,mat,(-16,-61),(164,11))
spire(0,(-23,-9,-68,-54,-17,0),191)
spire(0,(-19,-13,-64,-58,0,9),206)
spire(0,(-18,-14,-63,-59,9,15),191)
spire(2,(-16,-61,15,1.8,1.8,20),166)
# The same modest quay geometry is consumed by both native renderers.
quays = []
def quay_piece(kind,coords,mat):
piece = (kind,*[round(v*65536) for v in coords],mat)
pieces.append(piece)
quays.append((kind,*coords,mat))
for y0,y1 in ((55,60),(76,81)):
quay_piece(0,(-520,70,y0,y1,-62,-59.75),162)
edge = 59.65 if y0 < 60 else 76
for a,b in ((-520,-37.6),(-30.4,34.4),(41.6,70)):
quay_piece(0,(a,b,edge,edge+.35,-59.75,-58.85),162)
quay_piece(0,(a,b,edge-.04,edge+.39,-58.85,-58.7),168)
for x in range(-184,68,18):
if min(abs(x-b) for b in (-34,38)) < 6:
continue
for y in (56.7,79.3):
quay_piece(0,(x-.12,x+.12,y-.12,y+.12,-59.75,-56.8),162)
quay_piece(1,(x,y,-56.8,1.5,1.5,2.1),207)
quay_piece(0,(x+3,x+5,y-.25,y+.25,-59.35,-59.15),196)
for leg in (x+3.2,x+4.6):
quay_piece(0,(leg,leg+.18,y-.2,y+.2,-59.75,-59.35),165)
district += [f"ROOF_LANDMARK_PARTS equ {len(pieces)}","roofLandmarkParts3D LABEL DWORD"]
district += [" dd "+",".join(map(str,piece)) for piece in pieces]
fog = [min(190,round(d*180/(d+250))) for d in range(1024)]
for start in range(0,1024,32):
district.append(("roofFog3D" if start == 0 else " ")+" db "+
",".join(map(str,fog[start:start+32])))
district.append(".code")
(ROOT/"src_graphics/rooftop_district_data.inc").write_text("\n".join(district)+"\n",encoding="ascii")
(ART/"district-map-v1.json").write_text(json.dumps({"extent":[-520,560,-530,550],
"casino":[10.9,29.1,.9,21.1],"street_spacing":36,"street_width":8,
"street_level":-60,"canal":[-190,230,60,76],"bridges":[-34,38,110],
"east_river":[70,1800,29,76],"river_bridges":[110,254,398],
"waterfront_buildings":waterfront,"canal_quay_parts":quays,
"lots":lots,"parks":parks,"lamps":lamps,"towers":towers,"buildings":blocks,
"landmarks":{"dome":[20,119],"spire":[164,11]}},indent=2)+"\n",encoding="ascii")
return len(blocks)
def pack_roof_map():
# Collision-only enclosure uses a valid legacy prepass material; the final
# rooftop pass replaces its tall walls with low world-space railings.
roof = [[102]*32 for _ in range(32)]
for y in range(1,21):
for x in range(11,29):
roof[y][x] = 0
lines = ["; Generated by tools/generators/pack_rooftop.py.", ".data"]
for y,row in enumerate(roof):
lines.append(("roofMap3D" if y == 0 else " ")+" db "+",".join(map(str,row)))
columns = [round(math.atan((x/320-1)*.66)*2048/(2*math.pi)) for x in range(640)]
for start in range(0,640,16):
lines.append(("roofSkyColumns3D" if start == 0 else " ")+" dd "+
",".join(map(str,columns[start:start+16])))
(ROOT/"src_graphics/rooftop_map.inc").write_text("\n".join(lines)+"\n",encoding="ascii")
(ART/"packed-v1.json").write_text(json.dumps({"panorama":[512,512,4],
"materials":[256,256,8],"details":[256,256,15],"city":[512,512,22],
"city-night":[512,512,22],"night":[512,512,4],"rail-hands":[640,320,32],
"lighting":[512,512,3],"city_renderer":"world-space MASM geometry; sky only is raster",
"source":"Built-in image generation; runtime geometry and interaction are assembly-only",
"reference":"https://www.loftprojectetagi.ru/visitors/krysha/"},indent=2)+"\n",encoding="ascii")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--district-only",action="store_true")
if parser.parse_args().district_only:
print(f"Packed city district: {pack_city_district()} building parts")
else:
main()

View file

@ -0,0 +1,335 @@
"""Offline city meshes. Playback, GL submission and compositing remain MASM."""
from pathlib import Path
import json
import math
import struct
from collections import Counter
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT/"assets/animations/rooftop"
ART = ROOT/"assets/sprites/rooftop"
class Mesh:
def __init__(self):
self.triangles = []
def quad(self, material, points, uv=None, smooth=None):
# Points use the game's X/Y ground plane and Z up. GL receives X/Z/Y.
a,b,c = points[:3]
u = [b[i]-a[i] for i in range(3)]
v = [c[i]-a[i] for i in range(3)]
normal = (u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0])
size = math.sqrt(sum(x*x for x in normal))
if size < 1e-8:
return
normal = tuple(x/size for x in normal)
uv = uv or [(0,0),(1,0),(1,1),(0,1)]
for ids in ((0,1,2),(0,2,3)):
if len({points[i] for i in ids}) < 3:
continue
vertices = []
for i in ids:
x,y,z = points[i]
nx,ny,nz = smooth[i] if smooth else normal
# Vertical texture convention: the top of a facade is v=0.
vertices.extend((x,z,y,nx,nz,ny,*uv[i]))
self.triangles.append((material,vertices))
def box(self, x0,x1,y0,y1,z0,z1,material,roof=None,repeat_v=1):
if min(x1-x0,y1-y0,z1-z0) <= 0:
return
# Explicit winding/normals face outwards; negative scale is never used.
uv = [(0,repeat_v),(1,repeat_v),(1,0),(0,0)]
self.quad(material,[(x0,y0,z0),(x1,y0,z0),(x1,y0,z1),(x0,y0,z1)],uv)
self.quad(material,[(x1,y1,z0),(x0,y1,z0),(x0,y1,z1),(x1,y1,z1)],uv)
self.quad(material,[(x0,y1,z0),(x0,y0,z0),(x0,y0,z1),(x0,y1,z1)],uv)
self.quad(material,[(x1,y0,z0),(x1,y1,z0),(x1,y1,z1),(x1,y0,z1)],uv)
self.horizontal(x0,x1,y0,y1,z1,roof or material)
def horizontal(self,x0,x1,y0,y1,z,material,uv=None):
uv = uv or [(0,0),(1,0),(1,1),(0,1)]
# Vertex-fog drivers must not interpolate a whole city from distant corners.
def cuts(lo,hi):
if hi-lo <= 64:
return [lo,hi]
candidates = list(range(-512,513,64))+[-96,-32,32,96,-1536,-1024,-768,768,1024,1536]
return [lo,*sorted(v for v in candidates if lo < v < hi),hi]
def texcoord(x,y):
s,t = (x-x0)/(x1-x0),(y1-y)/(y1-y0)
return tuple((1-t)*((1-s)*uv[0][i]+s*uv[1][i])+
t*((1-s)*uv[3][i]+s*uv[2][i]) for i in range(2))
xs,ys = cuts(x0,x1),cuts(y0,y1)
for a,b in zip(xs,xs[1:]):
for c,d in zip(ys,ys[1:]):
points = [(a,d,z),(b,d,z),(b,c,z),(a,c,z)]
coords = [texcoord(x,y) for x,y,_ in points]
self.quad(material,list(reversed(points)),list(reversed(coords)))
def ellipsoid(self,x,y,z,rx,ry,rz,material,upper=False,rings=10,sides=24):
end = math.pi/2 if upper else math.pi
def point(t,p):
nx,ny,nz = math.sin(t)*math.cos(p),math.sin(t)*math.sin(p),math.cos(t)
n = (nx/rx,ny/ry,nz/rz)
length = math.sqrt(sum(v*v for v in n))
return (x+rx*nx,y+ry*ny,z+rz*nz),tuple(v/length for v in n)
for row in range(rings):
t0,t1 = max(.0001,row*end/rings),(row+1)*end/rings
for col in range(sides):
p0,p1 = col*math.tau/sides,(col+1)*math.tau/sides
samples = [point(t0,p0),point(t1,p0),point(t1,p1),point(t0,p1)]
self.quad(material,[p for p,n in samples],
[(col/sides,row/rings),(col/sides,(row+1)/rings),
((col+1)/sides,(row+1)/rings),((col+1)/sides,row/rings)],
[n for p,n in samples])
def cylinder(self,x,y,z0,z1,radius,material,sides=16,repeat=1):
for col in range(sides):
a,b = col*math.tau/sides,(col+1)*math.tau/sides
na,nb = (math.cos(a),math.sin(a),0),(math.cos(b),math.sin(b),0)
self.quad(material,[(x+radius*na[0],y+radius*na[1],z0),
(x+radius*nb[0],y+radius*nb[1],z0),
(x+radius*nb[0],y+radius*nb[1],z1),
(x+radius*na[0],y+radius*na[1],z1)],
uv=[(col*repeat/sides,1),((col+1)*repeat/sides,1),
((col+1)*repeat/sides,0),(col*repeat/sides,0)],
smooth=[na,nb,nb,na])
self.quad(material,[(x,y,z1),(x+radius*na[0],y+radius*na[1],z1),
(x+radius*nb[0],y+radius*nb[1],z1),
(x+radius*nb[0],y+radius*nb[1],z1)])
def hip(self,x0,x1,y0,y1,z,height,material=196):
if x1-x0 >= y1-y0:
ridge = [(x0+1,(y0+y1)/2,z+height),(x1-1,(y0+y1)/2,z+height)]
self.quad(material,[(x0,y0,z),(x1,y0,z),ridge[1],ridge[0]],[(0,0),((x1-x0)/4,0),((x1-x0)/4,1),(0,1)])
self.quad(material,[(x1,y1,z),(x0,y1,z),ridge[0],ridge[1]],[(0,0),((x1-x0)/4,0),((x1-x0)/4,1),(0,1)])
self.quad(material,[(x0,y1,z),(x0,y0,z),ridge[0],ridge[0]])
self.quad(material,[(x1,y0,z),(x1,y1,z),ridge[1],ridge[1]])
else:
# Transposing X/Y reverses winding; save() normalises every triangle.
cy = (x0+x1)/2
a,b = (cy,y0+1,z+height),(cy,y1-1,z+height)
self.quad(material,[(x0,y1,z),(x0,y0,z),a,b],[(0,0),((y1-y0)/4,0),((y1-y0)/4,1),(0,1)])
self.quad(material,[(x1,y0,z),(x1,y1,z),b,a],[(0,0),((y1-y0)/4,0),((y1-y0)/4,1),(0,1)])
self.quad(material,[(x0,y0,z),(x1,y0,z),a,a])
self.quad(material,[(x1,y1,z),(x0,y1,z),b,b])
def save(self,name,static=False):
original_count = len(self.triangles)
oriented = []
for mat,packed in self.triangles:
vertices = [packed[i:i+8] for i in (0,8,16)]
a,b,c = [v[:3] for v in vertices]
u,v = [b[i]-a[i] for i in range(3)],[c[i]-a[i] for i in range(3)]
normal = [u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]]
if sum(x*x for x in normal) < 1e-16:
continue
expected = [sum(vertex[i+3] for vertex in vertices) for i in range(3)]
if sum(x*y for x,y in zip(normal,expected)) < 0:
vertices[1],vertices[2] = vertices[2],vertices[1]
normal = [-x for x in normal]
if static:
# Union of every standing/seated/leaning eye position, in GL X/Z/Y axes.
bounds = ((11.0,29.0),(1.05,1.8),(1.0,21.0))
maximum = sum(n*((hi if n >= 0 else lo)-p)
for n,(lo,hi),p in zip(normal,bounds,a))
if maximum < -1e-7:
continue
oriented.append((mat,[value for vertex in vertices for value in vertex]))
self.triangles = oriented
self.triangles.sort(key=lambda t:t[0])
payload = b"".join(struct.pack("<I24f",mat,*vertices) for mat,vertices in self.triangles)
assert len(payload) == len(self.triangles)*100
assert all(161 <= mat <= 209 for mat,_ in self.triangles)
assert all(math.isfinite(v) for _,vs in self.triangles for v in vs)
assert len(self.triangles) <= 65000
(OUT/name).write_bytes(struct.pack("<4sII",b"RGM1",len(self.triangles),len(payload))+payload)
self.removed = original_count-len(self.triangles)
return len(self.triangles)
def main():
city = json.loads((ART/"district-map-v1.json").read_text())
scene = Mesh()
# The terrace is on a real sixty-metre high-rise, not a floating platform.
scene.box(10.9,29.1,.9,21.1,-60,0,200,roof=165,repeat_v=3)
scene.horizontal(10.9,29.1,.9,21.1,.004,161,[(0,10),(9,10),(9,0),(0,0)])
# Terrain is actual split geometry, leaving a lowered water channel.
def patches(lo,hi):
return [(lo,70,lo,60,False),(70,hi,lo,29,False),(lo,hi,76,hi,False),
(lo,hi,60,76,True),(70,hi,29,60,True)]
def world_uv(x0,x1,y0,y1,scale):
return [(x0/scale,y1/scale),(x1/scale,y1/scale),
(x1/scale,y0/scale),(x0/scale,y0/scale)]
for x0,x1,y0,y1,water in patches(-2000,2000):
scene.horizontal(x0,x1,y0,y1,-62 if water else -60,198 if water else 162,
world_uv(x0,x1,y0,y1,12 if water else 4))
def subtract(rect,obstacle):
a,b,c,d = rect
x0,x1,y0,y1 = obstacle
x0,x1,y0,y1 = max(a,x0),min(b,x1),max(c,y0),min(d,y1)
if x0 >= x1 or y0 >= y1:
return [rect]
return [r for r in ((a,x0,c,d),(x1,b,c,d),(x0,x1,c,y0),(x0,x1,y1,d))
if r[1]-r[0] > .01 and r[3]-r[2] > .01]
def road(rect,vertical=False):
pieces = [rect]
obstacles = [(-2000,2000,58,79),(68,2000,26,81)]
if vertical:
obstacles += [(-2000,2000,29+36*i-4,29+36*i+4) for i in range(-16,17)]
for obstacle in obstacles:
pieces = [p for r in pieces for p in subtract(r,obstacle)]
for a,b,c,d in pieces:
roads.append((a,b,c,d))
scene.horizontal(a,b,c,d,-59.98,197,world_uv(a,b,c,d,4))
roads = []
for i in range(-16,17):
x = 38+36*i
road((x-4,x+4,-600,600),vertical=True)
y = 29+36*i
road((-600,600,y-4,y+4))
# Small lane dashes and pedestrian crossings give the streets a readable scale.
for gx in range(-6,7):
x = 38+36*gx
for y in range(-205,231,9):
if not 54 < y < 82 and not (x > 68 and 24 < y < 82):
scene.horizontal(x-.07,x+.07,y,y+3,-59.965,168)
for gy in range(-6,7):
y = 29+36*gy
if 54 < y < 82 or (x > 68 and 24 < y < 82):
continue
for stripe in range(7):
sx = x-3.2+stripe*.95
scene.horizontal(sx,sx+.45,y+5,y+7,-59.96,168)
for index,(x0,x1,y0,y1,roof,mat,kind) in enumerate(city["buildings"]):
modern = mat >= 199
repeat = max(1,round((roof+60)/26)) if modern else 1
scene.box(x0,x1,y0,y1,-60,roof,mat,repeat_v=repeat)
scene.box(x0-.18,x1+.18,y0-.18,y1+.18,roof,roof+.18,165 if modern else 168)
distance = math.hypot((x0+x1)/2-20,(y0+y1)/2-11)
if not modern and distance < 220:
scene.box(x0-.12,x1+.12,y0-.12,y1+.12,-60,-58.8,162)
for z in (roof-.55,roof-.3):
scene.box(x0-.25,x1+.25,y0-.25,y1+.25,z,z+.12,168)
if distance < 150:
for x in (x0,x1-.28):
for y in (y0,y1-.28):
scene.box(x-.08,x+.36,y-.08,y+.36,-58.8,roof-.5,168)
scene.hip(x0-.25,x1+.25,y0-.25,y1+.25,roof+.18,2.4)
scene.box(x0+.8,x0+1.45,y0+.8,y0+1.35,roof+1.2,roof+3.2,190,roof=162)
# Recessed dormer volumes interrupt long, otherwise empty roof slopes.
if x1-x0 > 12 and distance < 150:
for x in range(math.ceil(x0+3),math.floor(x1-2),6):
scene.box(x-.55,x+.55,y0+.8,y0+1.55,roof+.45,roof+1.45,168)
scene.quad(200,[(x-.4,y0+.79,roof+.6),(x+.4,y0+.79,roof+.6),
(x+.4,y0+.79,roof+1.25),(x-.4,y0+.79,roof+1.25)])
scene.hip(x-.65,x+.65,y0+.7,y0+1.65,roof+1.45,.4)
else:
scene.horizontal(x0,x1,y0,y1,roof+.19,196)
# Static landmark records share their exact footprints with the MASM world.
data = (ROOT/"src_graphics/rooftop_district_data.inc").read_text()
section = data.split("roofLandmarkParts3D LABEL DWORD",1)[1].split("roofFog3D",1)[0]
for line in section.splitlines():
if not line.strip().startswith("dd "):
continue
values = list(map(int,line.strip()[3:].split(",")))
kind,coords,mat = values[0],[v/65536 for v in values[1:7]],values[7]
# The dome's old rectangular drum is replaced with a proper circular one.
if kind == 0 and 7 < coords[0] < 12 and 128 < coords[3] < 134 and coords[4] > -30:
continue
if kind == 0:
x0,x1,y0,y1,z0,z1 = coords
if x1-x0 < 1.1 and y1-y0 < 1.1 and z1-z0 > 3:
scene.cylinder((x0+x1)/2,(y0+y1)/2,z0,z1,(x1-x0)/2,mat)
scene.box(x0-.12,x1+.12,y0-.12,y1+.12,z1-.25,z1,mat)
else:
scene.box(*coords,mat)
elif kind == 1:
foliage = mat == 207
scene.ellipsoid(*coords,mat,upper=not foliage,
rings=4 if foliage else 14,sides=8 if foliage else 48)
elif kind == 2:
x,y,z,rx,ry,h = coords
h *= .65
for i in range(8):
a,b = i*math.tau/8,(i+1)*math.tau/8
scene.quad(mat,[(x+rx*math.cos(a),y+ry*math.sin(a),z),
(x+rx*math.cos(b),y+ry*math.sin(b),z),
(x,y,z+h),(x,y,z+h)])
# Cathedral drum, portico and a solid entablature under the copper dome.
scene.cylinder(20,119,-22,-10,9.5,191,sides=48,repeat=6)
for z,radius in ((-22,10.3),(-16,10.0),(-10.5,12.4)):
scene.cylinder(20,119,z,z+.45,radius,168,sides=48,repeat=6)
for step in range(4):
scene.box(3+step*.4,37-step*.4,95+step*.5,104,-60+step*.35,-59.65+step*.35,162)
for x in (6,11.5,17,22.5,28,33.5):
scene.cylinder(x,99,-58.6,-40,.55,168,sides=16)
scene.cylinder(x,99,-58.6,-57.8,.8,162,sides=16)
scene.cylinder(x,99,-40,-39.5,.8,168,sides=16)
scene.box(3,37,97,104,-39.5,-38.5,168)
scene.quad(168,[(3,96.99,-38.5),(37,96.99,-38.5),(20,96.99,-33),(20,96.99,-33)])
scene.hip(2.8,37.2,96.8,104.2,-38.5,5.5)
for x in sorted(set(city["bridges"]+city["river_bridges"])):
start,end = (28,77) if x >= 70 else (59,77)
scene.horizontal(x-3,x+3,start,end,-59.9,197,world_uv(x-3,x+3,start,end,4))
for side in (x-3,x+3):
for i in range(24):
y0,y1 = start+(end-start)*i/24,start+(end-start)*(i+1)/24
mid,radius = (start+end)/2,(end-start)/2
z0 = -62+1.8*math.sqrt(max(0,1-((y0-mid)/radius)**2))
z1 = -62+1.8*math.sqrt(max(0,1-((y1-mid)/radius)**2))
points = [(side,y0,z0),(side,y1,z1),(side,y1,-60),(side,y0,-60)]
scene.quad(162,list(reversed(points)) if side < x else points)
for y in range(start,end+1,2):
scene.box(side-.1,side+.1,y-.1,y+.1,-60,-59,165)
scene.box(side-.08,side+.08,start,end,-59.1,-59,165)
# Raised embankments, a tree-lined promenade and small piers follow the river.
for y0,y1 in ((26,29),(76,81)):
scene.box(70,1100,y0,y1,-62,-59.75,162)
for x in range(84,640,28):
y = (y0+y1)/2
if min(abs(x-b) for b in city["river_bridges"]) < 6:
continue
scene.cylinder(x,y,-59.75,-56,.14,162,sides=6)
scene.ellipsoid(x,y,-55,1.7,1.7,2.1,207,rings=4,sides=8)
scene.box(x+3,x+5,y-.3,y+.3,-59.75,-59.3,162)
for x in (156,300,444):
scene.box(x,x+7,28,34,-62,-60.3,196)
for x,y in city["parks"]:
scene.horizontal(x-12,x+12,y-12,y+12,-59.9,207)
scene.horizontal(x-1,x+1,y-12,y+12,-59.8,162)
for dx in (-8,8):
for dy in (-8,0,8):
scene.cylinder(x+dx,y+dy,-60,-56,.15,162,sides=8)
scene.ellipsoid(x+dx,y+dy,-55.5,2.0,2.0,2.4,207,rings=5,sides=10)
for x,y in city.get("lamps",[]):
scene.cylinder(x,y,-60,-55,.055,165,sides=6)
scene.box(x-.22,x+.22,y-.22,y+.22,-55.15,-54.9,178)
# A second ground pass multiplies the baked, geometry-derived light map.
for a,b,c,d,water in patches(-512,512):
uv = [((a+512)/1024,(d+512)/1024),((b+512)/1024,(d+512)/1024),
((b+512)/1024,(c+512)/1024),((a+512)/1024,(c+512)/1024)]
scene.horizontal(a,b,c,d,-61.94 if water else -59.94,208,uv)
scene.horizontal(a,b,c,d,-61.93 if water else -59.93,209,uv)
car = Mesh()
car.box(-1.9,1.9,-.8,.8,.25,.85,196)
car.box(-.9,.9,-.65,.65,.85,1.35,200)
for y in (-.57,.57):
car.box(1.88,1.93,y-.12,y+.12,.5,.68,178)
for x in (-1.2,1.2):
for y in (-.83,.83):
car.ellipsoid(x,y,.3,.32,.12,.32,197,rings=5,sides=10)
boat = Mesh()
boat.ellipsoid(0,0,-1.6,3,1,.5,196,rings=6,sides=16)
boat.box(-1,1,-.6,.6,-1.4,-.7,168)
counts = {"scene":scene.save("mesh-city-v1.bin",static=True),"car":car.save("mesh-car-v1.bin"),
"boat":boat.save("mesh-boat-v1.bin")}
(ART/"mesh-v1.json").write_text(json.dumps({"format":"RGM1: count/bytes + material/3*(position,normal,uv)",
"gl_axes":"X/Z/Y; normals explicitly outward; no runtime model engine", "triangles":counts,
"road_rectangles":roads,"river_patches":[p for p in patches(-2000,2000) if p[-1]],
"removed_invisible_triangles":scene.removed,"front_face":"CCW"},indent=2)+"\n")
print("Packed native GL meshes:",counts)
if __name__ == "__main__":
main()

View file

@ -6,6 +6,7 @@ from pathlib import Path
from PIL import Image
from check_card_exit_build import Coff
from check_coin_detail import font, text_matches
from check_terminals import signature
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "build/actions"
@ -62,11 +63,11 @@ def main(parity=False):
contact.paste(frames[name], ((i % 3) * 640, (i // 3) * 480))
contact.save(OUT / "billiards-contact.png")
if parity:
tested = Coff(OUT / "actions-test.obj")
tested = Coff(ROOT / "build/billiards/module.obj")
shipped = Coff(ROOT / "build/graphics/poker.obj")
for name, size in FUNCTIONS.items():
symbol = f"_{name}@{size}"
assert tested.signature(symbol) == shipped.signature(symbol), name
assert signature(tested,symbol,tail_branches=True) == signature(shipped,symbol,tail_branches=True), name
print("PASS: Russian selection, both ball sets, native captures, rebound prediction" +
(" and shipping/tested billiards parity" if parity else ""))

View file

@ -133,7 +133,9 @@ def main():
print("PASS: dart scene, scoring, input and camera match the shipping object")
return
masm = Path("F:/MASM32/masm32")
subprocess.run([str(masm / "bin/ml.exe"), "/c", "/coff", "/Cp", "/nologo", "/I.",
modern_ml = Path("C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.51.36231/bin/Hostx64/x86/ml.exe")
assembler = modern_ml if modern_ml.is_file() else masm / "bin/ml.exe"
subprocess.run([str(assembler), "/c", "/coff", "/Cp", "/nologo", "/I.",
"/Fobuild/darts/module.obj", "build/darts/module.asm"], cwd=ROOT, check=True)
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
target = "ui" if args.interactive else "module"
@ -165,7 +167,7 @@ def main():
sway[0].save(OUT / "sway.gif", save_all=True, append_images=sway[1:], duration=40, loop=0)
triples, = struct.unpack("<I", (OUT / "aim-metrics.bin").read_bytes())
print(f"PASS: stationary centre-of-T20 aim scored triples in {triples}/100 native samples")
print("PASS: 67 scoring vectors, 20 complete bot matches, bust/turn isolation, fine mouse/release, sway/fatigue, board footprint, Tab, orbit camera and native moving frames")
print("PASS: 67 scoring vectors, 20 complete bot matches, bust/turn isolation, fine mouse/release, sway/fatigue, board footprint, nine-pixel reticle, Tab, orbit camera and native moving frames")

View file

@ -1,10 +1,11 @@
"""Compile and execute fresh MASM navigation, actors and room renderers."""
from pathlib import Path
import argparse
import json
import re
import struct
import subprocess
from PIL import Image, ImageStat
from PIL import Image, ImageStat, ImageChops
from check_card_exit_build import Coff
from check_terminals import signature
@ -36,6 +37,7 @@ def prepare():
code += "\n" + read("src_graphics/steel_door.inc")
code += "\n" + read("src_graphics/fortune_room.inc")
code += "\n" + read("src_graphics/fortune_dialog.inc")
code += "\n" + expand("src_graphics/rooftop.inc")
code += "\n" + proc(read("src_graphics/unavailable.inc"),"BeginUnavailable3D")
code += "\n" + read("src_graphics/observer.inc")
code += "\n" + expand("src_graphics/darts.inc").replace(":SDWORD", ":DWORD")
@ -48,7 +50,7 @@ def prepare():
"CheckHallInteractions3D","TriggerHallInteraction3D","HandleStairs3D","RenderStairs3D",
"CanWalkHall3D","UpdateHall3D","UpdatePatrons3D","RenderPatrons3D","RenderBar3D",
"BuyBarDrink3D","HandleBar3D"]),
("src_graphics/platform_code.inc",["CanWalk3D","WindowEvents3D"]),
("src_graphics/platform_code.inc",["CanWalk3D","WindowEvents3D","Present3D"]),
("casino3d.inc",["HandlePlayerInput","CheckInteractions","RenderWallsRange","RenderWalls","RaycastWorkerProc","Trigger3DInteraction"]),
("src_graphics/city_bins.inc",["ResolveCitySurface3D","PrepareCityDepth3D","PrepareCityRays3D",
"CityRayPlane3D","DrawCityVertical3D","DrawCityHorizontal3D","DrawCityBoxSides3D"]),
@ -60,10 +62,10 @@ def prepare():
own_funcs = set(re.findall(r"^(\w+) PROC",code,re.M))
npc = read("src_graphics/npc_life_data.inc")
npc = npc[npc.index("npcGuestArt3D "):npc.index("npcLifeClock3D ")]
data = "\n".join(read(path) for path in ("src_graphics/hall_expansion_data.inc",
data = "\n".join(expand(path) for path in ("src_graphics/hall_expansion_data.inc",
"src_graphics/cabaret_data.inc","src_graphics/hall_layout.inc",
"src_graphics/fortune_room_data.inc","src_graphics/fortune_dialog_data.inc",
"src_graphics/observer_data.inc")) + "\n.data\n" + npc
"src_graphics/observer_data.inc","src_graphics/rooftop_data.inc")) + "\n.data\n" + npc
darts = read("src_graphics/darts_data.inc").replace(
"include src_graphics\\darts_art.inc", read("src_graphics/darts_art.inc"))
data += "\n" + re.sub(r"^\w+ PROTO[^\n]*\n", "", darts, flags=re.M)
@ -134,7 +136,8 @@ def prepare():
# Win32 prototypes are not declarations in the helper object.
for name,args in re.findall(r"^(\w+)\s+PROTO([^\n]*)",all_source,re.M):
protos.setdefault(name,args.count(":DWORD"))
protos.update({"ExitProcess":1,"WriteFile":5,"GetTickCount":0})
protos.update({"ExitProcess":1,"WriteFile":5,"GetTickCount":0,"SetUnhandledExceptionFilter":1,
"CreateDirectoryA":2})
header = ".386\n.model flat\noption casemap:none\n" + "\n".join(structures+ordered+externs)
header += "\noption language:stdcall\n" + "\n".join(
name+" PROTO"+(" "+",".join([":DWORD"]*size) if size else "")
@ -144,6 +147,64 @@ def prepare():
return own_funcs
def check_rooftop_navigation():
for name,dimensions in (("panorama",(512,512,4)),("materials",(256,256,8)),
("details",(256,256,15)),("city",(512,512,22)),
("night",(512,512,4)),("rail-hands",(640,320,32))):
raw = (ROOT/f"assets/animations/rooftop/{name}-v1.bin").read_bytes()
magic,w,h,frames,payload = struct.unpack_from("<4s4I",raw)
assert magic == b"ANM1" and (w,h,frames) == dimensions
assert len(raw) == payload+20 and payload == w*h*frames*4
navigation = (OUT/"roof-navigation.bin").read_bytes()
width,height,ox,oy,step,count = struct.unpack_from("<6I",navigation)
assert count == 13
xs = struct.unpack_from(f"<{count}I",navigation,24)
ys = struct.unpack_from(f"<{count}I",navigation,24+count*4)
kinds = struct.unpack_from(f"<{count}I",navigation,24+count*8)
cells = navigation[24+count*12:]
assert len(cells) == width*height
def nav_point(x,y):
return round((x*65536-ox)/step),round((y*65536-oy)/step)
start = nav_point(24.5,2.5)
assert cells[start[1]*width+start[0]], "lift cabin must be walkable when open"
reached,pending = {start},[start]
while pending:
x,y = pending.pop()
for a,b in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)):
if 0 <= a < width and 0 <= b < height and cells[b*width+a] and (a,b) not in reached:
reached.add((a,b))
pending.append((a,b))
assert len(reached) > 1500
for i,(sx,sy) in enumerate(zip(xs,ys)):
assert any((ox+x*step-sx)**2+(oy+y*step-sy)**2 < (1.8*65536)**2
for x,y in reached),(i,"seat has no reachable approach")
for point in ((15.5,6.75),(27,18),(13,17)):
assert nav_point(*point) in reached,(point,"blocked rooftop route")
assert list(kinds).count(0) == 3 and list(kinds).count(1) == 4
city = json.loads((ROOT/"assets/sprites/rooftop/district-map-v1.json").read_text())
assert city["street_level"] == -60
assert len(city["parks"]) >= 6 and len(city["towers"]) >= 8
assert len(city["buildings"]) > 400
waterfront = city["waterfront_buildings"]
assert len(waterfront) == 60
assert all(b in city["buildings"] for b in waterfront)
assert all(b[3] <= 54 or b[2] >= 82 for b in waterfront), "canal must stay open"
for bank in (0,1):
row = [b for b in waterfront if (b[3] < 60) == (bank == 0)]
assert len(row) == 30 and min(b[0] for b in row) < -450 and max(b[1] for b in row) >= 68
assert len(city["canal_quay_parts"]) > 100
heights = {round(b[4]+60,1) for b in city["buildings"]}
assert len(heights) >= 35 and min(heights) <= 6 and max(heights) >= 150
assert len({(round(b[1]-b[0],1),round(b[3]-b[2],1)) for b in city["buildings"]}) >= 15
assert {b[5] for b in city["buildings"]} >= {186,187,188,199,200,201,202,203,204}
for x0,x1,y0,y1,top,material,kind in city["buildings"]:
assert not (x0 < 29.1 and x1 > 10.9 and y0 < 21.1 and y1 > .9)
if max(abs((x0+x1)/2-20),abs((y0+y1)/2-11)) < 60:
assert top <= -19,"a neighbour must not block the sixty-metre-high terrace"
print(f"PASS: sixty-metre rooftop, {len(heights)} distinct city heights, streets/courtyards, parks, bridges and distant towers")
print(f"PASS: dense rooftop, {count} seats, {len(reached)} connected native walk points, lift/bar return and camera restore")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--parity",action="store_true")
@ -151,6 +212,7 @@ def main():
parser.add_argument("--prove-seam-regression",action="store_true")
parser.add_argument("--prove-distance-regression",action="store_true")
parser.add_argument("--prove-bar-camera-regression",action="store_true")
parser.add_argument("--prove-fate-anchor-regression",action="store_true")
args = parser.parse_args()
if args.parity:
native,shipping = Coff(OUT/"module.obj"),Coff(ROOT/"build/graphics/poker.obj")
@ -162,10 +224,25 @@ def main():
print("PASS: all tested navigation, collision, hall/upper rendering, guests and cabaret procedures match the shipping assembly")
return
prepare()
walk = read("casino3d.inc").split("check_walk_mode:",1)[1].split("check_slots_mode:",1)[0]
assert walk.index("je walk_world_ready") < walk.index("call RenderCeilingAndFloor")
assert walk.index("walk_world_ready:") > walk.index("call RenderSprites")
room = proc(read("src_graphics/fortune_room.inc"), "RenderFortuneFurnishings3D")
assert "DrawCityObject3D" not in room, "tarot furniture must not face the camera"
assert room.count("DrawFortuneFixedProp3D") == 24
stem = "module"
if args.prove_fate_anchor_regression:
stem = "module-fate-anchor-regression"
source = (OUT/"module.asm").read_text(encoding="cp1251")
before = proc(source,"ResolveFortuneOrb3D")
broken = before.replace("mov fortuneOrbCenterX3D,eax", "mov fortuneOrbCenterX3D,278")
broken = broken.replace("mov fortuneOrbCenterY3D,eax", "mov fortuneOrbCenterY3D,259")
assert broken != before
source = source.replace(before,broken)
source = source.replace("include tools/tests/hall_expansion_test.inc",
expand("tools/tests/hall_expansion_test.inc"))
source = source.replace("build\\hall-expansion\\", "build\\hall-expansion\\anchor-regression-")
(OUT/(stem+".asm")).write_text(source,encoding="cp1251")
if args.prove_bar_camera_regression:
stem = "module-camera-regression"
source = (OUT/"module.asm").read_text(encoding="cp1251")
@ -199,7 +276,9 @@ def main():
"invoke ExpansionPortalSnapshot,OFFSET expansionUpEntry\n invoke ExitProcess,0")
(OUT/(stem+".asm")).write_text(source,encoding="cp1251")
masm = Path("F:/MASM32/masm32")
subprocess.run([str(masm/"bin/ml.exe"),"/c","/coff","/Cp","/nologo","/I.",
modern_ml = Path("C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.51.36231/bin/Hostx64/x86/ml.exe")
assembler = modern_ml if modern_ml.is_file() else masm/"bin/ml.exe"
subprocess.run([str(assembler),"/c","/coff","/Cp","/nologo","/I.",
f"/Fobuild/hall-expansion/{stem}.obj",f"build/hall-expansion/{stem}.asm"],cwd=ROOT,check=True)
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
subprocess.run([str(masm/"bin/link.exe"),"/nologo","/SUBSYSTEM:CONSOLE","/ENTRY:ExpansionTestStart@0",
@ -207,6 +286,10 @@ def main():
f"build/hall-expansion/{stem}.obj","build/hall-expansion/helpers.obj",
"kernel32.lib","user32.lib","gdi32.lib","winmm.lib","ws2_32.lib"],cwd=ROOT,check=True)
result = subprocess.run([str(OUT/(stem+".exe"))],cwd=ROOT,timeout=360)
if args.prove_fate_anchor_regression:
assert result.returncode == 81,result.returncode
print("PASS: restoring fixed screen coordinates fails the native crystal-ball tracking assertions")
return
if args.prove_bar_camera_regression:
assert result.returncode == 70,result.returncode
print("PASS: removing camera recovery fails the native bar regression")
@ -220,6 +303,8 @@ def main():
print("PASS: restoring eight-metre cutoff fails the native far-doorway assertion")
return
assert result.returncode == 0,f"native case {result.returncode}"
if not args.prove_seam_regression:
check_rooftop_navigation()
closed = Image.open(OUT/"fortune-closed.bmp").tobytes()
opened = Image.open(OUT/"fortune-open.bmp").tobytes()
assert closed != opened, "secret panel must visibly reveal the stairwell"
@ -246,6 +331,37 @@ def main():
assert len({im.crop((190,285,475,398)).tobytes() for im in frames}) >= 7
question = frames[0].crop((18,400,620,444)).tobytes()
assert all(im.crop((18,400,620,444)).tobytes() == question for im in frames)
fate_motion = [Image.open(OUT/f"fortune-fate-motion-{i:02}.bmp").convert("RGB")
for i in range(32)]
assert len({im.crop((134,32,414,323)).tobytes() for im in fate_motion}) >= 24
fate_motion[0].save(OUT/"fortune-fate-mist.gif", save_all=True,
append_images=fate_motion[1:], duration=32, loop=0)
fate_result = Image.open(OUT/"fortune-fate-result.bmp").convert("RGB")
fate_form = Image.open(OUT/"fortune-fate-form.bmp").convert("RGB")
assert fate_form.crop((16,298,624,446)).tobytes() != fate_result.crop((16,298,624,446)).tobytes()
# Difference captures isolate the actual numeral, independently of fog highlights.
tracking = (ROOT/"assets/sprites/fortune/orb-tracking-v1.json").read_text()
anchors = json.loads(tracking)["frames"]
tracked = []
for i, anchor in enumerate(anchors):
result = Image.open(OUT/f"fortune-fate-track-{i:02}.bmp").convert("RGB")
base = Image.open(OUT/f"fortune-fate-base-{i:02}.bmp").convert("RGB")
box = ImageChops.difference(result, base).getbbox()
assert box, (i, "no numeral rendered")
cx = 214 + int(anchor["cx"]*128/192 + .5)
cy = 195 + int(anchor["cy"]*128/192 + .5)
assert abs((box[0]+box[2])/2-cx) <= 2.5, (i, box, cx)
assert abs((box[1]+box[3])/2-cy) <= 2.5, (i, box, cy)
assert cx-42 <= box[0] < box[2] <= cx+42
assert cy-28 <= box[1] < box[3] <= cy+28
tracked.append(result)
assert max(a["cx"] for a in anchors)-min(a["cx"] for a in anchors) > 8
assert max(a["cy"] for a in anchors)-min(a["cy"] for a in anchors) > 8
tracked[0].save(OUT/"fortune-fate-tracked.gif", save_all=True,
append_images=tracked[1:], duration=64, loop=0)
numerals = (ROOT/"assets/animations/fortune/orb-numerals-v1.bin").read_bytes()
assert struct.unpack_from("<4s4I", numerals) == (b"ANM1", 32, 48, 10, 32*48*10*4)
assert len(set(numerals[23::4])) > 50, "font must retain soft edges and glow alpha"
for name in ("room","shuffle","deal","gesture","orb-hands","orb-mist","cards"):
path = ROOT/f"assets/animations/fortune/{name}-v1.bin"
data = path.read_bytes()
@ -270,8 +386,54 @@ def main():
assert dominant < 0.5,"stair entrance must show textured continuation on both sides, not a solid rear-wall block"
for path in OUT.glob("*.bmp"):
image = Image.open(path).convert("RGB")
assert image.size == (640,480) and len(image.getcolors(640*480)) > 100
expected = (1280,960) if path.name.startswith("roof-hd-") else (640,480)
assert image.size == expected and len(image.getcolors(image.width*image.height)) > 100
image.save(path.with_suffix(".png"))
hd_day = Image.open(OUT/"roof-hd-south.bmp").convert("RGB")
hd_night = Image.open(OUT/"roof-hd-night.bmp").convert("RGB")
assert ImageChops.difference(hd_day,hd_night).getbbox(), "day/night must change actual native geometry lighting"
white_ground = sum(1 for rgb in hd_night.crop((0,650,1280,960)).get_flattened_data() if min(rgb) > 235)
assert white_ground < 500, "uninitialised emission textures must not turn ground white"
traffic = Image.open(OUT/"roof-hd-traffic.bmp").convert("RGB")
east = Image.open(OUT/"roof-hd-east.bmp").convert("RGB")
assert ImageChops.difference(east,traffic).getbbox(), "native clock must move city vehicles"
left = Image.open(OUT/"roof-hd-look-left.bmp").convert("RGB")
right = Image.open(OUT/"roof-hd-look-right.bmp").convert("RGB")
assert left.tobytes() != east.tobytes() != right.tobytes(), "leaning must allow actual head turns"
mesh = json.loads((ROOT/"assets/sprites/rooftop/mesh-v1.json").read_text())
assert mesh["triangles"]["scene"] < 48000, "driver-safe ground must stay within the optimised mesh budget"
assert mesh["removed_invisible_triangles"] > 18000
packed_mesh = (ROOT/"assets/animations/rooftop/mesh-city-v1.bin").read_bytes()
water_tiles = 0
for at in range(12,len(packed_mesh),100):
mat,*values = struct.unpack_from("<I24f",packed_mesh,at)
if mat == 198:
water_tiles += 1
if mat not in (162,197,198,208,209):
continue
vertices = [values[i:i+8] for i in (0,8,16)]
if not all(-62.1 <= v[1] <= -59.7 and v[4] > .99 for v in vertices):
continue
xs,zs = [v[0] for v in vertices],[v[2] for v in vertices]
if max(xs) > -480 and min(xs) < 480 and max(zs) > -480 and min(zs) < 480:
assert max(xs)-min(xs) <= 64.01 and max(zs)-min(zs) <= 64.01, "giant vertex-fog ground polygon"
assert water_tiles >= 80, "animated water must reuse the tessellated scene mesh"
fast_fog = Image.open(OUT/"roof-hd-vertex-fog.bmp").convert("RGB")
nice_fog = Image.open(OUT/"roof-hd-canal-quays.bmp").convert("RGB")
assert max(ImageStat.Stat(ImageChops.difference(fast_fog,nice_fog)).mean) < 20
for profile in ("eco", "auto"):
low = Image.open(OUT/f"roof-hd-{profile}.bmp").convert("RGB")
difference = ImageStat.Stat(ImageChops.difference(low, east)).mean
assert max(difference) < 12, (profile, "quality must not remove scene geometry", difference)
roads = mesh["road_rectangles"]
for i,(a,b,c,d) in enumerate(roads):
for x0,x1,y0,y1 in roads[i+1:]:
assert min(b,x1)-max(a,x0) < 1e-6 or min(d,y1)-max(c,y0) < 1e-6, "coplanar street overlap"
millis,frames,cpu,gpu,present = struct.unpack("<5I",(OUT/"roof-hd-timing.bin").read_bytes())
print(f"Roof passes ms/frame: CPU {cpu/frames:.2f}, GPU/readback {gpu/frames:.2f}, composite {present/frames:.2f}")
print(f"PASS: non-overlapping roads, head turns with anchored hands, final offscreen composite; {millis/frames:.2f} ms/frame in 24-frame native roof probe")
print("PASS: native GL city at 640/960/1280, auto downgrade and F8, direct foreground composite")
print("PASS: sunset/night, animated traffic, software roads/water even with missing city textures")
gait = [Image.open(OUT/f"gait-{i}.bmp").convert("RGB") for i in range(8)]
assert len({frame.crop((245,360,410,480)).tobytes() for frame in gait}) == 8, "legs must visibly change in native renders"
gait[0].save(OUT/"gait-native.gif",save_all=True,append_images=gait[1:],duration=110,loop=0)

View file

@ -64,11 +64,11 @@ def test_code_structure():
# Check constants
assert "MODE_JACKBOX equ 28" in data_inc
assert "INTERACT_JACKBOX equ 48" in data_inc
assert "INTERACT_JACKBOX equ 52" in data_inc
assert "JACKBOX_X equ FP_1*306/10" in data_inc
assert "JACKBOX_Y equ FP_1*235/10" in data_inc
assert "MATERIAL_JACKBOX_SIDE equ 121" in data_inc
assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc
assert "MATERIAL_JACKBOX_BACK equ 213" in data_inc
# Check procs
procs = [

View file

@ -71,6 +71,51 @@ dtVectorCount3D equ 67
dtTestTriples3D dd 0
dtTestMetricPath db "build\darts\aim-metrics.bin",0
.code
DartsTestReticle PROC uses ebx esi edi
mov edi,OFFSET screenBuffer
mov ecx,640*480
mov eax,00E0E0E0h
rep stosd
invoke DrawDartsReticle3D,320,214
mov esi,OFFSET screenBuffer
mov ecx,640*480
xor ebx,ebx
dt_test_reticle_light:
lodsd
cmp eax,00E0E0E0h
je @F
cmp eax,00152021h
jne dt_test_reticle_fail
inc ebx
@@:
loop dt_test_reticle_light
cmp ebx,9
jne dt_test_reticle_fail
mov edi,OFFSET screenBuffer
mov ecx,640*480
mov eax,00101010h
rep stosd
invoke DrawDartsReticle3D,0,32
mov esi,OFFSET screenBuffer
mov ecx,640*480
xor ebx,ebx
dt_test_reticle_dark:
lodsd
cmp eax,00101010h
je @F
cmp eax,00F6E6ADh
jne dt_test_reticle_fail
inc ebx
@@:
loop dt_test_reticle_dark
cmp ebx,5
jne dt_test_reticle_fail
ret
dt_test_reticle_fail:
invoke ExitProcess,8506
ret
DartsTestReticle ENDP
DartsTestWriteMetrics PROC uses ebx
local written:DWORD
invoke CreateFileA,OFFSET dtTestMetricPath,40000000h,0,0,2,80h,0
@ -88,6 +133,7 @@ DartsTestWriteMetrics ENDP
DartsTestStart PROC uses ebx esi edi
local legs:DWORD
call InitPaths3D
call DartsTestReticle
lea esi,dtVectors3D
mov ebx,dtVectorCount3D
dt_test_vector:

View file

@ -1,6 +1,18 @@
.data
fortuneSessionBank dd 0
fortuneSessionPlayer dd 0
fortuneFateSavedResult dd 0
fortuneFateSavedSeed dd 0
fortuneFateTestText db "Ivan",0,"Petrov",0,"29022000",0,"IS-42",0
fortuneFateFormShot db "build\hall-expansion\fortune-fate-form.bmp",0
fortuneFateResultShot db "build\hall-expansion\fortune-fate-result.bmp",0
fortuneFateHundredShot db "build\hall-expansion\fortune-fate-hundred.bmp",0
fortuneFateMotion db "build\hall-expansion\fortune-fate-motion-"
fortuneFateDigits db "00.bmp",0
fortuneFateTracking db "build\hall-expansion\fortune-fate-track-"
fortuneFateTrackingDigits db "00.bmp",0
fortuneFateTrackingBase db "build\hall-expansion\fortune-fate-base-"
fortuneFateTrackingBaseDigits db "00.bmp",0
fortuneSessionMotion db "build\hall-expansion\fortune-session-motion-"
fortuneSessionDigits db "00.bmp",0
fortuneSessionQuestion db 193,243,228,229,242,32,235,232,32,231,224,226,242,240,224,32,228,238,230,228,252,63,0
@ -43,6 +55,8 @@ FortuneDialogNativeTests PROC uses ebx esi edi
jne fortune_session_fail
cmp fortuneSessionCards3D.frames,45
jne fortune_session_fail
cmp fortuneSessionNumerals3D.frames,10
jne fortune_session_fail
cmp fortuneProcess3D,0
jne fortune_session_fail
cmp fortuneRead3D,0
@ -181,7 +195,7 @@ fortune_session_unique_next:
ja fortune_session_fail
cmp fortuneSpeechHeight3D,140
ja fortune_session_fail
; Return to selection, then display the neutral, non-grading orb sequence.
; The fictional crystal-ball lottery requires a form, not a neural worker.
mov mouseX3D,200
mov mouseY3D,232
mov mouseClick3D,1
@ -191,6 +205,126 @@ fortune_session_unique_next:
invoke WindowEvents3D,0,102h,'2',0
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne fortune_session_fail
cmp fortunePhase3D,0
jne fortune_session_fail
call SubmitFortuneFate3D
cmp fortunePhase3D,0
jne fortune_session_fail
cmp fortuneFateField3D,0
jne fortune_session_fail
; CP1251, control filtering and maximum field widths use the real input path.
mov ebx,40
fortune_fate_name_limit:
invoke WindowEvents3D,0,102h,200,0
dec ebx
jnz fortune_fate_name_limit
cmp fortuneFateLengths3D,30
jne fortune_session_fail
cmp fortuneFateFields3D[30],0
jne fortune_session_fail
invoke FortuneChar3D,127
cmp fortuneFateLengths3D,30
jne fortune_session_fail
call ResetFortuneFate3D
mov esi,OFFSET fortuneFateTestText
xor ebx,ebx
fortune_fate_input_fields:
movzx eax,BYTE PTR [esi]
inc esi
test eax,eax
jz fortune_fate_input_next
invoke WindowEvents3D,0,102h,eax,0
jmp fortune_fate_input_fields
fortune_fate_input_next:
invoke WindowEvents3D,0,102h,9,0
inc ebx
cmp ebx,4
jb fortune_fate_input_fields
cmp fortuneFateLengths3D[8],10
jne fortune_session_fail
call ValidateFortuneBirth3D
cmp eax,1
jne fortune_session_fail
mov fortuneFateFields3D[137],'1'
call ValidateFortuneBirth3D
test eax,eax
jnz fortune_session_fail
call SubmitFortuneFate3D
cmp fortuneFateField3D,2
jne fortune_session_fail
cmp fortunePhase3D,0
jne fortune_session_fail
mov fortuneFateFields3D[137],'0'
mov fortuneFateField3D,4
invoke FortuneChar3D,'x'
cmp fortuneFateLengths3D[16],1
jne fortune_session_fail
; A value larger than 100 is rejected, not clamped or used as an outcome.
mov fortuneFateFields3D[320],'1'
mov fortuneFateFields3D[321],'0'
mov fortuneFateFields3D[322],'1'
mov fortuneFateLengths3D[20],3
call SubmitFortuneFate3D
cmp fortunePhase3D,0
jne fortune_session_fail
mov fortuneFateFields3D[320],'8'
mov fortuneFateFields3D[321],0
mov fortuneFateFields3D[322],0
mov fortuneFateLengths3D[20],1
call SubmitFortuneFate3D
cmp fortunePhase3D,0
jne fortune_session_fail
cmp fortuneFateField3D,5
jne fortune_session_fail
mov fortuneFateFields3D[320],'1'
mov fortuneFateFields3D[321],'0'
mov fortuneFateLengths3D[20],2
invoke SetFortuneSpeech3D,OFFSET fortuneFateReady3D
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneFateFormShot
; Mouse focus and backspace obey per-field limits and reject control bytes.
mov mouseX3D,200
mov mouseY3D,168
mov mouseClick3D,1
call HandleFortuneDialog3D
cmp fortuneFateField3D,1
jne fortune_session_fail
invoke FortuneChar3D,8
cmp fortuneFateLengths3D[4],5
jne fortune_session_fail
invoke FortuneChar3D,'v'
invoke WindowEvents3D,0,100h,0Dh,0
invoke WindowEvents3D,0,102h,0Dh,0
call HandleFortuneDialog3D
invoke WindowEvents3D,0,101h,0Dh,0
cmp fortunePhase3D,1
jne fortune_session_fail
mov eax,fortuneFateResult3D
cmp eax,9
jb fortune_session_fail
cmp eax,10
ja fortune_session_fail
mov fortuneFateSavedResult,eax
mov eax,rngSeed3D
mov fortuneFateSavedSeed,eax
call SubmitFortuneFate3D
invoke FortuneChar3D,'X'
cmp fortuneFateLengths3D[4],6
jne fortune_session_fail
mov eax,fortuneFateSavedResult
cmp fortuneFateResult3D,eax
jne fortune_session_fail
mov eax,rngSeed3D
cmp eax,fortuneFateSavedSeed
jne fortune_session_fail
; Again cannot bypass the ritual or reroll while hands are moving.
mov mouseX3D,120
mov mouseY3D,232
mov mouseClick3D,1
call HandleFortuneDialog3D
cmp fortunePhase3D,1
jne fortune_session_fail
mov fortunePhaseTime3D,700
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionShot13
@ -202,6 +336,27 @@ fortune_session_unique_next:
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionShot14
xor edi,edi
fortune_fate_motion:
mov eax,edi
xor edx,edx
mov ecx,10
div ecx
add al,'0'
add dl,'0'
mov fortuneFateDigits[0],al
mov fortuneFateDigits[1],dl
mov eax,edi
imul eax,32
mov fortunePhaseTime3D,eax
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneFateMotion
mov eax,fortuneFateSavedResult
cmp fortuneFateResult3D,eax
jne fortune_session_fail
inc edi
cmp edi,32
jb fortune_fate_motion
mov fortunePhaseTime3D,5000
call FortuneSessionStepTest
cmp fortunePhase3D,3
@ -209,6 +364,102 @@ fortune_session_unique_next:
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionShot15
mov fortunePhaseTime3D,1200
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneFateResultShot
xor edi,edi
fortune_fate_tracking_frames:
mov eax,edi
xor edx,edx
mov ecx,10
div ecx
add al,'0'
add dl,'0'
mov fortuneFateTrackingDigits[0],al
mov fortuneFateTrackingDigits[1],dl
mov fortuneFateTrackingBaseDigits[0],al
mov fortuneFateTrackingBaseDigits[1],dl
mov eax,edi
shl eax,6
add eax,2048
mov fortunePhaseTime3D,eax
call RenderFortuneDialog3D
cmp fortuneOrbFrame3D,edi
jne fortune_session_fail
mov eax,fortuneOrbAnchorX3D[edi*4]
add eax,128
shr eax,8
add eax,214
cmp fortuneOrbCenterX3D,eax
jne fortune_session_fail
mov eax,fortuneOrbAnchorY3D[edi*4]
add eax,128
shr eax,8
add eax,195
cmp fortuneOrbCenterY3D,eax
jne fortune_session_fail
cmp animOpacity3D,255
jne fortune_session_fail
invoke ExpansionSnapshot,OFFSET fortuneFateTracking
mov fortuneFateResult3D,-1
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneFateTrackingBase
mov eax,fortuneFateSavedResult
mov fortuneFateResult3D,eax
inc edi
cmp edi,16
jb fortune_fate_tracking_frames
mov mouseX3D,120
mov mouseY3D,232
mov mouseClick3D,1
call HandleFortuneDialog3D
cmp fortunePhase3D,0
jne fortune_session_fail
cmp fortuneFateResult3D,-1
jne fortune_session_fail
cmp fortuneFateLengths3D[4],6
jne fortune_session_fail
; Test the full inclusive range, zero, fixed outcomes and both endpoints.
mov ebx,400
fortune_fate_range_samples:
mov fortunePhase3D,0
mov fortuneFateFields3D[256],'0'
mov fortuneFateLengths3D[16],1
mov fortuneFateFields3D[320],'1'
mov fortuneFateFields3D[321],'0'
mov fortuneFateFields3D[322],'0'
mov fortuneFateFields3D[323],0
mov fortuneFateLengths3D[20],3
call SubmitFortuneFate3D
cmp fortuneFateResult3D,100
ja fortune_session_fail
dec ebx
jnz fortune_fate_range_samples
; Endpoints are checked separately without relying on a random sample.
mov fortunePhase3D,0
mov fortuneFateFields3D[256],'0'
mov fortuneFateFields3D[320],'0'
mov fortuneFateFields3D[321],0
mov fortuneFateLengths3D[20],1
call SubmitFortuneFate3D
cmp fortuneFateResult3D,0
jne fortune_session_fail
mov fortunePhase3D,0
mov fortuneFateFields3D[256],'1'
mov fortuneFateFields3D[257],'0'
mov fortuneFateFields3D[258],'0'
mov fortuneFateLengths3D[16],3
mov fortuneFateFields3D[320],'1'
mov fortuneFateFields3D[321],'0'
mov fortuneFateFields3D[322],'0'
mov fortuneFateLengths3D[20],3
call SubmitFortuneFate3D
cmp fortuneFateResult3D,100
jne fortune_session_fail
mov fortunePhase3D,3
mov fortunePhaseTime3D,1200
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneFateHundredShot
; Exit in the middle of an animation as well as after completion.
mov mouseX3D,260
mov mouseY3D,232
@ -218,6 +469,12 @@ fortune_session_unique_next:
jne fortune_session_fail
cmp fortuneSessionRoom3D.pixels,0
jne fortune_session_fail
cmp fortuneSessionNumerals3D.pixels,0
jne fortune_session_fail
cmp fortuneFateLengths3D,0
jne fortune_session_fail
cmp fortuneFateFields3D,0
jne fortune_session_fail
call BeginFortuneDialog3D
invoke FortuneChar3D,'1'
invoke FortuneChar3D,'a'

View file

@ -1,3 +1,4 @@
RoofTestCrashFilter PROTO :DWORD
.data
expansionStage dd 1
expansionOcclusionPixel dd 0
@ -91,10 +92,13 @@ ExpansionSnapshot ENDP
ExpansionFrame PROC
call UpdateCamera3D
cmp hallLevel3D,HALL_ROOF
je expansion_world_ready
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderSprites
expansion_world_ready:
call RenderHall3D
ret
ExpansionFrame ENDP
@ -130,6 +134,7 @@ expansion_door_tick:
ExpansionDoorTicks ENDP
ExpansionTestStart PROC
invoke SetUnhandledExceptionFilter,OFFSET RoofTestCrashFilter
call InitPaths3D
call LoadTextures3D
call InitializeCasinoSprites3D
@ -1164,6 +1169,7 @@ expansion_leaf_shut:
call BarCameraNativeTests
call FortuneDialogNativeTests
call ExpansionDartsTests
call RoofNativeTests
invoke ExitProcess,0
expansion_fail:
invoke ExitProcess,expansionStage
@ -1173,3 +1179,4 @@ include tools/tests/fortune_room_test.inc
include tools/tests/bar_camera_test.inc
include tools/tests/fortune_dialog_test.inc
include tools/tests/darts_hall_test.inc
include tools/tests/rooftop_test.inc

View file

@ -0,0 +1,788 @@
ROOF_NAV_W equ 70
ROOF_NAV_H equ 78
.data
roofTestHall db "build\hall-expansion\roof-lift-hall.bmp",0
roofTestOpening db "build\hall-expansion\roof-lift-opening.bmp",0
roofTestCabin db "build\hall-expansion\roof-lift-cabin.bmp",0
roofTestEntry db "build\hall-expansion\roof-arrival.bmp",0
roofTestSouth db "build\hall-expansion\roof-terrace.bmp",0
roofTestEast db "build\hall-expansion\roof-city-east.bmp",0
roofTestWest db "build\hall-expansion\roof-city-west.bmp",0
roofTestSeated db "build\hall-expansion\roof-seated.bmp",0
roofTestBar db "build\hall-expansion\roof-bar.bmp",0
roofTestMix db "build\hall-expansion\roof-bar-mixing.bmp",0
roofTestReturn db "build\hall-expansion\roof-return.bmp",0
roofTestChairSide db "build\hall-expansion\roof-chair-side.bmp",0
roofTestChairBack db "build\hall-expansion\roof-chair-back.bmp",0
roofTestPlanter db "build\hall-expansion\roof-planter.bmp",0
roofTestLean db "build\hall-expansion\roof-lean.bmp",0
roofTestNight db "build\hall-expansion\roof-night.bmp",0
roofTestHDEast db "build\hall-expansion\roof-hd-east.bmp",0
roofTestHDSouth db "build\hall-expansion\roof-hd-south.bmp",0
roofTestHDNight db "build\hall-expansion\roof-hd-night.bmp",0
roofTestHDComposite db "build\hall-expansion\roof-hd-terrace.bmp",0
roofTestHDMotion db "build\hall-expansion\roof-hd-traffic.bmp",0
roofTestHDLeft db "build\hall-expansion\roof-hd-look-left.bmp",0
roofTestHDRight db "build\hall-expansion\roof-hd-look-right.bmp",0
roofTestHDQuay db "build\hall-expansion\roof-hd-canal-quays.bmp",0
roofTestHDQuayNight db "build\hall-expansion\roof-hd-canal-night.bmp",0
roofTestHDFastFog db "build\hall-expansion\roof-hd-vertex-fog.bmp",0
roofTestHDWest db "build\hall-expansion\roof-hd-west.bmp",0
roofTestHDEco db "build\hall-expansion\roof-hd-eco.bmp",0
roofTestHDAuto db "build\hall-expansion\roof-hd-auto.bmp",0
roofTestSoftware db "build\hall-expansion\roof-software-ground.bmp",0
roofTestMissingArt db "build\hall-expansion\roof-missing-art-ground.bmp",0
roofTestHDTiming db "build\hall-expansion\roof-hd-timing.bin",0
roofTestDriver db "build\hall-expansion\roof-driver.txt",0
roofTestNewline db 13,10
roofTestHDMillis dd 0,24,0,0,0
roofTestHDBmp db 42h,4Dh
dd 54+ROOF_HD_W*ROOF_HD_H*4
dw 0,0
dd 54,40,ROOF_HD_W,-ROOF_HD_H
dw 1,32
dd 0,ROOF_HD_W*ROOF_HD_H*4,0,0,0,0
roofTestHDBMI dd 40,ROOF_HD_W,-ROOF_HD_H
dw 1,32
dd 0,ROOF_HD_W*ROOF_HD_H*4,0,0,0,0
roofTestHDDC dd 0
roofTestHDBitmap dd 0
roofTestHDPixels dd 0
roofTestHDOld dd 0
roofTestLeanMove db "build\hall-expansion\roof-lean-motion.bin",0
roofTestGuestX dd 0
roofTestFloorColor dd 0
roofTestBank dd 0
roofTestX dd 0
roofTestY dd 0
roofTestCrash db "build\hall-expansion\roof-crash.bin",0
roofTestNavPath db "build\hall-expansion\roof-navigation.bin",0
roofTestNavHeader dd ROOF_NAV_W,ROOF_NAV_H,FP_1*45/4,FP_1*5/4,FP_1/4,ROOF_SEAT_COUNT
.data?
roofTestWalkPixels db ROOF_NAV_W*ROOF_NAV_H dup(?)
.code
RoofTestHDSnapshot PROC uses ebx path:DWORD
local count:DWORD
call UpdateCamera3D
call RenderRoof3D
invoke PresentRoofHD3D,roofTestHDDC,0,0,ROOF_HD_W,ROOF_HD_H
test eax,eax
jz roof_test_hd_capture_failed
invoke GdiFlush
invoke CreateFileA,path,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET roofTestHDBmp,54,ADDR count,0
invoke WriteFile,ebx,roofTestHDPixels,ROOF_HD_W*ROOF_HD_H*4,ADDR count,0
invoke CloseHandle,ebx
ret
roof_test_hd_capture_failed:
invoke ExitProcess,95
ret
RoofTestHDSnapshot ENDP
RoofTestHD PROC uses ebx esi
local started:DWORD,written:DWORD,driverPtr:DWORD,driverLen:DWORD
mov expansionStage,94
mov roofQualityMode3D,2
call InitRoofHD3D
test eax,eax
jnz roof_test_hd_ready
mov eax,roofHDStage3D
add eax,100
invoke ExitProcess,eax
roof_test_hd_ready:
invoke CreateFileA,OFFSET roofTestDriver,40000000h,0,0,2,80h,0
mov ebx,eax
mov esi,1F00h
roof_test_driver_string:
invoke glGetString,esi
mov driverPtr,eax
xor ecx,ecx
roof_test_driver_length:
cmp BYTE PTR [eax+ecx],0
je roof_test_driver_write
inc ecx
jmp roof_test_driver_length
roof_test_driver_write:
mov driverLen,ecx
invoke WriteFile,ebx,driverPtr,driverLen,ADDR written,0
invoke WriteFile,ebx,OFFSET roofTestNewline,2,ADDR written,0
inc esi
cmp esi,1F02h
jbe roof_test_driver_string
invoke CloseHandle,ebx
invoke CreateCompatibleDC,0
mov roofTestHDDC,eax
invoke CreateDIBSection,eax,OFFSET roofTestHDBMI,0,OFFSET roofTestHDPixels,0,0
mov roofTestHDBitmap,eax
invoke SelectObject,roofTestHDDC,eax
mov roofTestHDOld,eax
mov hallLevel3D,HALL_ROOF
mov engineMode,MODE_WALK
mov worldMap3D,OFFSET roofMap3D
mov floorTexture3D,ROOF_DECK
mov playerX,FP_1*275/10
mov playerY,10*FP_1
mov playerAngle,0
mov roofLean3D,2
mov roofLeanAngle3D,0
mov roofLeanTime3D,1100
mov roofNight3D,0
mov keys3D[41h],1
mov deltaTime3D,16
add animationClock3D,16
call UpdateRoof3D
cmp roofLean3D,2
jne roof_test_hd_failed
cmp playerAngle,0
je roof_test_hd_failed
mov keys3D[41h],0
mov playerAngle,0
invoke RoofTestHDSnapshot,OFFSET roofTestHDEast
add animationClock3D,1600
invoke RoofTestHDSnapshot,OFFSET roofTestHDMotion
mov playerAngle,464
invoke RoofTestHDSnapshot,OFFSET roofTestHDLeft
cmp roofLean3D,2
jne roof_test_hd_failed
cmp playerAngle,464
jne roof_test_hd_failed
cmp roofHandsContactX3D,320
jle roof_test_hd_failed
mov playerAngle,48
invoke RoofTestHDSnapshot,OFFSET roofTestHDRight
cmp playerAngle,48
jne roof_test_hd_failed
cmp roofHandsContactX3D,320
jge roof_test_hd_failed
mov playerAngle,180
call UpdateRoofLook3D
cmp playerAngle,104
jne roof_test_hd_failed
call RenderRoof3D
invoke PresentRoofHD3D,roofTestHDDC,0,0,ROOF_HD_W,ROOF_HD_H
test eax,eax
jz roof_test_hd_failed
mov playerAngle,408
call RenderRoof3D
invoke PresentRoofHD3D,roofTestHDDC,0,0,ROOF_HD_W,ROOF_HD_H
test eax,eax
jz roof_test_hd_failed
mov roofLean3D,0
mov playerX,20*FP_1
mov playerY,20*FP_1
mov playerAngle,128
invoke RoofTestHDSnapshot,OFFSET roofTestHDSouth
mov roofNight3D,1
invoke RoofTestHDSnapshot,OFFSET roofTestHDNight
mov roofNight3D,0
mov playerX,12*FP_1
mov playerY,10*FP_1
mov playerAngle,224
invoke RoofTestHDSnapshot,OFFSET roofTestHDQuay
mov roofGLFogHint3D,1101h
mov roofHDRenderClock3D,-1
invoke RoofTestHDSnapshot,OFFSET roofTestHDFastFog
mov roofGLFogHint3D,1102h
mov roofHDRenderClock3D,-1
mov roofNight3D,1
invoke RoofTestHDSnapshot,OFFSET roofTestHDQuayNight
mov roofNight3D,0
mov playerAngle,256
invoke RoofTestHDSnapshot,OFFSET roofTestHDWest
mov playerX,20*FP_1
mov playerY,7*FP_1
mov playerAngle,128
invoke RoofTestHDSnapshot,OFFSET roofTestHDComposite
invoke GetTickCount
mov started,eax
mov esi,24
roof_test_hd_frame_time:
add animationClock3D,16
call RenderRoof3D
invoke PresentRoofHD3D,roofTestHDDC,0,0,ROOF_HD_W,ROOF_HD_H
test eax,eax
jz roof_test_hd_failed
dec esi
jnz roof_test_hd_frame_time
invoke GdiFlush
invoke GetTickCount
sub eax,started
mov roofTestHDMillis,eax
invoke GetTickCount
mov started,eax
mov esi,24
roof_test_cpu_only_time:
call RenderRoof3D
dec esi
jnz roof_test_cpu_only_time
invoke GetTickCount
sub eax,started
mov roofTestHDMillis[8],eax
invoke GetTickCount
mov started,eax
mov esi,24
roof_test_gpu_only_time:
add animationClock3D,33
call RenderRoofHD3D
dec esi
jnz roof_test_gpu_only_time
invoke GetTickCount
sub eax,started
mov roofTestHDMillis[12],eax
invoke GetTickCount
mov started,eax
mov esi,24
roof_test_present_only_time:
invoke PresentRoofHD3D,roofTestHDDC,0,0,ROOF_HD_W,ROOF_HD_H
dec esi
jnz roof_test_present_only_time
invoke GdiFlush
invoke GetTickCount
sub eax,started
mov roofTestHDMillis[16],eax
invoke CreateFileA,OFFSET roofTestHDTiming,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET roofTestHDMillis,20,ADDR written,0
invoke CloseHandle,ebx
; Quality profiles retain the full scene and a correctly aligned foreground.
mov playerX,FP_1*275/10
mov playerY,10*FP_1
mov playerAngle,0
mov roofLean3D,2
mov roofLeanAngle3D,0
mov roofQualityMode3D,1
invoke RoofTestHDSnapshot,OFFSET roofTestHDEco
cmp roofRenderW3D,640
jne roof_test_hd_failed
cmp roofRenderH3D,480
jne roof_test_hd_failed
mov roofQualityMode3D,0
mov roofAutoWidth3D,960
invoke RoofTestHDSnapshot,OFFSET roofTestHDAuto
cmp roofRenderW3D,960
jne roof_test_hd_failed
mov deltaTime3D,100
mov roofQualitySlow3D,0
mov esi,11
roof_test_auto_slow:
add animationClock3D,100
call UpdateRoofQuality3D
dec esi
jnz roof_test_auto_slow
cmp roofRenderW3D,640
jne roof_test_hd_failed
call ClearKeys3D
invoke WindowEvents3D,0,100h,77h,0
add animationClock3D,16
call UpdateRoofQuality3D
cmp roofQualityMode3D,1
jne roof_test_hd_failed
call ClearKeys3D
invoke WindowEvents3D,0,100h,77h,0
add animationClock3D,16
call UpdateRoofQuality3D
cmp roofQualityMode3D,2
jne roof_test_hd_failed
cmp roofRenderW3D,1280
jne roof_test_hd_failed
call ClearKeys3D
mov roofLean3D,0
invoke SelectObject,roofTestHDDC,roofTestHDOld
invoke DeleteObject,roofTestHDBitmap
invoke DeleteDC,roofTestHDDC
call FreeRoofHD3D
cmp roofHDWindow3D,0
jne roof_test_hd_failed
cmp roofHDBuffer3D,0
jne roof_test_hd_failed
cmp roofHDRC3D,0
jne roof_test_hd_failed
cmp roofHDMeshScene3D[12],0
jne roof_test_hd_failed
cmp roofHDFinalDC3D,0
jne roof_test_hd_failed
cmp roofHDFinalBmp3D,0
jne roof_test_hd_failed
; Capability failure must return to software, without repeatedly opening contexts.
mov roofHDReady3D,-1
invoke PresentRoofHD3D,0,0,0,1280,960
test eax,eax
jnz roof_test_hd_failed
cmp roofHDReady3D,-1
jne roof_test_hd_failed
; Software roads and water remain visible even if the city pack is missing.
mov deltaTime3D,16
mov roofQualityMode3D,0
mov roofAutoWidth3D,960
call UpdateCamera3D
call RenderRoof3D
cmp roofGroundRoadPixels3D,0
je roof_test_hd_failed
cmp roofGroundWaterPixels3D,0
je roof_test_hd_failed
invoke ExpansionSnapshot,OFFSET roofTestSoftware
mov eax,roofCityArt3D.pixels
push eax
mov roofCityArt3D.pixels,0
call RenderRoof3D
cmp roofGroundRoadPixels3D,0
je roof_test_hd_failed
cmp roofGroundWaterPixels3D,0
je roof_test_hd_failed
invoke ExpansionSnapshot,OFFSET roofTestMissingArt
pop eax
mov roofCityArt3D.pixels,eax
mov roofHDReady3D,0
ret
roof_test_hd_failed:
invoke ExitProcess,94
ret
RoofTestHD ENDP
RoofTestNavigation PROC uses ebx esi edi
local px:DWORD,py:DWORD,written:DWORD
lea edi,roofTestWalkPixels
xor esi,esi
roof_test_nav_row:
mov eax,esi
imul eax,FP_1/4
add eax,FP_1*5/4
mov py,eax
xor ebx,ebx
roof_test_nav_column:
mov eax,ebx
imul eax,FP_1/4
add eax,FP_1*45/4
mov px,eax
invoke CanWalk3D,px,py
mov [edi],al
inc edi
inc ebx
cmp ebx,ROOF_NAV_W
jb roof_test_nav_column
inc esi
cmp esi,ROOF_NAV_H
jb roof_test_nav_row
invoke CreateFileA,OFFSET roofTestNavPath,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET roofTestNavHeader,24,ADDR written,0
invoke WriteFile,ebx,OFFSET roofSeatX3D,ROOF_SEAT_COUNT*4,ADDR written,0
invoke WriteFile,ebx,OFFSET roofSeatY3D,ROOF_SEAT_COUNT*4,ADDR written,0
invoke WriteFile,ebx,OFFSET roofSeatKind3D,ROOF_SEAT_COUNT*4,ADDR written,0
invoke WriteFile,ebx,OFFSET roofTestWalkPixels,ROOF_NAV_W*ROOF_NAV_H,ADDR written,0
invoke CloseHandle,ebx
ret
RoofTestNavigation ENDP
RoofTestCrashFilter PROC uses ebx esi pointers:DWORD
local written:DWORD
mov esi,pointers
mov esi,[esi+4]
invoke CreateFileA,OFFSET roofTestCrash,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,esi,204,ADDR written,0
invoke CloseHandle,ebx
invoke ExitProcess,expansionStage
mov eax,1
ret
RoofTestCrashFilter ENDP
RoofTestTicks PROC uses esi count:DWORD
mov esi,count
roof_test_tick:
mov deltaTime3D,16
add animationClock3D,16
call UpdateInteriorTravel3D
call UpdateInteriorEye3D
dec esi
jnz roof_test_tick
ret
RoofTestTicks ENDP
RoofNativeTests PROC uses ebx esi edi
invoke SetUnhandledExceptionFilter,OFFSET RoofTestCrashFilter
mov expansionStage,90
mov eax,bankroll
mov roofTestBank,eax
call InitRoof3D
call LoadRoofAssets3D
cmp roofSkyArt3D.imageWidth,512
jne roof_test_fail
cmp roofSkyArt3D.frames,4
jne roof_test_fail
cmp roofDetailArt3D.frames,15
jne roof_test_fail
cmp roofHandsArt3D.frames,32
jne roof_test_fail
cmp roofCityArt3D.frames,22
jne roof_test_fail
cmp roofNightArt3D.frames,4
jne roof_test_fail
xor esi,esi
roof_test_surface_theme:
mov roofNight3D,esi
mov ebx,ROOF_DECK
roof_test_surface:
invoke ResolveRoofSurface3D,ebx
cmp eax,65535
jbe roof_test_fail
mov edx,256
cmp ebx,ROOF_FACADE
jb @F
mov edx,512
@@:
cmp ebx,ROOF_INDICATOR
jne @F
mov edx,64
@@:
cmp ebx,ROOF_LIGHT
jne @F
mov edx,1
@@:
cmp ecx,edx
jne roof_test_fail
inc ebx
cmp ebx,ROOF_LAST_MATERIAL
jbe roof_test_surface
inc esi
cmp esi,2
jb roof_test_surface_theme
mov roofNight3D,0
invoke CityRayPlane3D,-512*FP_1,24*FP_1,405
cmp eax,7FFFFFFFh
jne roof_test_fail
mov engineMode,MODE_WALK
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov floorTexture3D,100
mov playerX,LIFT_CENTRE_X
mov playerY,FP_1*63/10
mov playerAngle,384
call UpdateCamera3D
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestHall
mov playerY,FP_1*48/10
call UpdateCamera3D
invoke CanWalk3D,LIFT_CENTRE_X,FP_1*34/10
test eax,eax
jnz roof_test_fail
call CheckInteractions
cmp interactTargetId,INTERACT_LIFT_CALL
jne roof_test_fail
call TriggerHallInteraction3D
cmp liftPhase3D,1
jne roof_test_fail
invoke RoofTestTicks,32
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestOpening
invoke RoofTestTicks,36
cmp liftDoor3D,1000
jne roof_test_fail
invoke CanWalk3D,LIFT_CENTRE_X,FP_1*34/10
test eax,eax
jz roof_test_fail
; Walk through the opened doorway using the actual normal movement handler.
mov keys3D[57h],1
mov esi,120
roof_test_walk_in:
call ExpansionWalkFrame
cmp playerY,FP_1*26/10
jb roof_test_inside
dec esi
jnz roof_test_walk_in
jmp roof_test_fail
roof_test_inside:
call ClearKeys3D
mov eax,playerX
mov roofTestX,eax
mov eax,playerY
mov roofTestY,eax
cmp hallLevel3D,0
jne roof_test_fail
invoke WindowEvents3D,0,100h,32h,0
invoke RoofTestTicks,1
cmp roofNight3D,1
jne roof_test_fail
call ClearKeys3D
invoke WindowEvents3D,0,100h,31h,0
invoke RoofTestTicks,1
cmp roofNight3D,0
jne roof_test_fail
call ClearKeys3D
call CheckInteractions
cmp interactTargetId,INTERACT_LIFT_TRAVEL
jne roof_test_fail
call TriggerHallInteraction3D
cmp liftPhase3D,3
jne roof_test_fail
invoke CanWalk3D,LIFT_CENTRE_X,2*FP_1
test eax,eax
jnz roof_test_fail
invoke RoofTestTicks,68
cmp liftPhase3D,4
jne roof_test_fail
cmp hallLevel3D,0
jne roof_test_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestCabin
invoke RoofTestTicks,270
cmp hallLevel3D,HALL_ROOF
jne roof_test_fail
cmp worldMap3D,OFFSET roofMap3D
jne roof_test_fail
cmp liftPhase3D,2
jne roof_test_fail
mov eax,roofTestX
cmp playerX,eax
jne roof_test_fail
mov eax,roofTestY
cmp playerY,eax
jne roof_test_fail
mov playerAngle,128
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestEntry
mov keys3D[57h],1
mov esi,120
roof_test_walk_out:
call ExpansionWalkFrame
cmp playerY,FP_1*46/10
ja roof_test_outside
dec esi
jnz roof_test_walk_out
jmp roof_test_fail
roof_test_outside:
call ClearKeys3D
call RoofTestNavigation
mov expansionStage,91
invoke CanWalk3D,29*FP_1,10*FP_1
test eax,eax
jnz roof_test_fail
invoke CanWalk3D,15*FP_1,FP_1*45/10
test eax,eax
jnz roof_test_fail
invoke CanWalk3D,FP_1*145/10,10*FP_1
test eax,eax
jnz roof_test_fail
invoke CanWalk3D,FP_1*145/10,FP_1*77/10
test eax,eax
jnz roof_test_fail
invoke CanWalk3D,12*FP_1,FP_1*68/10
test eax,eax
jnz roof_test_fail
mov playerX,20*FP_1
mov playerY,FP_1*66/10
mov playerAngle,128
call UpdateCamera3D
call UpdateRoofEye3D
call RenderCeilingAndFloor
mov eax,screenBuffer[(460*640+320)*4]
mov roofTestFloorColor,eax
call ExpansionFrame
mov eax,roofTestFloorColor
cmp screenBuffer[(460*640+320)*4],eax
jne roof_test_fail
cmp cityDepthBuffer3D[(460*640+320)*4],4*FP_1
ja roof_test_fail
cmp playerX,20*FP_1
jne roof_test_fail
cmp playerY,FP_1*66/10
jne roof_test_fail
invoke ExpansionSnapshot,OFFSET roofTestSouth
mov playerAngle,0
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestEast
mov playerAngle,256
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestWest
mov playerX,FP_1*145/10
mov playerY,FP_1*84/10
mov playerAngle,128
call UpdateCamera3D
call CheckInteractions
cmp interactTargetId,INTERACT_ROOF_SEAT
jne roof_test_fail
call TriggerHallInteraction3D
call UpdateInteriorEye3D
cmp roofSeat3D,1
jne roof_test_fail
cmp viewEye3D,ROOF_SEATED_EYE
jne roof_test_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestSeated
mov keys3D[57h],1
invoke RoofTestTicks,1
cmp roofSeat3D,0
jne roof_test_fail
cmp playerY,FP_1*84/10
jne roof_test_fail
call ClearKeys3D
xor ebx,ebx
roof_test_all_seats:
mov playerX,20*FP_1
mov playerY,7*FP_1
mov playerAngle,128
mov roofTargetSeat3D,ebx
mov interactTargetId,INTERACT_ROOF_SEAT
call TryRoofInteraction3D
lea eax,[ebx+1]
cmp roofSeat3D,eax
jne roof_test_fail
call UpdateRoofEye3D
mov eax,ROOF_SEATED_EYE
cmp roofSeatKind3D[ebx*4],2
jb @F
mov eax,ROOF_STANDING_EYE
@@:
cmp viewEye3D,eax
jne roof_test_fail
mov eax,384
cmp roofSeatKind3D[ebx*4],3
jne @F
mov eax,256
@@:
cmp playerAngle,eax
jne roof_test_fail
mov interactTargetId,INTERACT_ROOF_SEAT
call TryRoofInteraction3D
cmp roofSeat3D,0
jne roof_test_fail
cmp playerX,20*FP_1
jne roof_test_fail
cmp playerY,7*FP_1
jne roof_test_fail
cmp playerAngle,128
jne roof_test_fail
call UpdateRoofEye3D
inc ebx
cmp ebx,ROOF_SEAT_COUNT
jb roof_test_all_seats
mov expansionStage,92
mov playerX,20*FP_1
mov playerY,7*FP_1
mov engineMode,MODE_BAR
mov barPhase3D,0
mov esi,30
roof_test_bar_repeat:
call RenderBar3D
cmp playerX,20*FP_1
jne roof_test_fail
cmp playerY,7*FP_1
jne roof_test_fail
cmp viewEye3D,ROOF_STANDING_EYE
jne roof_test_fail
dec esi
jnz roof_test_bar_repeat
invoke ExpansionSnapshot,OFFSET roofTestBar
mov barPhase3D,1
mov barSelection3D,1
mov barTime3D,1500
call RenderBar3D
invoke ExpansionSnapshot,OFFSET roofTestMix
mov engineMode,MODE_WALK
mov barPhase3D,0
; Scale and full furniture volumes must survive side and back inspection.
mov expansionStage,93
mov playerX,FP_1*191/10
mov playerY,10*FP_1
mov playerAngle,256
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestChairSide
mov playerX,FP_1*177/10
mov playerY,FP_1*116/10
mov playerAngle,384
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestChairBack
mov playerX,FP_1*167/10
mov playerY,FP_1*197/10
mov playerAngle,256
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestPlanter
; The native gait advances position while every step respects roof collision.
mov eax,roofGuestX3D[0]
mov roofTestGuestX,eax
mov roofGuestWait3D[0],0
mov roofGuestDir3D[0],0
invoke RoofTestTicks,20
mov eax,roofGuestX3D[0]
cmp eax,roofTestGuestX
je roof_test_fail
cmp roofGuestMoving3D[0],1
jne roof_test_fail
mov playerX,FP_1*275/10
mov playerY,10*FP_1
mov playerAngle,0
call UpdateCamera3D
call CheckRoofInteractions3D
cmp interactTargetId,INTERACT_ROOF_RAIL
jne roof_test_fail
call TryRoofInteraction3D
cmp roofLean3D,1
jne roof_test_fail
invoke RoofTestTicks,75
cmp roofLean3D,2
jne roof_test_fail
cmp roofLeanTime3D,1100
jne roof_test_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestLean
invoke CanWalk3D,27*FP_1,10*FP_1
test eax,eax
jnz roof_test_fail
mov roofNight3D,1
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestNight
mov roofNight3D,0
mov interactTargetId,INTERACT_ROOF_RAIL
call TryRoofInteraction3D
invoke RoofTestTicks,40
cmp roofLean3D,0
jne roof_test_fail
cmp playerX,FP_1*275/10
jne roof_test_fail
cmp playerY,10*FP_1
jne roof_test_fail
cmp viewEye3D,ROOF_STANDING_EYE
jne roof_test_fail
mov expansionStage,92
invoke RoofTestTicks,250
cmp liftDoor3D,0
jne roof_test_fail
mov playerX,LIFT_CENTRE_X
mov playerY,FP_1*48/10
mov playerAngle,384
call UpdateCamera3D
call CheckInteractions
call TriggerHallInteraction3D
invoke RoofTestTicks,70
mov playerY,FP_1*24/10
call CheckInteractions
cmp interactTargetId,INTERACT_LIFT_TRAVEL
jne roof_test_fail
call TriggerHallInteraction3D
invoke RoofTestTicks,340
cmp hallLevel3D,0
jne roof_test_fail
cmp worldMap3D,OFFSET casinoMap
jne roof_test_fail
mov playerY,FP_1*48/10
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET roofTestReturn
mov eax,bankroll
cmp eax,roofTestBank
jne roof_test_fail
call FreeRoofAssets3D
cmp roofSkyArt3D.pixels,0
jne roof_test_fail
cmp roofMaterialArt3D.pixels,0
jne roof_test_fail
cmp roofDetailArt3D.pixels,0
jne roof_test_fail
call LoadRoofAssets3D
call RoofTestHD
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
ret
roof_test_fail:
invoke ExitProcess,expansionStage
ret
RoofNativeTests ENDP

View file

@ -5,7 +5,10 @@ Push-Location $root
try {
& 'C:\Python314\python.exe' tools\tests\prepare_billiards_module.py
if ($LASTEXITCODE -ne 0) { throw 'Billiards helper preparation failed' }
& "$MasmRoot\bin\ml.exe" /c /coff /Cp /nologo /I. /Fobuild\billiards\module.obj tools\tests\billiards_module.asm
$assembler = "$MasmRoot\bin\ml.exe"
$modernAssembler = 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.51.36231\bin\Hostx64\x86\ml.exe'
if (Test-Path -LiteralPath $modernAssembler) { $assembler = $modernAssembler }
& $assembler /c /coff /Cp /nologo /I. /Fobuild\billiards\module.obj tools\tests\billiards_module.asm
if ($LASTEXITCODE -ne 0) { throw 'Billiards module assembly failed' }
$sdk = Get-ChildItem -LiteralPath "${env:ProgramFiles(x86)}\Windows Kits\10\Lib" -Directory |
Sort-Object Name -Descending |