Compare commits

...
Author SHA1 Message Date
Brues 8ae69fd144 fix(model): scene terrain review fixes and shared view volume
SweepCollider in a scene with no WMO and no terrain returned the feet
again instead of feet + move: SweepGround copied the feet into the end
before its ground check. With no ground the body now moves the full
distance, as the doc says; while a WMO loads the feet still stay put.

Water::Update made the scene's ripple pool with a zero clock, so a
ripple added in the frame the camera first entered water died on the
next age. It now goes through Ensure, which sets the scene's clock.

Trace read 0x008029D4 under a second name, VAR_TRACE_RUN_EPSILON; it
now reads VAR_TWO_POW_MINUS_22 and the duplicate is gone.

The paint builds its view volume once and hands it to every pass that
built its own: the camera probe (0x006809E2), the portal walk for the
sky, the WMO draw and the terrain's chunk, magma and water passes. The
sky's draw (0x006D4940) only saves and restores the viewport, and
FrustumStore (0x00682D90) only copies from the volume, so one volume
serves the whole paint.

The SFile wrapper is one class (model/SceneFile.h) for the WMO and the
terrain readers. The terrain's decal and liquid box tests take their
chunk rectangle and shifted box from ChunkWindow, and AreaAt its row
and chunk from Cell and ChunkAt, in the same rounding as before.
2026-11-12 02:27:33 -06:00
Brues 454be4ce49 feat(model): foam rings and wakes of actors in shallow water
An actor whose movement is on now leaves the world's ripples in shallow
liquid: foam rings while it stands, turns or splashes, wakes along its
motion while it moves.

The world keeps its ripples in one pool of 128 records (0x50 bytes) that
its liquid query fills and its liquid pass draws. A scene keeps a pool
of its own with the same records (model/SceneWater.cpp), and the steps
are transcribed:

- CGUnit_C::EmitWaterRipple (0x005FA760), each actor update (as
  0x0060802E) and after the swim decision's splash (kind 0xC9, as
  0x0060319F): with the unit's liquid and bit 0x40 (flag 0x1 of the
  area's LiquidType, kept now by the placement's liquid leg), the depth
  under twice the collision height (at least 1.0) and the next time
  (+0xC78) reached. The kind (moving 0xF, turning 0x30, still, splash)
  and the table 0x00860A4C {0, 0, 1, 0} make a wake or a ring; the size,
  lifetime and growth come from three draws of the client random stream
  and the function's own constants, scaled down past half the depth and
  for a still unit (0x0080C474); a ring takes a fourth draw for its
  heading, a wake the motion's; the next time follows the speed, or
  400 ms and a draw's share of 50.
- AddSplash's immediate leg (0x0068BDA0, gated by waterRipples
  0x0086A03C and mode 0x00C89DB8), AddRipple (0x0068F9F0: the active
  mover's range 0 to 31) and EmitRipple (0x0068F5C0): the record, its
  rise and decay at 0.4 of the life (0x00810348), and the liquid
  triangles TestWorldBox finds in its box with class 0xF0000, here
  World::LiquidTriangles over the WMO and the scene's terrain.
- UpdateRipples (0x0068FAE0), its ageing in the scene's update and its
  draw in the scene's liquid phase: blend 3, fog, depth write, culling
  and lighting off, the depth bias footstepBias x 1/1024, the world
  stack at Translate(-eye), per category its texture (0x00C800E4) over
  format 4 vertices with the alpha's byte over white and the uv through
  BuildDecalTransforms (0x006D6FA0) of the box turned by the heading.

The scene's terrain box test (TestTerrainBox) now has TestChunkBox's
liquid leg (0x006AD75E): GatherLiquidCellFacets on each slot the class
names, into the same batches. The shadow decal's class asks for liquid
too, so actor shadows lie on terrain water as in the world.

The scene's clock times the ripples, so a paused scene holds them.
2026-11-12 01:58:47 -06:00
Brues 717d75cd33 feat(model): the drift motes around a scene camera in a liquid
A scene's camera in water, ocean or magma now sees the world's drift
cloud: the motes that float around the camera under a liquid.

The world builds one cloud at start-up (0x0066F971: 0xFA40 bytes,
CreateDriftCloud 0x0068E5A0 with the size base 1/36, the edge 30.0 and
Textures\WaterPoop02.blp at 0x00867A34) and drives it from its own
camera. A scene now owns a cloud made the same way the first time its
camera enters a liquid, and runs the world's steps from its camera:

- CameraEnvironment's change leg (0x00680AEE): on a change to water or
  ocean the scatter (0x0068E720) in the new liquid's mode and the size
  base 1/36 (0x0068E670), 1/9 for magma, and the enable byte +0xFA14;
  slime clears it.
- The world frame's advect (0x0066FEE0 to 0x0068E930), with the scene's
  eye staged in the camera position (0x00C7CF20) and its frame time in
  the frame seconds (0x00C62510) the advect reads.
- The world stage's draw (0x006701E0 to 0x0068EFE0) after the models,
  in the device's view with the world transform identity.

Both run while the camera is in a liquid and world flag 0x2000000 (the
waterParticulates setting) is set. The cloud's texture is closed with
0x0068E640 when the scene goes. The module is model/SceneWater.cpp.

The world transform stack id 8 moves from two local constants to
Offsets.h (GX_XFORM_WORLD).
2026-11-12 01:40:12 -06:00
Brues d4af7c4c6a feat(model): the map's sky over a scene's terrain without a WMO
A scene with terrain and no WMO now clears and draws its frame as the
world's does in the open, in place of a depth-only clear.

DrawWorldScene (0x0068123F) clears to black while the camera is dry and
to the fog colour (+0x70) under a liquid, and draws the sky only when
dry and the weather byte is set; outside every map object that byte
always stands set. The sky pass the WMO scenes already run (the dome
colours from the camera's heading, the map's light from BuildMapLight
with its sun disc and skybox slots, DrawSky, the world's objects put
back after) now takes the map, the camera's liquid and the portal
walk's skybox weight instead of the backdrop, so ClearAndDrawOpenSky
runs it for the terrain's map with no skybox from a walk. Under a
liquid the frame's fog is already the map's for it (LiquidFog), so the
clear takes that colour.
2026-11-12 00:54:25 -06:00
Brues 1b267039a6 feat(model): swimming, liquid queries and the camera under terrain water
The scene's terrain liquid now answers the queries the world asks of
its own, so actors swim in it, GetLiquidAtPoint finds it, the camera
goes under it, and the splashes play.

Point query: GetLiquidStatus (0x0069B6D0) finds its chunk through the
global area table, so its terrain leg is transcribed over the scene's
chunks: u and v at 0.24 cells a yard (0x0081073C), stored and rounded
less 0.5; each slot's tile byte (+0x10) gives type & 3; the ocean is
height 0 under 0.01, else the MCLQ heights (+0x0C) bilinear at 53
bits, stored, the point under it plus 0.01. The scene asks the WMO
first and the terrain after, as GetLiquidStatus asks the map objects
first; GetLiquidAtPoint and the actor's swim step use it.

Swimmer's facets: GatherChunkFacets (0x006AADC0) under class 0x30000
adds an impassable chunk's walls (they ignore the class) and, for
each slot the class names, GatherLiquidFacets (0x006AB4E0): the facet
batches reset, GatherLiquidCellFacets (0x0068CEC0, called on the
scene's own slots) with the chunk-relative box and clipped cell
rectangle, and the batches emitted. The doodad leg takes no def for
this class. The box-to-cells step and the chunk window are shared
with the solid query; ResetFacetBatches and EmitFacetBatches leave
SceneWorld's anonymous namespace for it.

Camera: CameraEnvironment (0x006809C0) asks the claimed group's
liquid, else the liquid query (0x006723D0) at the probe point: the
eye lowered to the lowest near corner of the view. The probe drop
now comes from the scene's paint, so a scene without a WMO has one.
Under a liquid the world builds its light table with that liquid's
params and takes its fog from it (BuildSceneFog 0x006CEE30: end
+0x44 capped at the far clip, start +0x48 times it, colour band 7
+0x1C); LiquidFog swaps the frame's fog for that during the paint,
and a WMO's lava or slime without its own fog targets the same. A
terrain-only scene clears to the fog colour under a liquid, as
DrawWorldScene does (0x0068124D); the paint order and the water
colours follow the camera's liquid.

Sounds: CGUnit_C::UpdateSwimState (0x006030C0) plays the race splash
(ChrRaces +0x34, kept at +0xB18 by PostInit 0x005FB880) when the depth
past 0.4 of the height (0x0080C5C0) differs from the last test's
(+0x9A4), while not swimming; the mover's port now does, and the
actor keeps its unit's splash. The $FSD footstep is wet
(IsPointAtLiquidSurface 0x0060A740) when the unit, placed outdoors,
stands in a liquid (UpdateMapEntityEnvironment 0x0069E280) whose
LiquidType for the area (0x007CCF40; area from 0x0069B120) has flag
0x2, and the event point is from 0.01 above to 2/3 below the surface.

Constants already named under other roles are reused: 0x0081073C and
0x0086A750 become VAR_POINT_CELLS_PER_YARD and VAR_POINT_HALF_CELL,
and the nameplate lift 0x0080ABFC becomes VAR_TWO_THIRDS.
2026-11-12 00:49:27 -06:00
Brues 930f969cd6 feat(model): the terrain's water and lava in a scene
A scene's terrain now shows the rivers, lakes, ocean and lava of its
chunks, drawn by the world's own liquid code.

Load: after LoadNormals, each chunk runs CMapChunk::LoadLiquids
(0x006AF760), which reads one MCLQ block (0x324 bytes) per liquid flag
of the MCNK header into the four slots at chunk +0x118 and builds each
slot's vertices. The record check now bounds the MCLQ blocks too.

Buffers: the slots take a vertex and an index buffer from two static
pools the scene makes, as CreateLiquidBufferPool (0x0068CA50) makes the
world's (81 vertices of 40 bytes, 192 indices). The world rebuilds its
own pool, which would leave the scene's slots on freed buffers;
CChunkLiquid::Draw (0x0068DE40) re-acquires only for a usage-2 pool.
Each pair is marked unfilled with 0x0058A130, the call
AcquireLiquidBuffers (0x0068CC60) makes, so the first draw fills the
vertices and the strip.

Draw: the three passes are copied in the world's order. Magma
(DrawMagmaPass 0x006855A0: no culling, lighting or blending) after the
WMO, as DrawWorldScene has it; the ocean (0x00685010) then the river
(0x006851B0) before the WMO's liquids, as RenderLiquid (0x006816D0)
has it. Each pass binds its swatch, then the water program
(0xC81B7C) with the white specular and the shininess (0x8102E8) and
alpha blend when fancy water (0xC9A324) is on, else the add combine
on stage 1. A slot is culled by its box (0x0068DF40: the chunk's box
with the slot's heights as z), placed at its chunk less the eye, lit
as GatherSlotLights (0x0068DF70) lights it, and drawn with the pass.
The camera, fog end, frustum and world stack are staged as for the
chunks (one Staged helper now serves all three).

Colours: the river and ocean swatches refill from the light state
while their fresh flags (0xC8117C, 0xC81B70) are clear. World::MapWater
swaps the eight water fields of the live light record (+0x34..+0x40,
+0x68..+0x74) for the scene map's at the eye; the flags are cleared
before and after the draw, so the swatches take the scene's colours
and the world's next frame refills its own. The WMO's water uses the
same swap.

Actors do not swim in this water yet, and GetLiquidAtPoint does not
find it.
2026-11-12 00:07:07 -06:00
Brues d2e52ea6dd feat(model): the terrain's grass (detail doodads) in a scene
The world's chunk pass (0x006847C9) scatters a drawn chunk's detail
doodads once, with world flag 0x100000, when its view depth +0x78 is below
the horizon 0x00867958 (read live; 300.0 in this image):
CMapChunk::CreateDetailDoodads (0x006BFC10) from the layers' ground
effects, seeded by the chunk's grid position. The detail pass (0x006813D0)
then sets stage 0's mip bias (the immediate at 0x006813EA) and, for each
such chunk with a batch +0xC0, the chunk's transform and lighting record,
then DrawDetailDoodads (0x006C02D0) with the fade ramp. The scene runs both
over its own chunks.

The batches' slot buffers come from the world's detail pools, which
PrepareDetailDoodadBuffers (0x006B1D30) frees and rebuilds while 0x00CB0034
is set (a frillDensity change), after freeing only the world's listed
chunks' batches (0x006924F0); a slot's release (0x006B21D0) reads its
buffer's pool. So a scene batch gives its slot buffers back after each draw
(the world's idle release 0x006B28E0); the scene runs Prepare itself while
the flag is set (the world's frame never does at the glue screen), and
frees its batches (0x006B2030) to scatter them again when the rebuild
stamp 0x00CB003C, written only by Prepare, moves. CreateDetailDoodads reads
512 bytes at the MCSH pointer with or without a shadow map, so a chunk
whose range lies outside its file gets none.

The chunk pass and the detail pass each get their own render state push,
as DrawChunkPass and the detail pass do: the shader path's layer draw
(DrawLayersShader 0x006BF690) leaves its pixel program bound, and with one
push around both the grass drew through it as opaque quads.
2026-11-11 23:28:20 -06:00
Brues 6422401ca7 feat(model): the terrain's own doodads (trees, plants, rocks) in a scene
A chunk's MCRF doodad references index the tile's MDDF placements, and the
world makes one CMapDoodadDef per unique id (+0x04, the hash 0x00CA7DDC).
PlaceDoodadDef (0x00694BC0) places it: position (origin - +0x10,
origin - +0x08, +0x0C), each a float sum; scale the word +0x20 x 1/1024
(0x008090D8); angles float(degrees x pi/180) with z + pi in one store;
the matrix TranslateMatrix, RotateMatrix about z, y and x (0x007BDD60),
ScaleMatrix (0x007BDD00); then CreateDoodadModel (0x00695100), the same
model setup as a WMO's doodads. LoadReferences (0x006B00F0) marks a chunk's
doodads outdoors (flag 4, intensity 1.0), and map lights only reach doodads
owned by WMO groups, so they take the sun and the zone fog: in the scene,
the frame's light and fog.

The WMO doodad code is now shared: a Doodad's WMO is optional and its sound
flag points to its owner's. World::Props holds a map's own doodads with the
same model setup, bounds, distance fade (0x00683F80), animate list, sounds
and collision facets (GatherDoodadFacets 0x006ABC40 rules, one stamp per
query). Terrain::QueryFacets adds each chunk's props after its triangles,
as GatherChunkFacets does (0x006AB496); a prop still loading fails the
query. SetPlayWorldModelSounds covers them.
2026-11-11 23:11:06 -06:00
Brues b5e97d7d98 feat(model): footsteps and the actor's ground on the scene's terrain
AttachEntityToGround (0x006A8A20) traces the terrain (TraceTerrainSegment,
mask 0x100) and the map objects down 1000 yards from the probe point; with
neither hit it probes up past the bounds; the terrain wins when it is
nearer. A terrain win links the unit outdoors (the sun, no map object
lights), and its ground id is GetGroundEffect (0x0069B1C0) at the entity's
position: the cell from the map origin (0.24 at 0x0081073C, 0.5 at
0x0086A750; distances 0 to 34133.332), no hole, then the cell's dominant
layer from the chunk's MCNK +0x40 map (the row's word masked by
0x008104B4[column] and shifted by 0x008104C4[column]), that layer's effect
id (+0x0A, 0xFFFF none) into GroundEffectTexture (0x00C0DC60 / 0x00C0DC64),
whose +0x18 is the TerrainType id. GetGroundEffect finds the chunk through
the global area table, so Terrain::GroundEffect transcribes it over the
scene's chunks.

World::Place now runs both legs (the terrain through Terrain::Trace), and
World::Settle / Ground take the backdrop (nullable) and the terrain, so a
scene with terrain and no WMO places its actors too; the cached placement
keys on the terrain's serial as well. The Gurubashi pit's layer (ground
effect 587) is TerrainType 0, Dirt.
2026-11-11 22:58:33 -06:00
Brues ca61a831f8 feat(model): shadows, selection circles, markers and reticles on terrain
The world lays a decal through its box test TestWorldBox (0x006AD2C0): map
objects into the shared triangle batches (mask 0xF00F0), then the terrain
(0xF0F00, TestTerrainBox 0x006AD4E0). The terrain leg's cells are
fistp(float(0.24 x d) - 0.5) of the distances from the map origin (0.24 at
0x00810B44, 0.5 at 0x0086AAC4), the first from the unrounded origin - min.y;
rows run from the max-x cell >> 3 to the min-x cell >> 3, columns likewise.
TestChunkBox (0x006AD610) finds each chunk through the global area table,
takes the box less the chunk position, clips the cell rectangle to
{0, 0, 7, 7} (IntersectCellRects 0x006ADBF0) and, under 0x100, calls
TestChunkTriangles (0x006AD7E0, fastcall(chunk, rect, box, unused), ret 8),
which reads only the chunk and writes one batch: its kept cell triangles,
the chunk's translation as the matrix, its vertices at +0x83C.

Terrain::CollectDecalBatches is that leg over the scene's own chunks,
calling TestChunkTriangles on each. World::DrawDecal now takes the WMO and
the terrain (either null), collects them in the world's order and draws
the batches with the existing DrawDecalBatches states. The scene's decal
helpers take the paint's ground (WMO, terrain); the flat quad is left for
a scene with neither.
2026-11-11 22:46:25 -06:00
Brues e39b87ab04 feat(model): actors walk on the scene's terrain; traces and sweeps find it
The scene's ground is now its WMO and its terrain, taken in the order the
world's queries take map objects and chunks:

- Box query (the mover's sweeps, SweepCollider): QueryGroundFacets gathers
  the WMO's facets, then the terrain's, as 0x006AA8B0 does; a WMO still
  loading fails it. The terrain leg is the gather's cell math (0x006AA92D:
  fistp(float(0.24 x float(17066.666 - c)) - 0.5), bounds against 0 and
  34133.332) and GatherChunkFacets' wall and triangle legs (0x006AAE2A..
  0x006AB3F1): AddImpassableWalls' quads (0x006AB530, the 32000 immediate
  at 0x006AB59C), the hole bits (0x008103F8), the five-vertex outcodes
  against the box grown by 0.019444443 (0x00810468, 0x008101B8) and the
  four cell triangles (0x00810438). Their planes are Contact::MakeFacet,
  whose arithmetic is the chunk gather's inline SSE path operand for
  operand. GatherChunkFacets itself finds its chunk through the global
  area table (0x00C96318), so it cannot see the scene's chunks.
- Segment trace (TraceSegment, area-kit shards): TraceGround traces the WMO,
  then lets the terrain lower the fraction, as 0x0069BFF0 does. The terrain
  leg converts the ends to cells as 0x0069C320 does and calls the client's
  four cell walks (0x0069C4E0, 0x0069C570, 0x0069C600, 0x0069C780), which
  only fill its scratch list (0x00C962C8, count 0x00C89F40); then
  TraceListedCells (0x0069C920) over the scene's chunks: the ray relative
  to each chunk, its four cell triangles (0x0081047C) through
  IntersectRayTriangle (0x007C2C40, slack immediate at 0x0069CB96), the
  nearest t*k at or above 0. The normal is PlaneFromTriangle's (as the
  terrain first hit 0x006AA600 takes it), turned toward the start.

The mover's facet context is the scene frame. World::Sweep moved to the
scene (SweepGround) so it sweeps against both. A cell outside the scene's
chunks has no ground; the world fails or ends there, but the scene's
terrain ends at its radius and a WMO may reach past it.
2026-11-11 22:33:28 -06:00
Brues fb17670e37 feat(model): scene:SetWorldTerrain, a map's ADT terrain in a ModelScene
scene:SetWorldTerrain(mapID, x, y [, radius]) shows the map's ground around
a point, in map coordinates (radius default one tile, 533.33331 at
0x0080654C); scene:ClearWorldTerrain() removes it. The Gurubashi Arena's
pit is terrain under its WMO, so SetWorldModel at the arena's MODF plus
SetWorldTerrain shows it as the world does.

The chunks are the world's CMapChunk objects, owned by the scene and built
and drawn by the world's own functions (model/SceneTerrain.cpp):

- Load: the ADT is read whole and its MHDR read as ParseAreaFile
  (0x006C2010) reads it. LoadAreaTextures (0x006C20E0) fills an area
  stand-in's texture slots. Each chunk is constructed (0x006AF1D0), gets
  CreateAreaChunk's fields (0x006C2880) and the self-contained steps of
  CMapChunk::Load (0x006AF5F0) in order: FixupRecordPointers, header
  fields, BuildVertices, LoadNormals, the impassable flag, AddLayer,
  CreateShadowTexture, CreateCombinedTexture. LinkNeighbours (global area
  table), LoadReferences (the world's doodad hashes), LinkLights and the
  area's filing are world state and left out; the scene links its own
  chunks by LinkNeighbours' rule. Each record is bounds-checked first.
- Buffers: the world's chunk pool is freed and rebuilt on a far clip change
  or map unload (CreateChunkBufferPool 0x006AE640), and only the world's
  listed chunks give theirs back; its empty-pool fallback is the device's
  shared dynamic buffer. The scene makes its own static pools the same way
  (CreatePool 0x0058A160, PoolBuffer 0x00589F80; freed by 0x0058A040 and
  GxBufferRelease), which SelectMesh never trades away (it re-acquires only
  for a usage 2 pool).
- Draw, before the WMO: DrawChunkPass (0x00684510) per chunk the scene's
  volume shows (IsBoxCulled 0x00682F40): the chunk level and view depth
  (0x006AFAD0), the world transform at the chunk less the eye, a lighting
  record at the chunk's centre with the scene's lights there, the scene's
  fog and, when the depth less twice the radius is inside the fog end, its
  light as the sun (DrawMapEntity's slots 0x006C0CC0), then SelectMesh
  (0x006BEB50). DrawLayers reads the camera (0x00C7CF20) and fog end
  (0x00C62964) globals, which hold the scene's values while it draws. The
  anisotropic mip bias is the pass's own immediate (0x00684528).
- Teardown: UnloadChunk (0x006A6B10) and ~CMapChunk, the scene's buffers,
  then each tile's texture handles (HandleClose, as UnloadArea does).
- The chunks are built again when the terrain shader flags, shadowLevel or
  map shadows change (glue screen to world).

Not yet: collision, the tile's doodads and grass, terrain liquid.
2026-11-11 22:06:51 -06:00
Brues 0963a4c38f fix(dbc): accept id 0 in DBC::Record, as the client's own lookup does
The client indexes a table's id map (store +0x08) by id and refuses only an
id above the largest id (+0x0C, signed, so negatives too) and an empty slot
(0x005F4010: test/jl, cmp/jg, null test). DBC::Record also refused id 0,
which is a real row in Map.dbc (Eastern Kingdoms), TerrainType and
ItemClass (Consumable); every other table leaves slot 0 empty, so it still
returns null there.

Effects: map 0 now has its directory, so scene:SetWorldTerrain(0, ...)
finds Eastern Kingdoms' tiles; the scene's death thud plays on TerrainType
0; ItemClass 0 resolves to "Consumable" in the item name lookups.
Instance::Info and the C_Map MapInfo reader had each copied the lookup
without the id 0 check to work around it; both now call DBC::Record.
2026-11-11 22:06:37 -06:00
20 changed files with 3883 additions and 428 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
| [Frame: ModelScene](docs/API.md#modelscene) | `CreateFrame("ModelScene")`, `<ModelScene>` (`drawLayer`, `fogNear`, `fogFar`, `<FogColor>`, `<ViewInsets>`), `:CreateActor`, `:GetNumActors`, `:GetActorAtIndex`, `:SetCameraPosition` / `:GetCameraPosition`, `:SetCameraOrientationByYawPitchRoll`, `:SetCameraOrientationByAxisVectors`, `:GetCameraForward`, `:GetCameraRight`, `:GetCameraUp`, `:SetCameraFieldOfView` / `:GetCameraFieldOfView`, `:SetCameraNearClip` / `:GetCameraNearClip`, `:SetCameraFarClip` / `:GetCameraFarClip`, `:SetViewInsets` / `:GetViewInsets`, `:SetViewTranslation` / `:GetViewTranslation`, `:Project3DPointTo2D`, `"FollowCamera"` (`:SetFollowActor` / `:GetFollowActor`), `:SetLightType` / `:GetLightType`, `:SetLightVisible` / `:IsLightVisible`, `:SetLightDirection` / `:GetLightDirection`, `:SetLightPosition` / `:GetLightPosition`, `:SetLightAmbientColor` / `:GetLightAmbientColor`, `:SetLightDiffuseColor` / `:GetLightDiffuseColor`, `:SetFogColor` / `:GetFogColor`, `:SetFogNear` / `:GetFogNear`, `:SetFogFar` / `:GetFogFar`, `:ClearFog`, `:SetDrawLayer` / `:GetDrawLayer`, `:SetPaused`, `:GetSceneTime`, `:After`, `:NewTimer`, `:NewTicker`, `:SetAllowOverlappedModels` / `:GetAllowOverlappedModels`, `:SetGroundTerrain` / `:GetGroundTerrain`, `:SetWorldModel`, `:SetWorldModelByMapID`, `:ClearWorldModel`, `:SetPlayWorldModelSounds` / `:GetPlayWorldModelSounds`, `:TraceSegment`, `:GetLiquidAtPoint`, `:SweepCollider`, `:SetSoundListenerActor` / `:GetSoundListenerActor`, `:GetActorAtCursor`, `:GetCursorRay`, `:GetActorsInRadius`, `:SetMoveMarker` / `:ClearMoveMarker` / `:GetMoveMarker`, `OnActorEnter` / `OnActorLeave` |
| [Frame: ModelScene](docs/API.md#modelscene) | `CreateFrame("ModelScene")`, `<ModelScene>` (`drawLayer`, `fogNear`, `fogFar`, `<FogColor>`, `<ViewInsets>`), `:CreateActor`, `:GetNumActors`, `:GetActorAtIndex`, `:SetCameraPosition` / `:GetCameraPosition`, `:SetCameraOrientationByYawPitchRoll`, `:SetCameraOrientationByAxisVectors`, `:GetCameraForward`, `:GetCameraRight`, `:GetCameraUp`, `:SetCameraFieldOfView` / `:GetCameraFieldOfView`, `:SetCameraNearClip` / `:GetCameraNearClip`, `:SetCameraFarClip` / `:GetCameraFarClip`, `:SetViewInsets` / `:GetViewInsets`, `:SetViewTranslation` / `:GetViewTranslation`, `:Project3DPointTo2D`, `"FollowCamera"` (`:SetFollowActor` / `:GetFollowActor`), `:SetLightType` / `:GetLightType`, `:SetLightVisible` / `:IsLightVisible`, `:SetLightDirection` / `:GetLightDirection`, `:SetLightPosition` / `:GetLightPosition`, `:SetLightAmbientColor` / `:GetLightAmbientColor`, `:SetLightDiffuseColor` / `:GetLightDiffuseColor`, `:SetFogColor` / `:GetFogColor`, `:SetFogNear` / `:GetFogNear`, `:SetFogFar` / `:GetFogFar`, `:ClearFog`, `:SetDrawLayer` / `:GetDrawLayer`, `:SetPaused`, `:GetSceneTime`, `:After`, `:NewTimer`, `:NewTicker`, `:SetAllowOverlappedModels` / `:GetAllowOverlappedModels`, `:SetGroundTerrain` / `:GetGroundTerrain`, `:SetWorldModel`, `:SetWorldModelByMapID`, `:ClearWorldModel`, `:SetWorldTerrain`, `:ClearWorldTerrain`, `:SetPlayWorldModelSounds` / `:GetPlayWorldModelSounds`, `:TraceSegment`, `:GetLiquidAtPoint`, `:SweepCollider`, `:SetSoundListenerActor` / `:GetSoundListenerActor`, `:GetActorAtCursor`, `:GetCursorRay`, `:GetActorsInRadius`, `:SetMoveMarker` / `:ClearMoveMarker` / `:GetMoveMarker`, `OnActorEnter` / `OnActorLeave` |
| [Frame: ModelSceneActor](docs/API.md#actorsetmodelbycreaturedisplayiddisplayid--useactiveplayercustomizations) | `:SetModelByCreatureDisplayID`, `:SetModelByPath`, `:SetModelByUnit`, `:SetModelByUnitCreatureDisplayID`, `:GetModelUnitGUID`, `:Dress`, `:Undress`, `:UndressSlot`, `:TryOn`, `:DressPlayerSlot`, `:IsSlotAllowed` / `:IsSlotVisible`, `:GetItemTransmogInfo`, `:GetItemTransmogInfoList`, `:SetItemTransmogInfo`, `:SetSheathed` / `:GetSheathed`, `:SetSheathedCategory`, `:UseUnitSheatheCategory`, `:SetAutoDress` / `:GetAutoDress`, `:IsGeoReady`, `:ClearModel`, `:IsLoaded`, `:GetModelPath`, `:SetPosition` / `:GetPosition`, `:SetYaw` / `:GetYaw`, `:SetPitch` / `:GetPitch`, `:SetRoll` / `:GetRoll`, `:SetScale` / `:GetScale`, `:SetUseCenterForOrigin` / `:IsUsingCenterForOrigin`, `:GetActiveBoundingBox`, `:GetMaxBoundingBox`, `:Show` / `:Hide` / `:SetShown` / `:IsShown` / `:IsVisible`, `:SetAlpha` / `:GetAlpha`, `:SetAnimation` / `:GetAnimation`, `:GetAnimationVariation`, `:SetAnimationBlendOperation` / `:GetAnimationBlendOperation`, `:SetPaused` / `:GetPaused`, `:SetSpellVisualKit` / `:GetSpellVisualKit`, `:ApplySpellVisualKit`, `:PlayAnimationKit` / `:StopAnimationKit`, `:AttachToMount`, `:DetachFromMount`, `:CalculateMountScale`, `:GetAttachmentPosition`, `:AttachToActor`, `:DetachFromActor`, `:PlaySpellVisual`, `:StopSpellVisual`, `:PlaySound`, `:SetPlayAnimationSounds` / `:GetPlayAnimationSounds`, `:SetShadowShown` / `:IsShadowShown`, `:SetHighlight` / `:IsHighlighted`, `:SetSelectionCircle` / `:ClearSelectionCircle` / `:GetSelectionCircle`, `:SetNameText` / `:ClearNameText` / `:GetNameText`, `:GetNamePosition`, `:SetMovementEnabled` / `:IsMovementEnabled`, `:StartMovingForward`, `:StartMovingBackward`, `:StopMoving`, `:StartStrafingLeft`, `:StartStrafingRight`, `:StopStrafing`, `:StartTurningLeft`, `:StartTurningRight`, `:StopTurning`, `:Jump`, `:StartPitchingUp`, `:StartPitchingDown`, `:StopPitching`, `:IsFalling`, `:IsSwimming`, `:MoveTo`, `:SetWalking` / `:IsWalking`, `:GetCameraPivotHeight`, `OnLoad`, `OnUpdate`, `OnModelLoading`, `OnModelLoaded`, `OnModelCleared`, `OnAnimFinished`, `OnSpellVisualImpact`, `OnMoveFinished`, `<Actor>` templates |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
+116 -36
View File
@@ -847,6 +847,8 @@ build instructions.
- [`scene:SetWorldModel(path [, x, y, z [, yaw [, doodadSet [, mapID]]]])`](#scenesetworldmodelpath--x-y-z--yaw--doodadset--mapid)
- [`scene:SetWorldModelByMapID(mapID)`](#scenesetworldmodelbymapidmapid)
- [`scene:ClearWorldModel()`](#sceneclearworldmodel)
- [`scene:SetWorldTerrain(mapID, x, y [, radius])`](#scenesetworldterrainmapid-x-y--radius)
- [`scene:ClearWorldTerrain()`](#sceneclearworldterrain)
- [`scene:SetPlayWorldModelSounds(enabled)` / `scene:GetPlayWorldModelSounds()`](#scenesetplayworldmodelsoundsenabled--scenegetplayworldmodelsounds)
- [`scene:TraceSegment(startX, startY, startZ, endX, endY, endZ [, camera])`](#scenetracesegmentstartx-starty-startz-endx-endy-endz--camera)
- [`scene:GetLiquidAtPoint(x, y, z)`](#scenegetliquidatpointx-y-z)
@@ -19423,11 +19425,13 @@ the sounds of their footsteps and death thuds (see
| `9` | Dusty grass |
Without `terrainTypeID`, an actor stands on the floor of the WMO of the scene
([`scene:SetWorldModel`](#scenesetworldmodelpath--x-y-z--yaw--doodadset--mapid)).
The material of the floor under the actor selects the sounds, as in the
world. An actor with no floor of a WMO under it makes no footsteps and no
death thuds. This is the default. A ground that you set replaces the floor of
the WMO for all actors. `scene:GetGroundTerrain()` returns the ID that you
([`scene:SetWorldModel`](#scenesetworldmodelpath--x-y-z--yaw--doodadset--mapid))
or on its ground
([`scene:SetWorldTerrain`](#scenesetworldterrainmapid-x-y--radius)),
whichever is nearer under it, as in the world. The material of the WMO floor,
or the main texture of the ground at that point, selects the sounds. An actor
with no floor and no ground under it makes no footsteps and no death thuds.
This is the default. A ground that you set replaces both for all actors. `scene:GetGroundTerrain()` returns the ID that you
set, or nothing. This is a ClassicAPI extension.
### `scene:SetWorldModel(path [, x, y, z [, yaw [, doodadSet [, mapID]]]])`
@@ -19455,8 +19459,11 @@ The scene shows the WMO as the world shows it:
- If the scene has no fog, the fog of the WMO comes in from the far clip of
the camera.
- When the camera goes into the water of the WMO, the fog becomes the
underwater fog of the WMO. In lava or slime, the fog becomes the fog of
the scene, unless the WMO has a fog for that liquid.
underwater fog of the WMO. Small motes drift in the liquid around the
camera, with the `waterParticulates` setting. In lava or slime, the fog becomes the fog of
the map for that liquid, unless the WMO has a fog for that liquid.
Without `mapID`, the fog of the scene takes the place of the fog of the
map.
- The WMO shows its props, for example torches, furniture and bones. The
props of doodad set `0` always show. `doodadSet` adds the props of one
more set. The props animate, and the lights of the WMO light them.
@@ -19533,6 +19540,62 @@ onyxia:SetPosition(0.7, -211.97, -86.08) -- her place in the lair
Removes the WMO of the scene and its props. This is a ClassicAPI extension.
### `scene:SetWorldTerrain(mapID, x, y [, radius])`
Shows the ground of a map around a point and returns `true`. `mapID` is a
row of `Map.dbc`, for example `0` for Eastern Kingdoms. `x` and `y` are a
position on that map, so the positions in the scene are the positions on
the map. The scene shows each part of the ground that is less than
`radius` yards from the point. The default radius is 533.33 yards, the
width of one map tile.
The ground looks as it does in the world: the same textures, the same
shadows and the same level of detail. The light of the scene stands in for
the sun, and the fog of the scene stands in for the fog of the zone. In a
scene without a WMO, the sky of the map shows behind the ground: its
colors, the sun and the moons, the stars, the clouds and the skybox, in the
light of the map at the camera and at the time of day of the world. The
sky hides the background of the scene. A WMO from
[`scene:SetWorldModel`](#scenesetworldmodelpath--x-y-z--yaw--doodadset--mapid)
stands on the ground when you give it its position on the same map.
The function returns `false` and shows no ground when the map has no
ground within the radius. A new call replaces the ground of the scene.
Actors stand, walk and fall on the ground as on the floor of a WMO (see
[`actor:SetMovementEnabled`](#actorsetmovementenabledenabled--actorismovementenabled)),
and
[`scene:TraceSegment`](#scenetracesegmentstartx-starty-startz-endx-endy-endz--camera)
and
[`scene:SweepCollider`](#scenesweepcolliderfeetx-feety-feetz-radius-height-movex-movey-movez)
find it. Past the radius there is no ground.
The props of the map on that ground, for example trees, plants and rocks,
show as in the world. They fade with distance, and actors collide with them.
They play their sounds when
[`scene:SetPlayWorldModelSounds`](#scenesetplayworldmodelsoundsenabled--scenegetplayworldmodelsounds)
is on. Near the camera, the ground shows its grass and small clutter, as
in the world, with the density of the ground clutter setting. The water and
lava of the map show, and their textures animate. The water takes its colors
from the light of the map at the camera. Actors swim in this water, and
[`scene:GetLiquidAtPoint`](#scenegetliquidatpointx-y-z) finds it. When the
camera goes into the water or the lava of the map, the fog becomes the fog
of the map for that liquid, as in the world. Small motes drift in the
liquid around the camera, with the `waterParticulates` setting. This is a ClassicAPI
extension.
```lua
-- The Gurubashi Arena: the arena WMO where the map puts it, on its ground.
scene:SetWorldModel("World\\wmo\\Azeroth\\Collidable Doodads\\Stranglethorn\\StranglethornArena\\StrangleGladiatorArena.wmo",
-13205.914, 271.832, 20.280, math.rad(64) + math.pi, 0, 0)
scene:SetWorldTerrain(0, -13205.914, 271.832, 250)
scene:SetCameraFarClip(500)
```
### `scene:ClearWorldTerrain()`
Removes the ground of the scene. This is a ClassicAPI extension.
### `scene:SetPlayWorldModelSounds(enabled)` / `scene:GetPlayWorldModelSounds()`
Sets whether the props of the WMO play their sounds, for example the
@@ -19547,10 +19610,12 @@ time later. This is a ClassicAPI extension.
### `scene:TraceSegment(startX, startY, startZ, endX, endY, endZ [, camera])`
Returns the first face of the WMO on the segment from the start to the
end. The positions are scene positions. The function tests the faces that
the line of sight of the world tests: walls, floors and roofs. It does not
test small details or the props of the WMO.
Returns the first face of the WMO or of the ground on the segment from the
start to the end. The positions are scene positions. The function tests the
faces that the line of sight of the world tests: the walls, floors and
roofs of the WMO, and the ground of
[`scene:SetWorldTerrain`](#scenesetworldterrainmapid-x-y--radius). It does
not test small details or the props of the WMO.
The function returns `hitX, hitY, hitZ, normalX, normalY, normalZ, fraction`:
@@ -19568,7 +19633,7 @@ wall.
The function returns nothing in these cases:
- The segment hits no face.
- The scene has no WMO.
- The scene has no WMO and no ground.
- The part of the WMO on the segment has not loaded.
This is a ClassicAPI extension.
@@ -19580,8 +19645,10 @@ local _, _, floorZ = scene:TraceSegment(x, y, z + 2, x, y, z - 50)
### `scene:GetLiquidAtPoint(x, y, z)`
Returns the liquid of the WMO at a scene point, as the world finds it for a
unit that stands at the point. The function returns `liquidType, surfaceZ`:
Returns the liquid of the WMO or of the terrain
([`scene:SetWorldTerrain`](#scenesetworldterrainmapid-x-y--radius)) at a
scene point, as the world finds it for a unit that stands at the point. The
liquid of the WMO comes first. The function returns `liquidType, surfaceZ`:
- `liquidType` is `0` for water, `1` for ocean, `2` for magma and `3` for
slime.
@@ -19592,16 +19659,16 @@ unit that stands at the point. The function returns `liquidType, surfaceZ`:
The function returns nothing in these cases:
- The point is above the surface, or it is not in a liquid.
- The scene has no WMO.
- The part of the WMO at the point has not loaded.
This is a ClassicAPI extension.
### `scene:SweepCollider(feetX, feetY, feetZ, radius, height, moveX, moveY, moveZ)`
Moves a body in a straight line until it touches the WMO or one of its
props. The world moves a character into a wall or onto a floor in the same
way. The body
Moves a body in a straight line until it touches the WMO, one of its
props, or the ground of
[`scene:SetWorldTerrain`](#scenesetworldterrainmapid-x-y--radius). The
world moves a character into a wall or onto a floor in the same way. The body
has the shape of a character in the world: a square box over a downward
point at the feet. `radius` is half the width of the box, and `height` is
the height of the box. `moveX, moveY, moveZ` is the full movement.
@@ -19611,8 +19678,8 @@ feet are the position where the body stops. The normal is the unit normal
of the face that the body touched. It points to the side that the face
blocks. When the body touched no face, the function returns only the feet.
A face blocks the body only from its front side. Without a WMO, the body
moves the full distance. While a part of the WMO in the way loads, the body
A face blocks the body only from its front side. Without a WMO or ground,
the body moves the full distance. While a part of the WMO in the way loads, the body
does not move. The function does not slide the body along a wall or step
it up a ledge. For that movement, use
[`actor:SetMovementEnabled`](#actorsetmovementenabledenabled--actorismovementenabled).
@@ -19723,8 +19790,10 @@ Shows the click-to-move marker at a point, in scene coordinates. It is the
same pulsing yellow mark that the world shows where you click to move. The
scene shows one marker. A new call moves it to the new point.
- In a scene with a WMO, the marker lies on the faces of the WMO near the
point. In a scene with no WMO, it lies flat at the height of the point.
- In a scene with a WMO or ground
([`scene:SetWorldTerrain`](#scenesetworldterrainmapid-x-y--radius)), the
marker lies on their faces near the point. In a scene with neither, it
lies flat at the height of the point.
- The marker pulses between 0.5 and 0.75 yards in size, two times each
second. It stops while the scene is paused.
- The marker stays until you call `ClearMoveMarker`. It does not go away
@@ -20267,9 +20336,17 @@ default. When it is `true`, the animations play these sounds:
does not play every time, and all actors together play it at most once
every 10 seconds. A copy of your own character does not play it.
- Footsteps and the death thud. These need a ground: the floor of the WMO
of the scene, or a ground that you set with
of the scene, its ground from
[`scene:SetWorldTerrain`](#scenesetworldterrainmapid-x-y--radius), or a
ground that you set with
[`scene:SetGroundTerrain`](#scenesetgroundterrainterraintypeid--scenegetgroundterrain).
Footsteps also need the `FootstepSounds` setting.
Footsteps also need the `FootstepSounds` setting. At the surface of
water, footsteps splash.
- The splash of the race of the unit, when the movement of the actor takes
it into or out of water that is deeper than two fifths of its height, or
when it starts to swim. Only an actor that shows a unit
([`actor:SetModelByUnit`](#actorsetmodelbyunitunit--sheatheweapons-autodress-hideweapons-useplayernativeform-holdbowstring-customraceid))
has a race.
- The sounds that some animations carry, with the `EmoteSounds` setting.
- During the preparation of
[`actor:PlaySpellVisual`](#actorplayspellvisualspellid--target--casttimems--channeltimems),
@@ -20295,13 +20372,14 @@ Shows or hides a blob shadow under the model. It is the same dark blob that
units have on the ground in the world. The default is off.
- In a scene with a WMO
([`scene:SetWorldModel`](#scenesetworldmodelpath--x-y-z--yaw--doodadset--mapid)),
the shadow lies on the floor of the WMO under the actor, as on the ground
in the world. It follows slopes and steps, and it fades with the height of
the actor above the floor. An actor with no floor of the WMO under it shows
no shadow.
- In a scene with no WMO, the shadow lies flat on the plane of the actor's
position, below its feet.
([`scene:SetWorldModel`](#scenesetworldmodelpath--x-y-z--yaw--doodadset--mapid))
or ground
([`scene:SetWorldTerrain`](#scenesetworldterrainmapid-x-y--radius)), the
shadow lies on the floor under the actor, as on the ground in the world.
It follows slopes and steps, and it fades with the height of the actor
above the floor. An actor with no floor under it shows no shadow.
- In a scene with no WMO and no ground, the shadow lies flat on the plane of
the actor's position, below its feet.
- The size of the shadow comes from the bounds of the model and from the
scale of the actor. The shadow turns with the yaw of the actor.
- The shadow darkens what is behind it. It fades with the alpha of the
@@ -20345,8 +20423,8 @@ Shows a selection circle under the feet of the actor. It is the same circle
that the world shows under your target. The colour values are from 0 to 1,
and `a` is 1 by default. An actor has no circle by default.
- In a scene with a WMO, the circle lies on the floor of the WMO under the
actor. In a scene with no WMO, it lies flat at the height of the actor's
- In a scene with a WMO or ground, the circle lies on the floor under the
actor. In a scene with neither, it lies flat at the height of the actor's
position.
- The size of the circle comes from the bounds of the model and from the
scale of the actor, as for a unit in the world.
@@ -20393,8 +20471,8 @@ yards. With `valid` set to `false`, it shows the reticle that the world
shows out of range. `valid` is `true` by default. An actor has no reticle
by default.
- In a scene with a WMO, the reticle lies on the floor of the WMO, up to 2
yards above or below the position of the actor. In a scene with no WMO,
- In a scene with a WMO or ground, the reticle lies on the floor, up to 2
yards above or below the position of the actor. In a scene with neither,
it lies flat at the height of the actor's position.
- An actor with no model shows its reticle too, so an empty actor can mark
a point on the ground.
@@ -20733,7 +20811,9 @@ is off. The actor moves on the WMO of the scene
- It walks and runs on the floors, and it steps up and down small ledges.
- It slides along walls and props.
- It falls where the floor ends, and it jumps.
- It swims in the liquids of the WMO.
- It swims in the liquids of the WMO and of the terrain.
- In shallow water it leaves foam rings, and wakes when it moves, as a
character does in the world, with the `waterRipples` setting.
The actor moves at the speeds of a character: it runs 7 yards a second,
walks 2.5, backs up 4.5, swims 4.72 and swims backward 2.5. It turns half a
+600 -3
View File
@@ -2067,13 +2067,14 @@ enum Offsets {
// CGUnit_C::GetNamePlateScreenPos — `bool __fastcall(unit, worldFrame,
// float screen[2])`, ret 4: the unit's name position (GetNamePosition
// 0x608640: head attachment 18, 29 mounted, else 1.25 × scale × box
// height above the feet) raised by VAR_NAMEPLATE_LIFT, projected with
// height above the feet) raised by VAR_TWO_THIRDS, projected with
// FUN_WORLDFRAME_GET_SCREEN_COORDS — where a nameplate is seated.
FUN_UNIT_GET_NAMEPLATE_SCREEN_POS = 0x0060F7F0,
// GetNamePosition's no-attachment factor (1.25) and the plate lift (2/3
// yard), both floats.
// yard), both floats; the 2/3 is also the wet footstep's depth below a
// liquid surface (0x0060A7BF).
VAR_NAME_HEIGHT_FACTOR = 0x0080C5D0,
VAR_NAMEPLATE_LIFT = 0x0080ABFC,
VAR_TWO_THIRDS = 0x0080ABFC,
// GetNamePosition's attachments (0x00608640): PlayerName on the unit's
// model, PlayerNameMounted when the unit is mounted (its model is then
// the mount's).
@@ -7003,6 +7004,10 @@ enum Offsets {
// builder (0x004799A0) reads it at 0x00479A21 for
// `Item\ObjectComponents\Head\<name>_<prefix><sex letter>.mdx`.
OFF_CHRRACES_CLIENT_PREFIX = 0x18,
// The race's splash (SoundEntries id): PostInit (0x005FB880) stores it
// for the unit's race byte (UNIT_FIELD_BYTES_0) at +0xB18, and the swim
// decision plays it (CGUnit_C::PlaySplash 0x006237C0).
OFF_CHRRACES_SPLASH_SOUND = 0x34,
// The helm builder's sex letters, `const char *[3]` = "M", "F", "U",
// indexed by the sex byte (0x00479A1A).
VAR_SEX_LETTERS = 0x00838AB4,
@@ -7088,6 +7093,25 @@ enum Offsets {
// 0x00458450): the FootstepTerrainLookup sound for the TerrainType row's
// +0x10, dry or wet, or 0 for an unknown terrain or footstep id.
FUN_GET_FOOTSTEP_SOUND = 0x00458450,
// The footstep's wet flag (CGUnit_C::PlayFootstep 0x00623390 through
// IsPointAtLiquidSurface 0x0060A740): the entity's liquid, as
// UpdateMapEntityEnvironment (0x0069E280) stores it outdoors from
// GetLiquidStatus at the entity's position (bit 0x20 of +0x90, the height
// +0x98, the type +0xF0), and bit 0x80, bit 1 of the flags (+0x8) of the
// LiquidType row FUN_LIQUIDTYPE_FOR_AREA `__fastcall(int areaID, uint
// type)` (0x007CCF40) returns: the area's override at AreaTable +0x54 +
// 4 x (type & 3), else its parent's, else row (type & 3) + 1; null for
// type 0xF. The area is the entity's (CWorld::GetEntityAreaId
// 0x006703A0): a map object's whole-WMO WMOAreaTable row (its name set,
// group -1), else the terrain's (0x0069B120: the chunk at k = 0.03
// 0x00810740, (17066.666 - x) stored, then times k and stored; y's
// product stored once; each less 0.5 and rounded; the chunk's +0x16C, 0
// without one). Wet: the event point's z under the height plus 0.01
// (VAR_ONE_HUNDREDTH) and the height less z under 2/3 (VAR_TWO_THIRDS).
FUN_LIQUIDTYPE_FOR_AREA = 0x007CCF40,
OFF_LIQUIDTYPE_FLAGS = 0x08,
LIQUIDTYPE_FLAG_SPLASH = 0x2,
VAR_AREA_CHUNKS_PER_YARD = 0x00810740,
// The death thud table: five size classes of {?, count, pairs *}, 12
// bytes each, the pair {dry, wet} indexed by TerrainTypeSounds id and
// bounded by count (PlayDeathThud 0x006236E0: `cmp ecx,[eax+0xC4E0F8]`,
@@ -14047,6 +14071,8 @@ enum Offsets {
GXRS_FOG = 0x0F,
GXRS_DEPTH_WRITE = 0x12,
GXRS_CULL = 0x14,
GXRS_BLEND_ALPHA = 2, // SRC_ALPHA / INV_SRC_ALPHA, with the alpha test
GXRS_BLEND_ADD = 3, // SRC_ALPHA / ONE (the ripple draw, 0x0068FCC1)
GXRS_BLEND_MODULATE = 4, // DST_COLOR / ZERO
GXRS_COMBINE_BLEND_DIFFUSE_ALPHA = 5, // stage: texture to the vertex colour by its alpha
// Topology for FUN_GX_PRIM_INDEXED: 4 is a triangle strip (the lightning
@@ -14273,6 +14299,15 @@ enum Offsets {
// immediate.
VAR_NO_LIGHT_FOG_END = 0x006D2301,
LIGHT_RECORD_SIZE = 0x90,
// The water fields of a light record (the light state's +0xE0 to +0xEC
// and +0x114 to +0x120 less 0xAC): the swatch fill 0x0068A830 reads ocean
// shallow, ocean deep, river shallow, river deep, then river shallow and
// deep alpha, ocean shallow and deep alpha.
OFF_LIGHT_RECORD_SHALLOW_OCEAN = 0x34,
OFF_LIGHT_RECORD_DEEP_OCEAN = 0x38,
OFF_LIGHT_RECORD_SHALLOW_RIVER = 0x3C,
OFF_LIGHT_RECORD_OCEAN_ALPHA_SHALLOW = 0x70,
OFF_LIGHT_RECORD_OCEAN_ALPHA_DEEP = 0x74,
OFF_LIGHT_RECORD_DEEP_RIVER = 0x40,
OFF_LIGHT_RECORD_RIVER_ALPHA_SHALLOW = 0x68,
OFF_LIGHT_RECORD_RIVER_ALPHA_DEEP = 0x6C,
@@ -14341,6 +14376,14 @@ enum Offsets {
OFF_SKY_DISC_COLOUR = 0x0C,
SKY_DISCS_LIT = 2, // the sun disc and the white moon take band 9
OFF_LIGHT_RECORD_SUN = 0x20,
// The scene fog BuildSceneFog (0x006CEE30) takes from the light table:
// end = scalar band 0 (+0x44), the far clip when that is no less or NaN;
// start = scalar band 1 (+0x48) x end; colour band 7 (+0x1C). The world
// builds its table with the camera's liquid (underwater, magma or slime
// params), so this is its fog under a liquid.
OFF_LIGHT_RECORD_FOG_END = 0x44,
OFF_LIGHT_RECORD_FOG_START_SCALE = 0x48,
OFF_LIGHT_RECORD_FOG_COLOR = 0x1C,
OFF_LIGHT_RECORD_SKYBOXES = 0x78, // {LightSkybox id, float weight} x 2
// The portal walk (benilla terrain/portals.md) decides the sky from
// inside. WalkPortals `__thiscall(obj, group, from, const float
@@ -14390,6 +14433,10 @@ enum Offsets {
FUN_FRUSTUM_BOX_CULLED = 0x00682F60,
FUN_GX_GET_VIEW_PROJECTION = 0x0058B0C0,
FUN_GX_XFORM_GET = 0x0058B280,
// The world transform stack (unit 8): the world's passes set each object's
// matrix here (GxXformSetIdentity(8) and the translate 0x0058B250 in the
// liquid passes, 0x00685144); its view-space passes leave it identity.
GX_XFORM_WORLD = 8,
FUN_SET_SKYBOX_SLOT = 0x006D4810,
VAR_MAPOBJ_PASS = 0x00CA8014,
VAR_PORTAL_INSIDE_LEG = 0x00CDE5AC,
@@ -16118,6 +16165,7 @@ enum Offsets {
// are .rdata except the three noted.
VAR_HALF = 0x007FFA24, // float 0.5
VAR_THREE_QUARTERS = 0x008012CC, // float 0.75; a swimmer's height share, the aim height
VAR_TWO_FIFTHS = 0x0080C5C0, // float 0.4; the splash depth's height share (0x00603150)
VAR_ONE_HUNDREDTH = 0x008029D0, // float 0.01
VAR_ONE_THIRTY_SIXTH = 0x007FF9D0, // float 1/36
VAR_MINUS_ONE_THIRTY_SIXTH = 0x007FF9C8, // float -1/36
@@ -16203,4 +16251,553 @@ enum Offsets {
// q[3], const float dir[3])`, ret 4: from p to the line through q
// along the unit dir.
FUN_POINT_TO_LINE_DISTANCE_SQ = 0x00634F40,
// ---- ADT terrain (Model::Scene::Terrain; benilla terrain/chunk, render,
// lod, cull, files; Ghidra decompiles 2026-11-11) ---------------------------
// A tile's chunks built and drawn by the world's own chunk functions on
// chunks the scene owns. CMapChunk::Load (0x006AF5F0) also links the
// global area table, the world's doodad hashes and map lights; the scene
// runs only its self-contained steps, in Load's order.
// CMapChunk::CMapChunk `__thiscall(chunk)` (0x006AF1D0; base 0x006C3020
// touches only the object) and ~CMapChunk `__thiscall(chunk)`
// (0x006AF460, its own lists only); UnloadChunk `__thiscall(chunk)`
// (0x006A6B10): layers, neighbour back-links, textures, liquids, lists;
// a buffer not from the world's pool it nulls without freeing
// (ReleaseChunkBuffers 0x006AE9A0). The world's chunk list (0x00CA7D48,
// AllocChunk 0x006A06E0) is walked only by ReleaseAllChunkBuffers,
// before the world's pool is rebuilt (far clip, map unload).
FUN_CMAPCHUNK_CTOR = 0x006AF1D0,
FUN_CMAPCHUNK_DTOR = 0x006AF460,
FUN_UNLOAD_CHUNK = 0x006A6B10,
// FixupRecordPointers `__thiscall(chunk, record)` reads +0x168;
// BuildVertices / LoadNormals / CreateCombinedTexture `__thiscall(chunk)`;
// AddLayer `__thiscall(chunk, area, mcly entry, mcal data)`, ret 0xC;
// CreateShadowTexture `__thiscall(chunk, mcsh data)`; SelectMesh
// `__thiscall(chunk, uint draw)`: mesh, buffers, DrawLayers 0x006BEF30.
FUN_CMAPCHUNK_FIXUP_RECORD = 0x006AF970,
FUN_CMAPCHUNK_BUILD_VERTICES = 0x006B0E50,
FUN_CMAPCHUNK_LOAD_NORMALS = 0x006AFF10,
FUN_CMAPCHUNK_ADD_LAYER = 0x006AFF80,
FUN_CMAPCHUNK_CREATE_SHADOW = 0x006B0030,
FUN_CMAPCHUNK_CREATE_COMBINED = 0x006B00A0,
FUN_CMAPCHUNK_SELECT_MESH = 0x006BEB50,
// LoadAreaTextures `__thiscall(area, const char *names, uint size)`,
// ret 8: a LoadMapTexture handle per MTEX name at area +0xEC, count
// +0xE8, no bound; it touches only +0x9C, +0xE8 and the array, which
// ends at +0x26C (96 slots). UnloadArea (0x006A6A10) closes each with
// FUN_HANDLE_RELEASE. AddLayer reads only the area's slot.
FUN_LOAD_AREA_TEXTURES = 0x006C20E0,
CMAPAREA_SIZE = 0x6A8,
OFF_CMAPAREA_TEXTURE_COUNT = 0xE8,
OFF_CMAPAREA_TEXTURES = 0xEC,
CMAPAREA_TEXTURE_SLOTS = 96,
// A static buffer pool and its sub-buffers, as CreateChunkBufferPool
// (0x006AE640) makes CMapChunk_vtx / _idx: CreatePool `void
// *__fastcall(kind, usage, size, uint, const char *name)`, ret 0xC
// (kind 0 vertex, 1 index; usage 1; 3); PoolBuffer `void *__fastcall(
// pool, itemSize, count, offset)`, ret 8; FreeBuffer `__fastcall(buf)`
// (0x0058A040); GxBufferRelease `__fastcall(pool)`. SelectMesh takes the
// world's pool only for a buffer whose pool has usage 2 (pool +0x0C;
// the device's shared dynamic buffer, GxBufCreate 0x0058A140).
FUN_GX_CREATE_POOL = 0x0058A160,
FUN_GX_POOL_BUFFER = 0x00589F80,
FUN_GX_FREE_BUFFER = 0x0058A040,
FUN_GX_BUFFER_RELEASE = 0x0058A1A0,
GX_POOL_KIND_VERTEX = 0,
GX_POOL_KIND_INDEX = 1,
GX_POOL_USAGE_STATIC = 1,
GX_POOL_ARG = 3,
CHUNK_VERTEX_SIZE = 0x18,
CHUNK_INDEX_SIZE = 2,
// Per level, 8 bytes each: the count, the list (0x0086C290 vertices,
// 0x0086C2B0 indices; level 3 = 145 / 768 in the image). The pool's
// buffers hold the full level (AcquireChunkBuffers 0x006AE8A0).
VAR_CHUNK_LEVEL_VERTEX_LISTS = 0x0086C290,
VAR_CHUNK_LEVEL_INDEX_LISTS = 0x0086C2B0,
CHUNK_LEVEL_LIST_SIZE = 8,
CHUNK_LEVEL_FULL = 3,
// The chunk level (0x006AFAD0): maxLOD, or the far level when world
// flag 4 is on and the view depth is above lodDist (strictly).
VAR_CHUNK_LOD_DIST = 0x00867960,
VAR_CHUNK_MAX_LOD = 0x00867964,
VAR_CHUNK_FAR_LOD = 0x00867968,
// IsBoxCulled `uint __thiscall(const float box[6])`: nonzero when the
// top of the frustum stack rejects the box (0x00683C3C).
FUN_FRUSTUM_IS_BOX_CULLED = 0x00682F40,
// The world flags (benilla terrain/world.md): 2 chunks drawn, 4 lod,
// 0x40 map shadows, the sign anisotropic: the chunk pass then sets the
// mip bias of stages 0-3 (states 0x27-0x2A) to the immediate pushed at
// 0x00684527 (0.25).
VAR_WORLD_FLAGS = 0x00C7B2A4,
WORLD_FLAG_CHUNKS = 0x2,
WORLD_FLAG_LOD = 0x4,
WORLD_FLAG_MAP_SHADOWS = 0x40,
WORLD_FLAG_ANISOTROPIC = 0x80000000,
GXRS_MIP_BIAS_STAGE0 = 0x27,
MIP_BIAS_STAGES = 4,
VAR_CHUNK_MIP_BIAS_IMMEDIATE = 0x00684528,
// DrawLayers reads both: the camera (texgen matrices are camera-relative)
// and the fog end (beyond it, less the radius, a chunk draws flat; the
// light slot needs the depth less twice the radius under it, 0x006C0CC0).
VAR_CAMERA_POSITION = 0x00C7CF20,
VAR_FOG_END = 0x00C62964,
// The shader paths (0x006BEF30): the pixel-program flag (`_s` textures,
// 0x006C2104) and the second; shadowLevel (alpha size, 0x006AFFE2). A
// chunk's textures are made for the values it was built with.
VAR_TERRAIN_SHADER_FLAG = 0x00C9A31C,
VAR_TERRAIN_SHADER_FLAG_B = 0x00C89F44,
VAR_SHADOW_LEVEL = 0x00C63284,
// CMapChunk (0xF34 bytes, benilla terrain/chunk.md).
CMAPCHUNK_SIZE = 0xF34,
OFF_CMAPCHUNK_FLAGS = 0x0C, // word: 0x40 impassable, 1 lights, 0x80 filed
OFF_CMAPCHUNK_CENTRE = 0x5C, // 3 floats, then the radius
OFF_CMAPCHUNK_RADIUS = 0x68,
OFF_CMAPCHUNK_POSITION = 0x6C, // from MCNK +0x68
OFF_CMAPCHUNK_VIEW_DEPTH = 0x78,
OFF_CMAPCHUNK_COLUMN = 0x7C,
OFF_CMAPCHUNK_ROW = 0x80,
OFF_CMAPCHUNK_COLUMN8 = 0x84,
OFF_CMAPCHUNK_ROW8 = 0x88,
OFF_CMAPCHUNK_GLOBAL_COLUMN = 0x8C,
OFF_CMAPCHUNK_GLOBAL_ROW = 0x90,
OFF_CMAPCHUNK_BOX = 0x94, // 6 floats, the culling copy
OFF_CMAPCHUNK_INDEX = 0xB4,
OFF_CMAPCHUNK_LEVEL = 0xB8,
OFF_CMAPCHUNK_NEIGHBOURS = 0xC4, // row - 1, column + 1, row + 1, column - 1
OFF_CMAPCHUNK_VERTEX_BUFFER = 0x14C,
OFF_CMAPCHUNK_INDEX_BUFFER = 0x150,
OFF_CMAPCHUNK_RECORD = 0x168,
OFF_CMAPCHUNK_AREA_ID = 0x16C,
OFF_CMAPCHUNK_HEADER = 0xF08,
OFF_CMAPCHUNK_LOW_TEXTURES = 0xF0C,
OFF_CMAPCHUNK_DOODAD_MAP = 0xF10,
OFF_CMAPCHUNK_MCSH = 0xF1C,
OFF_CMAPCHUNK_MCLY = 0xF20,
OFF_CMAPCHUNK_MCAL = 0xF24,
// CMapChunk::Load 0x006AF6E5: with a shadow, +0x148 takes MCNK +0x30 and
// CreateShadowTexture keeps the map at +0x144; else both are zeroed.
OFF_CMAPCHUNK_SHADOW_MAP = 0x144,
OFF_CMAPCHUNK_SHADOW_WORD = 0x148,
CMAPCHUNK_FLAG_IMPASSABLE = 0x40,
CMAPCHUNK_FLAG_LIGHTS = 0x1,
CMAPCHUNK_FLAG_FILED = 0x80,
// An ADT as ParseAreaFile (0x006C2010) reads it: MHDR data at buffer +
// 0x10 + the MVER size (+0x04); its +0x04 MCIN and +0x08 MTEX offsets
// point 8 bytes before the data, the size the dword before it. MCIN:
// 256 x 16 bytes, +0 the MCNK record's file offset. MCNK header (record
// + 8): +0x00 flags (1 shadow, 2 impassable), +0x0C layers, +0x14 MCVT,
// +0x18 MCNR, +0x1C MCLY, +0x24 MCAL, +0x2C MCSH, +0x34 area, +0x3C holes,
// +0x40 / +0x50 maps, +0x68 position; each sub-record offset is from the
// record and lands 8 bytes before its data (FixupRecordPointers). MCLY
// entries are 16 bytes: +0 texture index, +4 flags (0x100 alpha map), +8
// MCAL offset.
OFF_ADT_MVER_SIZE = 0x04,
ADT_MHDR_BASE = 0x10,
OFF_MHDR_MCIN = 0x04,
OFF_MHDR_MTEX = 0x08,
// ParseAreaFile's model tables: MMDX names +0x0C, MMID name offsets +0x10,
// MDDF placements +0x1C (36 bytes each, the count size / 36).
OFF_MHDR_MMDX = 0x0C,
OFF_MHDR_MMID = 0x10,
OFF_MHDR_MDDF = 0x1C,
// An MDDF entry as PlaceDoodadDef (0x00694BC0) reads it: +0x00 the MMID
// index, +0x04 the unique id (one def per id, the hash 0x00CA7DDC), +0x08
// position (x = origin - +0x10, y = origin - +0x08, z = +0x0C), +0x14 /
// +0x18 / +0x1C the y / z / x angles in degrees (z + pi), +0x20 the scale
// word x 1/1024 (0x008090D8). The matrix: TranslateMatrix, RotateMatrix
// about z, y, x (0x007BDD60), ScaleMatrix (0x007BDD00); then
// CreateDoodadModel (0x00695100) as for a WMO's doodads. LoadReferences
// (0x006B00F0) gives a chunk's doodads flag 4 (outdoors, the sun) and
// intensity 1.0; their map lights come only from WMO group owners, so a
// chunk's doodads have none.
MDDF_SIZE = 36,
OFF_MDDF_NAME_INDEX = 0x00,
OFF_MDDF_UNIQUE_ID = 0x04,
OFF_MDDF_POSITION = 0x08,
OFF_MDDF_ROTATION = 0x14,
OFF_MDDF_SCALE = 0x20,
VAR_ONE_OVER_1024 = 0x008090D8,
// Detail doodads (benilla terrain/detail.md; Ghidra 2026-11-11). In the
// chunk pass (0x006847C9), with world flag 0x100000, a drawn chunk whose
// view depth +0x78 is below the horizon (0x00867958, read live: 300.0 in
// both the stock and the Octo image) scatters its batch once
// (CMapChunk::CreateDetailDoodads `__thiscall(chunk)` 0x006BFC10: it reads
// the chunk, the models table, the layers' GroundEffectTexture rows and
// 512 bytes at the MCSH pointer +0xF1C, flag or not); any other drawn
// chunk leaves the list. The pass (0x006813D0) sets stage 0's mip bias to
// the immediate at 0x006813EA (0.25), then for each listed chunk with a
// batch +0xC0: the world transform at the chunk, the chunk's lighting
// record, CMapChunk::DrawDetailDoodads `__thiscall(chunk, float)`
// (0x006C02D0; nonzero draws, the pass passes the bits 1). The batches'
// slot buffers come from the world's detail pools, which
// PrepareDetailDoodadBuffers `__cdecl` (0x006B1D30) frees and rebuilds
// while the flag 0x00CB0034 is set (frillDensity), after freeing only the
// listed chunks' batches (FreeAllDetailDoodads 0x006924F0); each rebuild
// adds 0x80 to 0x00CB003C, which nothing else writes. A slot's release
// (ReleaseDetailDoodadSlot 0x006B21D0) reads its buffer's pool, so a scene
// batch gives its buffers back after each draw (the world's idle release
// ReleaseDetailDoodadBatchBuffers `__thiscall(batch)` 0x006B28E0) and is
// freed (FreeDetailDoodadBatch `__thiscall(batch)` 0x006B2030) when the
// stamp moves. The models load into the world's model manager 0x00C7B298.
WORLD_FLAG_DETAIL_DOODADS = 0x100000,
VAR_DETAIL_DOODAD_HORIZON = 0x00867958,
VAR_DETAIL_MIP_BIAS_IMMEDIATE = 0x006813EA,
VAR_DETAIL_DOODAD_REBUILD = 0x00CB0034,
VAR_DETAIL_DOODAD_STAMP = 0x00CB003C,
VAR_WORLD_MODEL_MANAGER = 0x00C7B298,
FUN_PREPARE_DETAIL_DOODAD_BUFFERS = 0x006B1D30,
FUN_CMAPCHUNK_CREATE_DETAIL_DOODADS = 0x006BFC10,
FUN_CMAPCHUNK_DRAW_DETAIL_DOODADS = 0x006C02D0,
FUN_RELEASE_DETAIL_BATCH_BUFFERS = 0x006B28E0,
FUN_FREE_DETAIL_DOODAD_BATCH = 0x006B2030,
OFF_CMAPCHUNK_DETAIL_BATCH = 0xC0,
// Terrain liquid (benilla terrain/liquid, liquidpass, watershade; Ghidra
// 2026-11-11). CMapChunk::LoadLiquids `__thiscall(chunk, uint first)`
// (0x006AF760, Load's third step): one 0x324-byte MCLQ block (MCNK +0x60)
// per set flag bit 0x4 / 0x8 / 0x10 / 0x20, in order, into slots 0-3 (+0x118)
// of the growable WCHUNKLIQUID pool (AllocChunkLiquid 0x006A0EE0; its list
// 0x00CA7D7C is walked only at exit). A slot: +0x04 / +0x08 the low and
// high heights, +0x1C its chunk, +0x400 / +0x404 its buffers (81 x 0x28,
// 192 x 2). The world's liquid buffer pool is rebuilt in DrawWorldScene
// (CreateLiquidBufferPool 0x0068CA50), and ReleaseLiquidBuffers (0x0068CD50)
// only nulls a buffer of another pool; CChunkLiquid::Draw `__thiscall(slot,
// uint pass)` (0x0068DE40) acquires only for no buffer or a usage 2 pool.
// A slot draws in the pass of its index (CMapChunk::Update 0x006AFAD0
// buckets slot i in layer i): 0 river, 1 ocean, 2 magma; slime (3) is
// never drawn. Its box: the chunk's (+0x44) with z the slot's heights
// (0x0068DF40).
FUN_CMAPCHUNK_LOAD_LIQUIDS = 0x006AF760,
FUN_CHUNKLIQUID_DRAW = 0x0068DE40,
OFF_MCNK_MCLQ = 0x60,
MCLQ_SIZE = 0x324,
MCNK_FLAG_LIQUID_FIRST = 0x4,
CHUNK_LIQUID_SLOTS = 4,
OFF_CMAPCHUNK_LIQUIDS = 0x118,
OFF_CHUNKLIQUID_LOW = 0x04,
OFF_CHUNKLIQUID_HIGH = 0x08,
OFF_CHUNKLIQUID_CHUNK = 0x1C,
OFF_CHUNKLIQUID_VERTEX_BUFFER = 0x400,
OFF_CHUNKLIQUID_INDEX_BUFFER = 0x404,
// GetLiquidStatus (0x0069B6D0) after the map objects' liquid: the cells
// from u = (17066.666 - y) x 0.24 (VAR_POINT_CELLS_PER_YARD) and v from
// x, each stored as a float, less 0.5 (VAR_POINT_HALF_CELL) and rounded;
// the chunk is the cells >> 3, the tile (v & 7, u & 7) and the fractions
// the products less their cells. Each slot's tile byte ([slot + 0x10] +
// 8 x row + column; low nibble 0xF none) answers type & 3: ocean (1) at
// height 0 when z is under 0.01 (VAR_ONE_HUNDREDTH), else the MCLQ
// heights ([slot + 0x0C], 8-byte records, the height at +4) bilinear (a = (h01 - h00) x fu + h00 along
// u, then b - a times fv), stored as a float, when z is under it plus
// 0.01. The camera's probe outside a map object group asks the same
// (0x006723D0 from CameraEnvironment 0x006809C0).
OFF_CHUNKLIQUID_MCLQ_VERTICES = 0x0C,
OFF_CHUNKLIQUID_TILES = 0x10,
MCLQ_VERTEX_SIZE = 8,
OFF_MCLQ_VERTEX_HEIGHT = 4,
LIQUID_TILE_ROW = 8,
LIQUID_VERTEX_ROW = 9,
LIQUID_TILE_TYPE_MASK = 0xF,
LIQUID_TILE_NONE = 0xF,
LIQUID_TYPE_MASK = 3,
LIQUID_OCEAN = 1,
// GatherChunkFacets' liquid leg (0x006AB3F7): each slot whose bit
// (0x10000 << slot) the class has goes to GatherLiquidFacets
// (0x006AB4E0), which resets the facet batches (the five words
// ResetFacetBatches clears), runs GatherLiquidCellFacets `__thiscall
// (slot, void *unused, const float *box, const int *cells)` (0x0068CEC0,
// ret 0xC, AL = any batch) with the chunk-relative box and the clipped
// cell rectangle {row first, column first, row last, column last}, and
// emits the batches on a hit (0x00671CC0). The batch is the slot's 81
// vertices (+0x34) at the chunk's position. The walls of an impassable
// chunk ignore the class; the doodad leg takes no def for 0x30000.
FUN_CHUNKLIQUID_GATHER_CELL_FACETS = 0x0068CEC0,
LIQUID_CLASS_FIRST_BIT = 16,
LIQUID_VERTEX_SIZE = 0x28,
LIQUID_VERTEX_COUNT = 0x51,
LIQUID_INDEX_COUNT = 0xC0,
LIQUID_PASS_RIVER = 0,
LIQUID_PASS_OCEAN = 1,
LIQUID_PASS_MAGMA = 2,
// The passes: magma (0x006855A0) in DrawWorldScene after the map objects;
// ocean (0x00685010) then river (0x006851B0) in RenderLiquid before the
// group liquids. Each pushes the state and turns culling off. Ocean and
// river bind their swatch (0x00C81B90, 0x00C89D68) on stage 0; with the
// fancy-water byte (0x00C9A324) the water program (0x00C81B7C), specular
// white, shininess 6.0 (0x008102E8), blend 2 and no depth write, else no
// program and stage 1 preset 3 (add); the river's third-stage leg tests
// a program 0x00C800EC that nothing writes. Magma: lighting and blending
// off. Per slot, with world flag 0x1000000: the world transform at the
// chunk less the camera, GatherSlotLights (0x0068DF70: a record at the
// chunk's x, y and the middle of the slot's heights, (high - low) x 0.5 +
// low, radius 23.570166 the immediate at 0x0068DFAF; the zone fog; the
// sun and the chunk's lights when the depth less twice the radius is
// inside the fog end), then the draw. Each swatch refills from the light
// state on the first slot drawn after its fresh flag (0x00C8117C river,
// 0x00C81B70 ocean) is cleared, which the world does once a frame
// (0x006809C0).
WORLD_FLAG_LIQUIDS = 0x1000000,
// The drift cloud (model/SceneWater.cpp): 0xFA40 bytes, 4000 motes of 16
// bytes (a camera-relative position, the quad size at +0xC), built by
// CreateDriftCloud `__thiscall(cloud, sizeBase bits, edge bits, path)`
// with the immediates the world pushes (0x0066F99E: 1/36, 30.0, the path
// at 0x00867A34); its texture is closed by 0x0068E640. The camera's
// liquid change (0x00680AEE) calls the scatter `__thiscall(cloud, mode)`
// with the new liquid, then the size base (1/36 for water and ocean,
// 1/9 for magma, 0x00680B57) and sets the enable byte +0xFA14; slime
// clears it. The advect `__thiscall(cloud)` reads the camera position
// (VAR_CAMERA_POSITION) and the frame seconds (0x00C62510); the draw
// `__thiscall(cloud)` the device view. Both run with world flag 0x2000000
// (the waterParticulates setting) and the camera in a liquid.
WORLD_FLAG_PARTICULATES = 0x2000000,
DRIFT_CLOUD_SIZE = 0xFA40,
FUN_CREATE_DRIFT_CLOUD = 0x0068E5A0,
FUN_FREE_DRIFT_CLOUD_TEXTURE = 0x0068E640,
FUN_SET_DRIFT_CLOUD_SIZE_BASE = 0x0068E670,
FUN_SCATTER_DRIFT_CLOUD = 0x0068E720,
FUN_ADVECT_DRIFT_CLOUD = 0x0068E930,
FUN_DRAW_DRIFT_CLOUD = 0x0068EFE0,
OFF_DRIFT_CLOUD_ENABLED = 0xFA14,
VAR_DRIFT_TEXTURE_PATH = 0x00867A34,
DRIFT_EDGE = 0x41F00000, // 30.0
DRIFT_SIZE_BASE_WATER = 0x3CE38E39, // 1/36
DRIFT_SIZE_BASE_MAGMA = 0x3DE38E39, // 1/9
VAR_FRAME_SECONDS = 0x00C62510,
// The foam rings and wakes of units in shallow liquid (model/SceneWater.cpp).
// CGUnit_C::EmitWaterRipple `__thiscall(unit, u32 kind)` (0x005FA760)
// runs each unit update (0x0060802E, kind 0) and after the swim
// decision's splash (0x0060319F, kind 0xC9): with the unit's liquid
// (UpdateMapEntityEnvironment: bit 0x20, the height +0x98) and bit 0x40
// (flag 0x1 of the area's LiquidType), the depth under twice the
// collision height (at least 1.0) and its next time (+0xC78, OS ms)
// reached, it emits at its position on the surface. The ripple kind:
// 3 for 0xC9, else 2 moving (move flags 0xF), 1 turning (0x30), 0 still;
// the byte table VAR_RIPPLE_KIND_WAKE {0, 0, 1, 0} makes kind 2 a wake
// (category 1, along the motion) and the rest rings (category 0, a random
// heading). AddSplash (0x0068BDA0) emits nothing while the waterRipples
// word is 0 and only in mode 0 (VAR_RIPPLE_MODE); AddRipple (0x0068F9F0)
// takes the next of 128 records of 0x50, 0 to 31 for the active mover and
// 32 to 127 for the rest; EmitRipple (0x0068F5C0) writes it and copies
// the liquid triangles TestWorldBox (class 0xF0000) finds in its box.
// UpdateRipples (0x0068FAE0, the tail of RenderLiquid) ages and draws
// them: blend 3, fog, depth write, culling and lighting off, the depth
// bias footstepBias (CVar 0x00C7F300) x 1/1024, the world stack at
// Translate(-camera), per category its texture (VAR_RIPPLE_TEXTURES,
// XTextures\splash\splash.blp and wake.blp, loaded by InitRipples
// 0x0068F8B0) over the polygon in format 4 (position, normal (0, 0, 1),
// colour, uv; 0x24 bytes), the uv the polygon through the texture matrix
// BuildDecalTransforms makes of the ripple's box turned by its heading.
VAR_WATER_RIPPLES = 0x0086A03C,
VAR_RIPPLE_MODE = 0x00C89DB8,
VAR_RIPPLE_TEXTURES = 0x00C800E4,
VAR_RIPPLE_KIND_WAKE = 0x00860A4C,
VAR_RIPPLE_RISE_SHARES = 0x00810348, // float[2] 0.4, the rise's share of the life
VAR_FOOTSTEP_BIAS_CVAR = 0x00C7F300,
VAR_RIPPLE_BIAS_SCALE = 0x00810390, // float 1/1024
RIPPLE_POOL = 0x80,
RIPPLE_POOL_SELF = 0x20,
RIPPLE_KIND_SPLASH = 0xC9,
RIPPLE_KIND_TURNING_INDEX = 1,
RIPPLE_KIND_MOVING_INDEX = 2,
RIPPLE_KIND_SPLASH_INDEX = 3,
RIPPLE_MOVE_FLAGS = 0xF, // forward, backward, strafing (0x005FA7E8)
RIPPLE_TURN_FLAGS = 0x30, // turning left or right (0x005FA7F3)
LIQUID_TRIANGLE_CLASS = 0xF0000,
LIQUIDTYPE_FLAG_RIPPLES = 0x1,
// EmitWaterRipple's own floats (each checked against the image).
VAR_RIPPLE_SPEED_CAP = 0x00804478, // float 20.0
VAR_RIPPLE_SPEED_SCALE = 0x00803088, // float 2.5
VAR_RIPPLE_SIZE_MAX = 0x0080655C, // float 5/3
VAR_RIPPLE_LIFE_SPREAD = 0x008029C8, // float 0.05
VAR_RIPPLE_LIFE_BASE = 0x0080C4F0, // float 0.65
VAR_RIPPLE_GROWTH_SPAN = 0x0080308C, // float 1.5
VAR_RIPPLE_GROWTH_BASE = 0x0080C4EC, // float 3.75
VAR_RIPPLE_GROWTH_SCALE = 0x0080C4E8, // float 12.0
VAR_RIPPLE_STILL_SCALES = 0x0080C474, // float[4] 0.8, 1.0, 0.25, 0.6
VAR_RIPPLE_THREE_QUARTER_PI = 0x0080C4E0, // float 3pi/4
VAR_RIPPLE_BACK_TURN = 0x008030B0, // float pi
VAR_RIPPLE_NEXT_SHARE = 0x008029B0, // float 0.25
VAR_RIPPLE_NEXT_MS = 0x00801448, // float -1000.0
VAR_RIPPLE_PEAK_SCALE = 0x00802990, // float 6.0
RIPPLE_STILL_DELAY_MS = 400, // 0x005FAC4E: 400 + the high word of 50 x a draw
RIPPLE_STILL_DELAY_SPREAD = 50,
// The GX buffers the ripple draw makes each frame: GxBufCreate
// `void *__fastcall(int kind, int itemSize, int count)` ret 4 (kind 0
// vertices, 1 indices), Lock `void *__fastcall(buf)`, Unlock
// `__fastcall(buf, int)`, the vertex bind `__fastcall(buf, int format)`
// and the index bind `__fastcall(buf)`.
FUN_GX_BUF_CREATE = 0x0058A140,
FUN_GX_BUF_LOCK = 0x0058A080,
FUN_GX_BUF_UNLOCK = 0x0058A0A0,
FUN_GX_VERTEX_BUF_BIND = 0x0058A7C0,
FUN_GX_INDEX_BUF_BIND = 0x0058A800,
GX_BUF_VERTICES = 0,
GX_BUF_INDICES = 1,
GX_FORMAT_PNCT = 4,
GX_PNCT_SIZE = 0x24,
VAR_OCEAN_SWATCH = 0x00C81B90,
VAR_RIVER_SWATCH = 0x00C89D68,
VAR_FANCY_WATER = 0x00C9A324,
VAR_WATER_PROGRAM = 0x00C81B7C,
VAR_WATER_SHININESS = 0x008102E8,
VAR_RIVER_SWATCH_FRESH = 0x00C8117C,
VAR_OCEAN_SWATCH_FRESH = 0x00C81B70,
VAR_LIQUID_LIGHT_RADIUS_IMMEDIATE = 0x0068DFAF,
GXRS_SPECULAR = 0x03,
GXRS_SHININESS = 0x04,
LIQUID_STAGE1_ADD = 3,
FUN_GX_BUFFER_INVALIDATE = 0x0058A130,
// The MCNK header's reference counts (LoadReferences): doodads +0x10,
// map objects +0x38; the MCRF list holds the doodad indices first.
OFF_MCNK_DOODAD_REF_COUNT = 0x10,
OFF_MCNK_MCRF = 0x20,
OFF_MCNK_MAPOBJ_REF_COUNT = 0x38,
ADT_CHUNKS = 256,
MCIN_ENTRY_SIZE = 0x10,
MCNK_RECORD_HEADER = 8,
OFF_MCNK_FLAGS = 0x00,
OFF_MCNK_LAYER_COUNT = 0x0C,
OFF_MCNK_MCVT = 0x14,
OFF_MCNK_MCNR = 0x18,
OFF_MCNK_MCLY = 0x1C,
OFF_MCNK_MCAL = 0x24,
OFF_MCNK_MCSH = 0x2C,
OFF_MCNK_SHADOW_WORD = 0x30,
OFF_MCNK_AREA_ID = 0x34,
OFF_MCNK_HOLES = 0x3C,
OFF_MCNK_LOW_TEXTURES = 0x40,
OFF_MCNK_DOODAD_MAP = 0x50,
OFF_MCNK_POSITION = 0x68,
MCNK_HEADER_SIZE = 0x80,
MCNK_FLAG_SHADOW = 0x1,
MCNK_FLAG_IMPASSABLE = 0x2,
MCNK_HOLES_ALL = 0xFFFF,
MCNK_MAX_LAYERS = 4,
MCLY_ENTRY_SIZE = 0x10,
OFF_MCLY_FLAGS = 0x04,
OFF_MCLY_ALPHA = 0x08,
MCLY_FLAG_ALPHA = 0x100,
MCVT_SIZE = 145 * 4,
MCNR_SIZE = 145 * 3,
MCAL_MAP_SIZE = 0x800,
MCSH_SIZE = 0x200,
// Tiles fall from the map origin (17066.666, 0x007FFAB4) in steps of
// 533.33331 (0x0080654C): the row from x, the column from y; a tile is
// <dir>\<dir>_<column>_<row>.adt (0x006C274D) of 16 x 16 chunks.
VAR_MAP_ORIGIN = 0x007FFAB4,
VAR_TILE_SIZE = 0x0080654C,
MAP_TILES = 64,
TILE_CHUNKS = 16,
// ---- Terrain collision (Model::Scene::Terrain, layer 3) ----------------------
// The box gather (0x006AA8B0 under the box query 0x006721B0): map objects
// first, then each chunk whose cells the box touches. Corners map to
// cells as fistp(float(0.24 x float(17066.666 - c)) - 0.5), rows from x;
// the box must have origin - max >= 0 (a NaN passes) and origin - min
// < 34133.332 on both axes. Chunk rows run from the max-x cell >> 3 to
// the min-x cell >> 3, columns likewise from y.
VAR_GATHER_CELLS_PER_YARD = 0x00810AE4,
VAR_GATHER_HALF_CELL = 0x0086AA2C,
VAR_MAP_EXTENT = 0x007FFAB0,
// GatherChunkFacets (0x006AADC0, terrain legs): the fence walls of an
// impassable chunk first, then each cell of the clamped rectangle that
// no hole bit drops: outcodes of its five vertices (offsets 0, 9, 17, 1,
// 18 from its corner, 0x00810468) against the box less the chunk
// position, grown by 0.019444443 (0x008101B8), sign bits of float(v -
// min + e) and float(max - v + e); each of its four triangles (0x00810438,
// three vertex offsets each) kept unless its three codes share a bit, at
// vertex + position, its plane by the emitter's SSE arithmetic
// (Contact::MakeFacet). Hole bit (column >> 1) + (row >> 1) x 4 of the
// table 0x008103F8 against MCNK +0x3C.
VAR_CELL_OUTCODE_VERTICES = 0x00810468,
CELL_OUTCODE_VERTICES = 5,
VAR_CELL_FACET_TRIANGLES = 0x00810438,
CELL_TRIANGLES = 4,
VAR_GATHER_EPSILON = 0x008101B8,
VAR_HOLE_BITS = 0x008103F8,
CHUNK_ROW_VERTICES = 17,
CHUNK_CELLS = 8,
// The chunk's own box (BuildVertices 0x006B0E50): min +0x44, max +0x50.
// AddImpassableWalls (0x006AB530): for each side the query box crosses
// (strictly: y-min, y-max, x-min, x-max), facets (p, p+e, p+e+u) and (p,
// p+e+u, p+u) with u = (0, 0, 32000), the side's outward axis normal and
// d = -n.p; the 32000 is the immediate stored at 0x006AB599.
OFF_CMAPCHUNK_BOX_MIN = 0x44,
OFF_CMAPCHUNK_BOX_MAX = 0x50,
// The 145 chunk-relative vertices (BuildVertices), 12 bytes each: row r,
// column c of the outer grid at r x 17 + c, a cell centre 9 past its corner.
OFF_CMAPCHUNK_VERTICES = 0x83C,
VAR_CHUNK_WALL_HEIGHT_IMMEDIATE = 0x006AB59C,
// The segment trace's terrain leg (TraceTerrainSegment 0x0069C320): ends
// to cells as the gather does (0x008107A4 = 0.24, 0x0086A758 = 0.5), a
// run along one column when |dy| < 2^-22 (0x008029D4) or both ends share
// a column, else along one row likewise for x, else a line walk: Rows
// (0x0069C600) when |dy| > |dx|, else Columns (0x0069C780), each
// `__fastcall(start, end, cells)` ret 4 with start/end = {origin - y,
// origin - x, 0}; the runs are `__thiscall(cells)`; cells = {start x, start
// y, end x, end y}. They fill the cell list 0x00C962C8 (pairs: the y
// cell, then the x cell; count of dwords 0x00C89F40, zeroed first).
VAR_TRACE_CELLS_PER_YARD = 0x008107A4,
VAR_TRACE_HALF_CELL = 0x0086A758,
FUN_LIST_CELL_ROW_RUN = 0x0069C4E0,
FUN_LIST_CELL_COLUMN_RUN = 0x0069C570,
FUN_LIST_CELLS_ALONG_ROWS = 0x0069C600,
FUN_LIST_CELLS_ALONG_COLUMNS = 0x0069C780,
VAR_TRACE_CELLS = 0x00C962C8,
VAR_TRACE_CELL_COUNT = 0x00C89F40,
// TraceListedCells (0x0069C920): a cell word above 0x2000 or a missing
// chunk ends the walk; the ray is the start less the chunk position and
// the unit direction (k = 1/sqrt((dz*float(dz) + dy*dy) + dx*dx)); each
// of the cell's four triangles (centre + 9, then the pairs at 0x0081047C,
// second then first) through IntersectRayTriangle `uint8 __fastcall(ray,
// vertices, const uint32 indices[3], float *t, float *uv, float slack)`
// ret 0x10, slack the immediate pushed at 0x0069CB95 (0.01); a hit at
// t*k below the best and not below 0 is the best.
TRACE_CELL_LIMIT = 0x2000,
TRACE_CELL_CHUNK_BITS = 0x1FF8,
VAR_CELL_TRACE_TRIANGLES = 0x0081047C,
CELL_CENTRE_VERTEX = 9,
FUN_INTERSECT_RAY_TRIANGLE = 0x007C2C40,
VAR_TRACE_SLACK_IMMEDIATE = 0x0069CB96,
// The decal's world box test (TestWorldBox 0x006AD2C0): map objects under
// 0xF00F0, then the terrain under 0xF0F00 (TestTerrainBox 0x006AD4E0,
// `__fastcall(box, mask, out)`). Its cells: the first from the unrounded
// origin - min.y, the rest from the stored floats, each fistp(float(0.24 x
// d) - 0.5) (0x00810B44, 0x0086AAC4); rows from the max-x cell >> 3 to the
// min-x cell >> 3, columns likewise from y; no bounds test. Per chunk
// (TestChunkBox 0x006AD610) the box less the chunk position and the
// rectangle clipped to {0, 0, 7, 7} (IntersectCellRects 0x006ADBF0), then,
// under 0x100, TestChunkTriangles `uint8 __fastcall(chunk, const int32
// rect[4], const float box[6], uint32 unused)` ret 8 (0x006AD7E0): the
// cells' kept triangles into one batch of the triangle batches, its matrix
// the chunk's translation, its vertices +0x83C. It reads only the chunk.
DECAL_MASK_TERRAIN = 0xF0F00,
CHUNK_TRIANGLE_CLASS = 0x100,
VAR_DECAL_CELLS_PER_YARD = 0x00810B44,
VAR_DECAL_HALF_CELL = 0x0086AAC4,
FUN_TEST_CHUNK_TRIANGLES = 0x006AD7E0,
// A unit's ground on terrain (AttachEntityToGround 0x006A8A20): the
// terrain (TraceTerrainSegment, mask 0x100) and the map objects traced
// down 1000 from the probe point, else up past the bounds; the terrain wins
// when nearer. Then GetGroundEffect `bool __fastcall(const float point[3],
// uint32 *out)` (0x0069B1C0) at the entity's position: the cell as the
// others (0.24 at 0x0081073C, 0.5 at 0x0086A750; distances >= 0 and below
// 34133.332), no hole, a layer, then the dominant layer of the cell: the
// MCNK +0x40 map's word for the row (+0xF0C, u16 per row) masked by
// 0x008104B4[column] (u16) and shifted right by 0x008104C4[column] (u32),
// that layer's effect id (+0x0A; 0xFFFF none) into GroundEffectTexture,
// whose +0x18 is the TerrainType id. It finds the chunk through the
// global area table, so the scene transcribes it.
// The same 0.24 and 0.5 serve GetLiquidStatus (0x0069B709), and the 0.5
// the area query (0x0069B120).
VAR_POINT_CELLS_PER_YARD = 0x0081073C,
VAR_POINT_HALF_CELL = 0x0086A750,
VAR_LOW_TEXTURE_MASKS = 0x008104B4,
VAR_LOW_TEXTURE_SHIFTS = 0x008104C4,
OFF_CMAPCHUNK_LAYERS = 0x128,
OFF_CMAPCHUNK_LAYER_COUNT = 0x138,
OFF_CHUNKLAYER_EFFECT = 0x0A,
GROUND_EFFECT_NONE = 0xFFFF,
// GroundEffectTexture.dbc (docs/DBCs.md; 7 columns: id, 4 doodads,
// density, TerrainType id at +0x18). Row 0 exists.
VAR_GROUNDEFFECTTEXTURE_RECORDS = 0x00C0DC60,
VAR_GROUNDEFFECTTEXTURE_COUNT = 0x00C0DC64,
OFF_GROUNDEFFECTTEXTURE_TERRAIN_TYPE = 0x18,
};
+1 -1
View File
@@ -31,7 +31,7 @@ const char *NormalizeEmpty(const char *s) {
// (0x00472797). An unloaded store's count is -1, so it takes no id.
const uint8_t *Record(uintptr_t recordsVar, uintptr_t countVar, uint32_t id) {
const int count = *reinterpret_cast<const int *>(countVar);
if (id == 0 || count <= 0 || id > static_cast<uint32_t>(count))
if (count < 0 || id > static_cast<uint32_t>(count))
return nullptr;
const uint8_t *const *records =
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
+7 -4
View File
@@ -37,10 +37,13 @@
// `nullptr` rather than checking individual error conditions.
namespace DBC {
// Resolves `id` to its record pointer in the table whose records-base
// global is at `recordsVar` and count global at `countVar`. Returns
// `nullptr` for `id == 0`, OOR ids (> count, which includes -1 and every
// other negative id), and unpopulated slots.
// Resolves `id` to its record pointer in the table whose id map global is
// at `recordsVar` and largest-id global at `countVar`, by the engine's own
// rule (0x005F4010, benilla dbc.md): `nullptr` for an id above the largest
// (which includes -1 and every other negative id) and for an empty slot.
// Id 0 is a row like any other where the table has one (Map.dbc's Eastern
// Kingdoms, TerrainType, ItemClass's Consumable); elsewhere its slot is
// empty.
// Caller reads whatever fields it needs off the returned record.
//
// Use this when `LocalizedField` / `StringField` don't fit — e.g. for
+2 -10
View File
@@ -13,6 +13,7 @@
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
@@ -46,16 +47,7 @@ int CapForType(uint32_t type) {
}
const uint8_t *MapRecord(uint32_t mapID) {
const int count = *reinterpret_cast<const int *>(
static_cast<uintptr_t>(Offsets::VAR_MAP_COUNT));
if (static_cast<int>(mapID) < 0 || static_cast<int>(mapID) > count)
return nullptr;
const uint8_t *const *records =
*reinterpret_cast<const uint8_t *const *const *>(
static_cast<uintptr_t>(Offsets::VAR_MAP_RECORDS));
if (records == nullptr)
return nullptr;
return records[mapID];
return DBC::Record(Offsets::VAR_MAP_RECORDS, Offsets::VAR_MAP_COUNT, mapID);
}
} // namespace
+1 -13
View File
@@ -65,20 +65,8 @@ constexpr int kMapTypeContinent = 2;
constexpr int kMapTypeZone = 3;
constexpr int kMapTypeDungeon = 4;
// `Map.dbc` is indexed by map id, and map id 0 (Eastern Kingdoms) is a real
// row — so the shared `DBC::Record` (which treats id 0 as "unused slot") can't
// be used here. Mirrors the reader in `Instance::Info`.
const uint8_t *MapRecord(int mapID) {
if (mapID < 0)
return nullptr;
const int count = Game::Read<int>(Offsets::VAR_MAP_COUNT);
if (mapID > count)
return nullptr;
auto *const *records = *reinterpret_cast<const uint8_t *const *const *>(
static_cast<uintptr_t>(Offsets::VAR_MAP_RECORDS));
if (records == nullptr)
return nullptr;
return records[mapID];
return DBC::Record(Offsets::VAR_MAP_RECORDS, Offsets::VAR_MAP_COUNT, static_cast<uint32_t>(mapID));
}
// Localized `Map.dbc` display name ("Eastern Kingdoms", "Deadmines"), or null.
+289 -72
View File
@@ -107,7 +107,9 @@
// SetWorldModelByMapID / ClearWorldModel: a WMO drawn by the world's own map
// object draw after the animate pass, with its doodads as models of the
// scene; Set/GetPlayWorldModelSounds: the doodads' sounds
// (model/SceneWorld.cpp).
// (model/SceneWorld.cpp). SetWorldTerrain / ClearWorldTerrain: a map's ADT
// chunks around a point, built and drawn by the world's own chunk functions
// before the WMO (model/SceneTerrain.cpp).
#include "model/Scene.h"
@@ -169,6 +171,12 @@ struct Fields {
int32_t groundTerrain; // SetGroundTerrain: a TerrainType id, or -1
Actor *hovered; // the actor OnActorEnter last ran for, or null
World::Backdrop *world; // SetWorldModel, owned, or null
Terrain::Land *terrain; // SetWorldTerrain, owned, or null
// The camera's liquid (0xF dry), from the last update, and how far below
// the eye its probe is, from the last paint (World::ProbeDrop).
uint32_t cameraLiquid;
float probeDrop;
Water::Effects *water; // the drift cloud, made when the camera first enters a liquid
bool playWorldModelSounds; // SetPlayWorldModelSounds
Actor *listener; // SetSoundListenerActor, or null; the frame owns its actors
// GetSceneTime: the seconds the M2 scene's clock has advanced since the
@@ -380,8 +388,9 @@ void CameraTransforms(void *frame, void *camera, const float (&rect)[4]) {
// it onto its ground with a decal (DrawDecal 0x006D7330): the faces in a box
// from below the feet to above them, the texture placed on them by texgen,
// faded with height by the ramp on stage 1. In a scene with a WMO the decal
// is laid the same way on the WMO's faces (World::DrawDecal). A scene without
// one has no ground, so the turned box itself is drawn, flat on the plane of
// is laid the same way on the WMO's and the terrain's faces (World::DrawDecal).
// A scene without either has no ground, so the turned box itself is drawn,
// flat on the plane of
// the base translation (the model's origin, its feet), culling off as for the
// engine's other single quads (the lightning bolt, 0x007AFCB0), with no
// height fade and no depth function (the quad is its own geometry). A model
@@ -487,8 +496,21 @@ void DrawQuad(const Blob &blob, const float (&eye)[3]) {
kStrip);
}
// The ground the paint lays decals on: the WMO and the terrain it draws.
struct Ground {
World::Backdrop *world = nullptr;
Terrain::Land *terrain = nullptr;
bool Any() const { return world != nullptr || terrain != nullptr; }
};
Ground GroundOf(void *frame) {
if (CameraOf(frame) == nullptr)
return {};
return {F(frame).world, F(frame).terrain};
}
// Draws one actor's shadow; the blob states are set.
void DrawShadow(Actor &actor, World::Backdrop *world, const float (&eye)[3]) {
void DrawShadow(Actor &actor, const Ground &ground, const float (&eye)[3]) {
void *model = actor.model;
if (!actor.shadowShown || !actor.shown || model == nullptr || M2::Parent(model) != nullptr ||
!M2::IsReady(model, 0, 0))
@@ -496,18 +518,19 @@ void DrawShadow(Actor &actor, World::Backdrop *world, const float (&eye)[3]) {
Blob blob;
if (!MakeBlob(model, blob))
return;
if (world == nullptr) {
if (!ground.Any()) {
DrawQuad(blob, eye);
return;
}
const uint32_t fadeBits = Offsets::DECAL_SHADOW_FADE;
float fade;
std::memcpy(&fade, &fadeBits, sizeof fade);
World::DrawDecal(*world, blob.box, blob.color, blob.matrix, fade, Offsets::SHADOW_DECAL_MASK, eye);
World::DrawDecal(ground.world, ground.terrain, blob.box, blob.color, blob.matrix, fade,
Offsets::SHADOW_DECAL_MASK, eye);
}
// Draws one actor's shadow with the blob states (while shadowLOD is 1).
void DrawShadowWithStates(Actor &actor, World::Backdrop *world, const float (&eye)[3]) {
void DrawShadowWithStates(Actor &actor, const Ground &ground, const float (&eye)[3]) {
void *texture = Game::Read<void *>(Offsets::VAR_SHADOW_BLOB_TEXTURE);
if (!actor.shadowShown || Game::Read<int>(Offsets::VAR_SHADOW_LOD) != 1 || texture == nullptr)
return;
@@ -519,11 +542,11 @@ void DrawShadowWithStates(Actor &actor, World::Backdrop *world, const float (&ey
Gx::SetState(Offsets::GXRS_TEXTURE0, Texture::DeviceTexture(texture));
Gx::SetStateWord(Offsets::GXRS_FOG_COLOR, 0xFFFFFFFFu);
Gx::SetState(Offsets::GXRS_COMBINER0, Offsets::GXRS_COMBINE_BLEND_DIFFUSE_ALPHA);
if (world == nullptr)
if (!ground.Any())
Gx::SetState(Offsets::GXRS_CULL, 0);
else if (Game::Read<float>(Offsets::VAR_SHADOW_BIAS) == 0.0f)
Gx::SetState(Offsets::GXRS_DEPTH_FUNC, Offsets::GXRS_DEPTH_FUNC_DECAL);
DrawShadow(actor, world, eye);
DrawShadow(actor, ground, eye);
Gx::PopStates();
}
@@ -536,9 +559,9 @@ void DrawShadowWithStates(Actor &actor, World::Backdrop *world, const float (&ey
// state push, in the scene's fog for the world's (World::CommitDecalFog),
// the circle's state (FUN_SELECTION_CIRCLE_STATE: blend 3, which
// adds by source alpha, UnitSelectTexture.blp and its ramp) and the decal at
// fade 0.5, flags 0x200122. In a scene with a WMO the decal lands on its
// faces (World::DrawDecal), as the shadows do; without one, on the flat
// ground at the feet (World::DrawDecalOnPlane).
// fade 0.5, flags 0x200122. In a scene with a WMO or terrain the decal lands
// on their faces (World::DrawDecal), as the shadows do; without either, on
// the flat ground at the feet (World::DrawDecalOnPlane).
//
// The circle: the box about the feet, half size r across and 2r up and
// down, r = the radius the unit takes when its model loads (0x0060AEE0,
@@ -554,7 +577,7 @@ bool SelectionDecalReady() {
Game::Read<void *>(Offsets::VAR_SELECTION_CIRCLE_RAMP) != nullptr;
}
void DrawSelectionDecal(void *frame, World::Backdrop *world, const float (&box)[6], uint32_t argb,
void DrawSelectionDecal(void *frame, const Ground &ground, const float (&box)[6], uint32_t argb,
const float (&point)[3], float groundZ, const float (&eye)[3]) {
using Heading_t = float(__fastcall *)(const float *from, const float *to);
using CircleState_t = void(__thiscall *)(void *obj);
@@ -566,10 +589,10 @@ void DrawSelectionDecal(void *frame, World::Backdrop *world, const float (&box)[
float fade;
std::memcpy(&fade, &fadeBits, sizeof fade);
Gx::PushStates();
World::CommitDecalFog(world, frame, point, eye);
World::CommitDecalFog(ground.world, frame, point, eye);
reinterpret_cast<CircleState_t>(Offsets::FUN_SELECTION_CIRCLE_STATE)(nullptr);
if (world != nullptr)
World::DrawDecal(*world, box, argb, m, fade, Offsets::DECAL_SELECTION_FLAGS, eye);
if (ground.Any())
World::DrawDecal(ground.world, ground.terrain, box, argb, m, fade, Offsets::DECAL_SELECTION_FLAGS, eye);
else
World::DrawDecalOnPlane(box, argb, m, fade, groundZ, eye);
Gx::PopStates();
@@ -593,7 +616,7 @@ float SelectionRadius(void *model) {
return std::sqrt(std::sqrt(dx * dx + dy * dy) * Game::Read<float>(Offsets::VAR_HALF));
}
void DrawSelectionCircle(void *frame, Actor &actor, World::Backdrop *world, const float (&eye)[3]) {
void DrawSelectionCircle(void *frame, Actor &actor, const Ground &ground, const float (&eye)[3]) {
void *model = actor.model;
if (!actor.selectionCircleShown || !actor.shown || model == nullptr || M2::Parent(model) != nullptr ||
!M2::IsReady(model, 0, 0))
@@ -605,10 +628,10 @@ void DrawSelectionCircle(void *frame, Actor &actor, World::Backdrop *world, cons
const float feet[3] = {m[12], m[13], m[14]};
const float box[6] = {feet[0] - r, feet[1] - r, feet[2] - 2.0f * r,
feet[0] + r, feet[1] + r, feet[2] + 2.0f * r};
DrawSelectionDecal(frame, world, box, actor.selectionCircle, feet, feet[2], eye);
DrawSelectionDecal(frame, ground, box, actor.selectionCircle, feet, feet[2], eye);
}
void DrawMoveMarker(void *frame, World::Backdrop *world, const float (&eye)[3]) {
void DrawMoveMarker(void *frame, const Ground &ground, const float (&eye)[3]) {
const Fields &f = F(frame);
if (!f.moveMarkerShown)
return;
@@ -623,7 +646,7 @@ void DrawMoveMarker(void *frame, World::Backdrop *world, const float (&eye)[3])
const float s = wave * (high - low) + low;
const float(&p)[3] = f.moveMarker;
const float box[6] = {p[0] - s, p[1] - s, p[2] - s, p[0] + s, p[1] + s, p[2] + s};
DrawSelectionDecal(frame, world, box, Offsets::AUTOMOVE_MARKER_COLOR, p, p[2], eye);
DrawSelectionDecal(frame, ground, box, Offsets::AUTOMOVE_MARKER_COLOR, p, p[2], eye);
}
// ---- the ground-target reticle (SetGroundReticle) ----------------------------------
@@ -638,11 +661,11 @@ void DrawMoveMarker(void *frame, World::Backdrop *world, const float (&eye)[3])
// selection circle does) and fade 0.5, onto terrain and buildings. It is
// drawn after the opaque models and before the water and the translucent
// models (0x004836B0), in the world's fog, so here in the scene's
// (World::CommitDecalFog). Without a WMO it lies on the flat ground at the
// actor's height. The textures exist once the world frame is built, so at
// (World::CommitDecalFog). Without a WMO or terrain it lies on the flat
// ground at the actor's height. The textures exist once the world frame is built, so at
// the login and character screens no reticle shows.
void DrawGroundReticle(void *frame, const Actor &actor, World::Backdrop *world, const float (&eye)[3]) {
void DrawGroundReticle(void *frame, const Actor &actor, const Ground &ground, const float (&eye)[3]) {
if (!actor.groundReticleShown || !actor.shown)
return;
void *texture = Game::Read<void *>(actor.groundReticleValid ? Offsets::VAR_RETICLE_TEXTURE_ACCEPTABLE
@@ -659,37 +682,38 @@ void DrawGroundReticle(void *frame, const Actor &actor, World::Backdrop *world,
float fade;
std::memcpy(&fade, &fadeBits, sizeof fade);
Gx::PushStates();
World::CommitDecalFog(world, frame, p, eye);
World::CommitDecalFog(ground.world, frame, p, eye);
Gx::SetState(Offsets::GXRS_LIGHTING, 0);
Gx::SetState(Offsets::GXRS_DEPTH_WRITE, 0);
Gx::SetState(Offsets::GXRS_BLEND, Offsets::RETICLE_BLEND);
Gx::SetState(Offsets::GXRS_TEXTURE0, Texture::DeviceTexture(texture));
if (world != nullptr)
World::DrawDecal(*world, box, Offsets::RETICLE_COLOR, m, fade, Offsets::DECAL_SELECTION_FLAGS, eye);
if (ground.Any())
World::DrawDecal(ground.world, ground.terrain, box, Offsets::RETICLE_COLOR, m, fade,
Offsets::DECAL_SELECTION_FLAGS, eye);
else
World::DrawDecalOnPlane(box, Offsets::RETICLE_COLOR, m, fade, p[2], eye);
Gx::PopStates();
}
void DrawGroundReticles(void *frame, const float (&eye)[3]) {
World::Backdrop *world = CameraOf(frame) != nullptr ? F(frame).world : nullptr;
const Ground ground = GroundOf(frame);
for (const Actor *actor : *F(frame).actors)
DrawGroundReticle(frame, *actor, world, eye);
DrawGroundReticle(frame, *actor, ground, eye);
}
// What the world lays on the ground under its entities, for the actors.
void DrawGroundDecals(void *frame, const float (&eye)[3]) {
World::Backdrop *world = CameraOf(frame) != nullptr ? F(frame).world : nullptr;
const Ground ground = GroundOf(frame);
const bool decals = SelectionDecalReady();
void *cvar = Game::Read<void *>(Offsets::VAR_CVAR_OBJECT_SELECTION_CIRCLE);
const bool circles = decals && cvar != nullptr && CVar::Factory::GetInt(cvar) != 0;
for (Actor *actor : *F(frame).actors) {
if (circles)
DrawSelectionCircle(frame, *actor, world, eye);
DrawShadowWithStates(*actor, world, eye);
DrawSelectionCircle(frame, *actor, ground, eye);
DrawShadowWithStates(*actor, ground, eye);
}
if (decals)
DrawMoveMarker(frame, world, eye);
DrawMoveMarker(frame, ground, eye);
}
void __fastcall Paint(void *frame) {
@@ -729,29 +753,54 @@ void __fastcall Paint(void *frame) {
Gx::SetViewport({rect[1] * vp.maxX, rect[3] * vp.maxX, rect[0] * vp.maxY, rect[2] * vp.maxY,
0.0f, 1.0f});
World::Backdrop *world = CameraOf(frame) != nullptr ? F(frame).world : nullptr;
Terrain::Land *terrain = CameraOf(frame) != nullptr ? F(frame).terrain : nullptr;
World::ViewVolume volume;
World::BuildViewVolume(volume, eye);
F(frame).probeDrop = World::ProbeDrop(volume, eye);
// The world's frame under a liquid: its fog the map's for that liquid.
const uint32_t liquid = CameraLiquid(frame);
const uint32_t map = terrain != nullptr ? Terrain::MapOf(*terrain)
: world != nullptr ? World::MapOf(*world)
: World::kNoMap;
const World::LiquidFog liquidFog(frame, map, liquid, eye, F(frame).farClip);
const bool wet = liquid != Offsets::LIQUID_DRY;
if (world != nullptr)
World::ClearAndDrawSky(*world, frame, eye);
World::ClearAndDrawSky(*world, frame, eye, volume);
else if (terrain != nullptr)
World::ClearAndDrawOpenSky(frame, map, liquid, eye);
else
Gx::Clear(2);
PlaceActors(frame);
void *scene = Model::Pane::Scene(frame);
M2::AnimateScene(scene, eye);
// The world's frame draws its chunks before its map objects, and the
// terrain's magma after them (DrawWorldScene 0x006812B1..0x006812BB).
if (terrain != nullptr)
Terrain::Draw(*terrain, frame, eye, volume, F(frame).farClip);
if (world != nullptr)
World::Draw(*world, frame, eye);
World::Draw(*world, frame, eye, volume);
if (terrain != nullptr)
Terrain::DrawMagma(*terrain, frame, eye, volume, F(frame).farClip);
DrawGroundDecals(frame, eye);
M2::DrawScene(scene, 0);
DrawGroundReticles(frame, eye);
// The world's frame draws its liquids before the translucent models
// over the water and, with the camera in liquid, after them
// (0x004836D6).
const bool wet = world != nullptr && World::CameraWet(*world);
if (wet)
M2::DrawScene(scene, 1);
// RenderLiquid (0x006816D0): the terrain's ocean and river, then the
// map objects' liquids.
if (terrain != nullptr)
Terrain::DrawWater(*terrain, frame, eye, volume, F(frame).farClip, liquid);
if (world != nullptr)
World::DrawLiquids(*world, frame, eye);
Water::DrawRipples(F(frame).water, eye);
if (!wet)
M2::DrawScene(scene, 1);
Chains::Render(frame, eye);
// The world stage's particulates follow its models (0x006701E0).
Water::DrawDrift(F(frame).water, liquid);
}
}
@@ -976,6 +1025,10 @@ void Destroy(void *frame) {
f.actors = nullptr;
World::Free(f.world);
f.world = nullptr;
Terrain::Free(f.terrain);
f.terrain = nullptr;
Water::Free(f.water);
f.water = nullptr;
Time::Timer::CancelOwned(frame);
}
@@ -1045,13 +1098,21 @@ void __fastcall SetFrameAlpha(void *self, void *, uint8_t alpha) {
void __fastcall OnFrameUpdate(void *self, void *, float elapsedSec) {
for (Actor *actor : *F(self).actors)
actor->Link();
if (World::Backdrop *world = F(self).world) {
World::Backdrop *world = F(self).world;
Terrain::Land *terrain = F(self).terrain;
if (world != nullptr)
World::Update(*world, self, F(self).playWorldModelSounds, elapsedSec);
// A seated rider or an attached actor takes its parent's light.
if (terrain != nullptr)
Terrain::Update(*terrain, self, F(self).playWorldModelSounds);
const uint32_t lastLiquid = F(self).cameraLiquid;
F(self).cameraLiquid = world != nullptr ? World::CameraLiquid(*world) : OutdoorCameraLiquid(self);
Water::Update(F(self).water, lastLiquid, F(self).cameraLiquid, CameraView(self).eye, elapsedSec,
static_cast<double>(ClockMs(self)) / 1000.0);
// A seated rider or an attached actor takes its parent's light.
if (world != nullptr || terrain != nullptr)
for (Actor *actor : *F(self).actors)
if (actor->model != nullptr && M2::Parent(actor->model) == nullptr && M2::IsLoaded(actor->model, 0, 0))
World::Settle(*world, *actor->ambience, self, actor->model, elapsedSec);
}
World::Settle(world, terrain, *actor->ambience, self, actor->model, elapsedSec);
const uint32_t before = ClockMs(self);
if (F(self).paused)
FrameBase<OnFrameUpdate_t>(Offsets::VTBL_FRAME_ON_FRAME_UPDATE)(self, nullptr, elapsedSec);
@@ -1085,6 +1146,8 @@ void __fastcall UpdateModel(void *self) { PlaceActors(self); }
int __fastcall Hide(void *self, void *) {
if (World::Backdrop *world = F(self).world)
World::Silence(*world);
if (Terrain::Land *terrain = F(self).terrain)
Terrain::Silence(*terrain);
for (Actor *actor : *F(self).actors) {
Casts::Silence(*actor);
Areas::Silence(*actor);
@@ -1153,6 +1216,10 @@ void *__fastcall CreateModelScene(void *parent) {
f.groundTerrain = -1;
f.hovered = nullptr;
f.world = nullptr;
f.terrain = nullptr;
f.cameraLiquid = Offsets::LIQUID_DRY;
f.probeDrop = 0.0f;
f.water = nullptr;
f.playWorldModelSounds = false;
f.listener = nullptr;
f.clock = 0.0;
@@ -1588,23 +1655,53 @@ int __fastcall Script_ClearWorldModel(void *L) {
return 0;
}
// The WMO's nearest face on a segment (a ClassicAPI extension): the hit
// point, the face's normal toward the start, and the fraction. Nothing on a
// miss or without a WMO.
// The scene's terrain (a ClassicAPI extension, model/SceneTerrain.cpp). A
// set that fails clears it, as SetWorldModel does.
void SetTerrain(void *frame, Terrain::Land *terrain) {
Terrain::Free(F(frame).terrain);
F(frame).terrain = terrain;
}
constexpr char kUsageSetWorldTerrain[] = "SetWorldTerrain(mapID, x, y [, radius])";
int __fastcall Script_SetWorldTerrain(void *L) {
void *frame = Self(L);
if (frame == nullptr)
return 0;
const char *owner = Game::Lua::ObjectDisplayName(frame);
if (!NumberArgs(L, 2, 3, owner, kUsageSetWorldTerrain))
return 0;
const bool hasRadius = Game::Lua::Type(L, 5) > Game::Lua::TYPE_NIL;
if (hasRadius && !NumberArgs(L, 5, 1, owner, kUsageSetWorldTerrain))
return 0;
// One tile's width by default, the world's own unit of terrain.
const float radius = hasRadius ? FloatArg(L, 5) : Game::Read<float>(Offsets::VAR_TILE_SIZE);
SetTerrain(frame, Terrain::Load(frame, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)), FloatArg(L, 3),
FloatArg(L, 4), radius));
Game::Lua::PushBool(L, F(frame).terrain != nullptr);
return 1;
}
int __fastcall Script_ClearWorldTerrain(void *L) {
if (void *frame = Self(L))
SetTerrain(frame, nullptr);
return 0;
}
// The nearest face of the scene's ground on a segment (a ClassicAPI
// extension): the hit point, the face's normal toward the start, and the
// fraction. Nothing on a miss or without ground.
int __fastcall Script_TraceSegment(void *L) {
void *frame = Self(L);
if (frame == nullptr || !NumberArgs(L, 2, 6, Game::Lua::ObjectDisplayName(frame),
"TraceSegment(startX, startY, startZ, endX, endY, endZ [, camera])"))
return 0;
const World::Backdrop *world = F(frame).world;
if (world == nullptr)
return 0;
const float start[3] = {FloatArg(L, 2), FloatArg(L, 3), FloatArg(L, 4)};
const float end[3] = {FloatArg(L, 5), FloatArg(L, 6), FloatArg(L, 7)};
float fraction = 1.0f;
float normal[3];
const bool camera = Game::Lua::ToBoolean(L, 8) != 0;
if (!World::Trace(*world, start, end, camera, fraction, normal))
if (!TraceGround(frame, start, end, camera, fraction, normal))
return 0;
for (int i = 0; i < 3; ++i)
Game::Lua::PushNumber(L, start[i] + (end[i] - start[i]) * fraction);
@@ -1614,21 +1711,18 @@ int __fastcall Script_TraceSegment(void *L) {
return 7;
}
// The liquid at a point of the scene's WMO (a ClassicAPI extension): its type
// and its surface's z, nil for a group that is liquid throughout. Nothing
// when the point is dry or the scene has no WMO.
// The liquid at a point of the scene's WMO or terrain (a ClassicAPI
// extension): its type and its surface's z, nil for a WMO group that is
// liquid throughout. Nothing when the point is dry.
int __fastcall Script_GetLiquidAtPoint(void *L) {
void *frame = Self(L);
if (frame == nullptr ||
!NumberArgs(L, 2, 3, Game::Lua::ObjectDisplayName(frame), "GetLiquidAtPoint(pointX, pointY, pointZ)"))
return 0;
const World::Backdrop *world = F(frame).world;
if (world == nullptr)
return 0;
const float point[3] = {FloatArg(L, 2), FloatArg(L, 3), FloatArg(L, 4)};
uint32_t type = 0;
float surface = 0.0f;
if (!World::LiquidAt(*world, point, type, surface))
if (!LiquidAtPoint(frame, point, type, surface))
return 0;
Game::Lua::PushNumber(L, type);
if (surface >= FLT_MAX)
@@ -1638,7 +1732,52 @@ int __fastcall Script_GetLiquidAtPoint(void *L) {
return 2;
}
// A body swept through the scene's WMO to its first contact (a ClassicAPI
// Sweeps a body by `move` to its first contact with the scene's ground, as
// the client's mover sweeps a unit's volume (CMovement::SweepVolume
// 0x00632BA0, SceneContact.h): a box `radius` from `feet` on each side and
// `height` tall over a downward point at `feet`. `end` is where the feet
// stop; true when a face was met, `normal` the facet's plane normal. With
// no ground the body moves the full distance; the feet stay put while the
// WMO is loading.
bool SweepGround(void *frame, const float (&feet)[3], float radius, float height, const float (&move)[3],
float (&end)[3], float (&normal)[3]) {
normal[0] = normal[1] = normal[2] = 0.0f;
if (!HasGround(frame)) {
for (int k = 0; k < 3; ++k)
end[k] = feet[k] + move[k];
return false;
}
std::copy(std::begin(feet), std::end(feet), std::begin(end));
if (!(radius > 0.0f) || !(height > 0.0f))
return false;
// The move as the substep loop (0x00634040) hands it on: its length at
// 53 bits, the unit direction by the reciprocal, none below 2^-20.
const double length = std::sqrt((static_cast<double>(move[0]) * move[0] + static_cast<double>(move[1]) * move[1]) +
static_cast<double>(move[2]) * move[2]);
float dir[3] = {0.0f, 0.0f, 0.0f};
if (!(length < 9.5367431640625e-7)) {
const double inverse = 1.0 / length;
for (int k = 0; k < 3; ++k)
dir[k] = static_cast<float>(move[k] * inverse);
}
// The facets start empty over the volume's own box, as the substep's
// query frame (0x00633840) leaves its cache; the sweep's gather then
// takes that box joined with the end of the move.
Contact::Facets facets{{}, {}, {}, &QueryGroundFacets, &QueryGroundLiquidFacets, frame};
Contact::BaseBox(feet, radius, height, facets.box);
Contact::Contact contact;
if (!Contact::Sweep(facets, false, false, feet, radius, height, dir, static_cast<float>(length), contact))
return false;
for (int k = 0; k < 3; ++k)
end[k] = static_cast<float>(feet[k] + static_cast<double>(dir[k]) * contact.distance);
if (contact.hit >= facets.solid.size())
return false;
std::copy(std::begin(facets.solid[contact.hit].plane.n), std::end(facets.solid[contact.hit].plane.n),
std::begin(normal));
return true;
}
// A body swept through the scene's ground to its first contact (a ClassicAPI
// extension): where its feet stop, and the normal of the face it met.
int __fastcall Script_SweepCollider(void *L) {
void *frame = Self(L);
@@ -1648,11 +1787,9 @@ int __fastcall Script_SweepCollider(void *L) {
return 0;
const float feet[3] = {FloatArg(L, 2), FloatArg(L, 3), FloatArg(L, 4)};
const float move[3] = {FloatArg(L, 7), FloatArg(L, 8), FloatArg(L, 9)};
float end[3] = {feet[0] + move[0], feet[1] + move[1], feet[2] + move[2]};
float normal[3] = {};
bool hit = false;
if (const World::Backdrop *world = F(frame).world)
hit = World::Sweep(*world, feet, FloatArg(L, 5), FloatArg(L, 6), move, end, normal);
float end[3];
float normal[3];
const bool hit = SweepGround(frame, feet, FloatArg(L, 5), FloatArg(L, 6), move, end, normal);
for (float c : end)
Game::Lua::PushNumber(L, c);
if (!hit)
@@ -1876,6 +2013,8 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetWorldModel", &Script_SetWorldModel},
{"SetWorldModelByMapID", &Script_SetWorldModelByMapID},
{"ClearWorldModel", &Script_ClearWorldModel},
{"SetWorldTerrain", &Script_SetWorldTerrain},
{"ClearWorldTerrain", &Script_ClearWorldTerrain},
{"TraceSegment", &Script_TraceSegment},
{"GetLiquidAtPoint", &Script_GetLiquidAtPoint},
{"SweepCollider", &Script_SweepCollider},
@@ -2071,6 +2210,22 @@ const Game::Doc::Function kSetWorldModelByMapID{
"map's positions. Returns false, and shows no WMO, for any other map.",
kWorldModelMapArg, kWorldModelRet, nullptr, true};
const Game::Doc::Function kClearWorldModel{"Removes the scene's WMO.", {}, {}, nullptr, true};
const Game::Doc::Field kWorldTerrainArgs[] = {
Req("mapID", "number", "A Map.dbc row with terrain, for example 0 (Eastern Kingdoms)."),
Req("x", "number", "The map's x, as the map places its terrain."),
Req("y", "number"),
Opt("radius", "number", "533.33331", "Chunks within this distance of (x, y) are shown: one map tile by default."),
};
const Game::Doc::Function kSetWorldTerrain{
"Shows the map's ground around a point the way the world draws it, with its textures, shadows and "
"level of detail, in map coordinates. The scene's light stands in for the sun, and its fog for the "
"zone's. Without a WMO, the map's sky shows behind it. Actors walk on it, and TraceSegment and SweepCollider find it; past the radius there is no "
"ground. The map's props on it (trees, plants, rocks) show too, and actors collide with them; its "
"grass shows near the camera, and its water and lava show; actors swim in the water, and under it the "
"camera takes the map's underwater fog. Returns false, and shows no terrain, when no ground lies within the "
"radius.",
kWorldTerrainArgs, kWorldModelRet, nullptr, true};
const Game::Doc::Function kClearWorldTerrain{"Removes the scene's terrain.", {}, {}, nullptr, true};
const Game::Doc::Field kTraceSegmentArgs[] = {
Req("startX", "number"), Req("startY", "number"), Req("startZ", "number"),
Req("endX", "number"), Req("endY", "number"), Req("endZ", "number"),
@@ -2088,10 +2243,10 @@ const Game::Doc::Field kTraceSegmentRets[] = {
Req("fraction", "number", "0 at the start, 1 at the end."),
};
const Game::Doc::Function kTraceSegment{
"Returns the first face of the WMO on the segment from the start to the end, in scene positions. "
"It tests the faces that the world's line of sight tests: walls, floors and roofs, not small details. "
"Returns nothing when the segment hits no face, when the scene has no WMO, or for parts of the WMO "
"that have not loaded.",
"Returns the first face of the WMO or of the scene's terrain on the segment from the start to the end, "
"in scene positions. It tests the faces that the world's line of sight tests: the WMO's walls, floors "
"and roofs, not small details, and the terrain. Returns nothing when the segment hits no face, when the "
"scene has no WMO and no terrain, or for parts of the WMO that have not loaded.",
kTraceSegmentArgs, kTraceSegmentRets, nullptr, true};
const Game::Doc::Field kLiquidArgs[] = {Req("pointX", "number"), Req("pointY", "number"),
Req("pointZ", "number")};
@@ -2101,9 +2256,9 @@ const Game::Doc::Field kLiquidRets[] = {
"The height of the surface above the point. Nil when that part of the WMO is liquid throughout."),
};
const Game::Doc::Function kGetLiquidAtPoint{
"Returns the liquid of the WMO that a scene point is in, as the world finds it for a unit that stands "
"there. Returns nothing when the point is above the surface or not in a liquid, when the scene has no "
"WMO, or for parts of the WMO that have not loaded.",
"Returns the liquid of the WMO or of the scene's terrain that a scene point is in, as the world finds "
"it for a unit that stands there: the WMO's first, then the terrain's. Returns nothing when the point "
"is above the surface or not in a liquid, or for parts of the WMO that have not loaded.",
kLiquidArgs, kLiquidRets, nullptr, true};
const Game::Doc::Field kSweepArgs[] = {
Req("feetX", "number"),
@@ -2124,12 +2279,12 @@ const Game::Doc::Field kSweepRets[] = {
Opt("normalZ", "number"),
};
const Game::Doc::Function kSweepCollider{
"Moves a body in a straight line until it touches the WMO or one of its doodads, as the world moves a "
"character into a wall or onto a floor. The body has the shape a character has in the world: a square "
"box, radius from the feet on each side and height tall, over a downward point at the feet. A face "
"blocks the body only from its front side. Returns where the feet stop, and the normal of the face the "
"body met, or only the feet when it met none. Without a WMO the body moves the full distance. While a "
"part of the WMO in the way is still loading, the body does not move.",
"Moves a body in a straight line until it touches the WMO, one of its doodads or the scene's terrain, "
"as the world moves a character into a wall or onto a floor. The body has the shape a character has "
"in the world: a square box, radius from the feet on each side and height tall, over a downward point "
"at the feet. A face blocks the body only from its front side. Returns where the feet stop, and the "
"normal of the face the body met, or only the feet when it met none. Without a WMO or terrain the body "
"moves the full distance. While a part of the WMO in the way is still loading, the body does not move.",
kSweepArgs, kSweepRets, nullptr, true};
const Game::Doc::Field kListenerArg[] = {
Opt("actor", "ModelSceneFrameActor", nullptr, "An actor of this scene. Without it, the camera hears the sounds."),
@@ -2260,6 +2415,8 @@ const Game::Doc::Method g_methodDocs[] = {
{"SetWorldModel", &kSetWorldModel},
{"SetWorldModelByMapID", &kSetWorldModelByMapID},
{"ClearWorldModel", &kClearWorldModel},
{"SetWorldTerrain", &kSetWorldTerrain},
{"ClearWorldTerrain", &kClearWorldTerrain},
{"TraceSegment", &kTraceSegment},
{"GetLiquidAtPoint", &kGetLiquidAtPoint},
{"SweepCollider", &kSweepCollider},
@@ -2342,6 +2499,66 @@ int GroundTerrain(void *sceneFrame) { return F(sceneFrame).groundTerrain; }
World::Backdrop *WorldOf(void *sceneFrame) { return F(sceneFrame).world; }
Terrain::Land *TerrainOf(void *sceneFrame) { return F(sceneFrame).terrain; }
Water::Effects *&WaterOf(void *sceneFrame) { return F(sceneFrame).water; }
bool QueryGroundFacets(void *sceneFrame, const float (&box)[6], std::vector<Contact::Facet> &out) {
const Fields &f = F(sceneFrame);
out.clear();
if (f.world == nullptr && f.terrain == nullptr)
return false;
if (f.world != nullptr && !World::QueryFacets(f.world, box, out))
return false;
return f.terrain == nullptr || Terrain::QueryFacets(*f.terrain, box, out);
}
bool QueryGroundLiquidFacets(void *sceneFrame, const float (&box)[6], std::vector<Contact::Facet> &out) {
const Fields &f = F(sceneFrame);
out.clear();
if (f.world == nullptr && f.terrain == nullptr)
return false;
if (f.world != nullptr && !World::QueryLiquidFacets(f.world, box, out))
return false;
return f.terrain == nullptr || Terrain::QueryLiquidFacets(*f.terrain, box, out);
}
bool LiquidAtPoint(void *sceneFrame, const float (&point)[3], uint32_t &type, float &surface) {
const Fields &f = F(sceneFrame);
if (f.world != nullptr && World::LiquidAt(*f.world, point, type, surface))
return true;
return f.terrain != nullptr && Terrain::LiquidAt(*f.terrain, point, type, surface);
}
uint32_t OutdoorCameraLiquid(void *sceneFrame) {
const View view = CameraView(sceneFrame);
const float probe[3] = {view.eye[0], view.eye[1], view.eye[2] - F(sceneFrame).probeDrop};
uint32_t type = 0;
float surface = 0.0f;
return LiquidAtPoint(sceneFrame, probe, type, surface) ? type : static_cast<uint32_t>(Offsets::LIQUID_DRY);
}
float CameraProbeDrop(void *sceneFrame) { return F(sceneFrame).probeDrop; }
uint32_t CameraLiquid(void *sceneFrame) { return F(sceneFrame).cameraLiquid; }
bool TraceGround(void *sceneFrame, const float (&start)[3], const float (&end)[3], bool camera, float &fraction,
float (&normal)[3]) {
const Fields &f = F(sceneFrame);
fraction = 1.0f;
bool hit = false;
float wmoFraction = 1.0f;
if (f.world != nullptr && World::Trace(*f.world, start, end, camera, wmoFraction, normal)) {
fraction = wmoFraction;
hit = true;
}
if (f.terrain != nullptr && Terrain::Trace(*f.terrain, start, end, fraction, normal))
hit = true;
return hit;
}
bool HasGround(void *sceneFrame) { return F(sceneFrame).world != nullptr || F(sceneFrame).terrain != nullptr; }
Actor *SoundListener(void *sceneFrame) { return F(sceneFrame).listener; }
const float *LightAmbient(void *sceneFrame) { return AmbientOf(sceneFrame); }
+239 -19
View File
@@ -292,6 +292,10 @@ class Actor : public Animation::Object {
void *dresser = nullptr;
// A unit's model (SetModelByUnit): the unit and what the copy holds.
uint64_t unitGuid = 0;
// The unit's race splash (ChrRaces +0x34, the unit's +0xB18), played as
// it wades in or out or starts to swim; 0 for a model without a unit.
int32_t splashSound = 0;
uint32_t nextRippleMs = 0; // the unit's +0xC78, on the scene's clock
Outfit::Lanes lanes = {};
Outfit::Weapons weapons;
Outfit::Worn worn = {}; // what each inventory slot wears (GetItemTransmogInfoList)
@@ -611,9 +615,22 @@ void __stdcall OnModelEvent(uint32_t tag, uint32_t data, const float *point, uin
uint32_t userLow, uint32_t userHigh);
} // namespace Events
namespace Terrain {
struct Land;
} // namespace Terrain
// The scene's WMO (SetWorldModel; model/SceneWorld.cpp).
namespace World {
struct Backdrop;
// The scene's view volume in scene space (the device's view moved by -eye
// and its projection), as the world's camera builds its own. The paint
// builds it once, after its camera transforms, and its passes all read it:
// none changes the view or projection (the sky's draw 0x006D4940 sets only
// the viewport, and puts it back).
struct ViewVolume {
alignas(16) uint8_t bytes[Offsets::FRUSTUM_VOLUME_SIZE] = {};
};
void BuildViewVolume(ViewVolume &volume, const float (&eye)[3]);
// Whether `path` names a WMO (ends in ".wmo"), which only the scene shows.
bool IsWmoPath(const char *path);
// Null when the engine cannot read `path` safely: it is not a WMO, a file
@@ -641,15 +658,23 @@ void Silence(Backdrop &backdrop);
// with the camera outside the WMO's interior groups and dry, the world's sky
// (the dome, the sun and moons, the stars, the clouds and the skyboxes) in
// the device's view, lit by the backdrop map's light at the camera.
void ClearAndDrawSky(const Backdrop &backdrop, void *frame, const float (&eye)[3]);
void ClearAndDrawSky(const Backdrop &backdrop, void *frame, const float (&eye)[3], const ViewVolume &volume);
// The same without a WMO, as the world's frame does in the open (the weather
// byte stands set outside every map object): dry, the clear to black and the
// sky in map `mapID`'s light at the camera; under a liquid, the clear to the
// frame's fog colour (the zone's, LiquidFog) and no sky.
void ClearAndDrawOpenSky(void *frame, uint32_t mapID, uint32_t cameraLiquid, const float (&eye)[3]);
// From the paint, after the animate pass: the WMO in the device's view.
void Draw(Backdrop &backdrop, void *frame, const float (&eye)[3]);
void Draw(Backdrop &backdrop, void *frame, const float (&eye)[3], const ViewVolume &volume);
// From the paint, between the scene's opaque and translucent lists: the
// liquids of the WMO's groups (the world draws them between its model lists).
void DrawLiquids(Backdrop &backdrop, void *frame, const float (&eye)[3]);
// Whether the camera is in one of the WMO's liquids: the world then draws the
// liquids after the translucent models.
bool CameraWet(const Backdrop &backdrop);
// The camera's liquid type (0xF dry), as the scene's last update found it
// (CameraEnvironment 0x006809C0): the claimed group's, else the scene's
// liquid at the camera's probe point.
uint32_t CameraLiquid(const Backdrop &backdrop);
// The map whose light the backdrop takes, or kNoMap.
uint32_t MapOf(const Backdrop &backdrop);
// How the world lights a unit that stands in a WMO, kept per actor: the
// colour of the face under it, or the sun, and the WMO's lights near it.
struct Ambience;
@@ -657,13 +682,27 @@ Ambience *NewAmbience();
void FreeAmbience(Ambience *ambience);
// From the scene's update: places the model in the WMO again when it moved,
// or when more of the WMO has loaded since.
void Settle(const Backdrop &backdrop, Ambience &ambience, void *frame, void *model, float elapsed);
void Settle(const Backdrop *backdrop, const Terrain::Land *terrain, Ambience &ambience, void *frame, void *model,
float elapsed);
// The actor's lighting slots: the frame's light and fog without a WMO, or
// before the model is placed in this one.
void ActorLight(const Backdrop *backdrop, const Ambience &ambience, void *frame, void *record);
// The TerrainType id of the WMO face under the actor, or -1 (no face, no
// WMO, or not placed in this one).
int Ground(const Backdrop *backdrop, const Ambience &ambience);
int Ground(const Backdrop *backdrop, const Terrain::Land *terrain, const Ambience &ambience);
// Whether a footstep at `point` is wet (IsPointAtLiquidSurface 0x0060A740):
// the actor, placed outdoors, stands in a liquid whose LiquidType splashes,
// and the point is from 0.01 above to 2/3 yard below its surface.
bool FootstepWet(const Backdrop *backdrop, const Terrain::Land *terrain, const Ambience &ambience,
const float (&point)[3]);
// The surface a unit's ripples float on (EmitWaterRipple 0x005FA760): the
// actor, placed outdoors, stands in a liquid whose LiquidType has flag 0x1.
bool RippleSurface(const Backdrop *backdrop, const Terrain::Land *terrain, const Ambience &ambience, float &surface);
// The liquid triangles in a box (scene space) a ripple lies on (EmitRipple
// 0x0068F5C0: TestWorldBox with class 0xF0000, the WMO's then the
// terrain's), appended to `out` as nine floats each.
void LiquidTriangles(const Backdrop *backdrop, const Terrain::Land *terrain, const float (&box)[6],
std::vector<float> &out);
// The nearest face of the loaded groups that the segment from `start` to
// `end` (scene space) crosses, of the faces the world's line of sight tests,
// or with `camera` the faces its camera collides with: its fraction along the
@@ -686,18 +725,12 @@ bool QueryFacets(void *backdrop, const float (&box)[6], std::vector<Contact::Fac
// water and ocean surfaces of the WMO's liquid groups in a box (scene space),
// as their tiles' triangles. False without a WMO or before it has loaded.
bool QueryLiquidFacets(void *backdrop, const float (&box)[6], std::vector<Contact::Facet> &out);
// Sweeps a body by `move` to its first contact with the WMO's collision
// faces, as the client's mover sweeps a unit's volume (CMovement::SweepVolume
// 0x00632BA0, SceneContact.h): a box `radius` from `feet` on each side and
// `height` tall over a downward point at `feet`. `end` is where the feet
// stop; true when a face was met, `normal` the facet's plane normal (scene
// space). The feet stay put before the WMO has loaded.
bool Sweep(const Backdrop &backdrop, const float (&feet)[3], float radius, float height, const float (&move)[3],
float (&end)[3], float (&normal)[3]);
// From the paint, after Draw: a texture laid over the WMO's faces in `box`
// (scene space), as the world lays a decal on its ground.
void DrawDecal(const Backdrop &backdrop, const float (&box)[6], uint32_t argb, const float (&m)[16], float fade,
uint32_t mask, const float (&eye)[3]);
// From the paint, after Draw: a texture laid over the faces of the WMO
// and of the terrain in `box` (scene space), as the world lays a decal on
// its ground (DrawDecal 0x006D7330, TestWorldBox 0x006AD2C0: map objects,
// then terrain). Either may be null.
void DrawDecal(const Backdrop *backdrop, const Terrain::Land *terrain, const float (&box)[6], uint32_t argb,
const float (&m)[16], float fade, uint32_t mask, const float (&eye)[3]);
// The same decal in a scene without a WMO: laid on the flat ground at
// height `z`, one quad over the box.
void DrawDecalOnPlane(const float (&box)[6], uint32_t argb, const float (&m)[16], float fade, float z,
@@ -705,10 +738,197 @@ void DrawDecalOnPlane(const float (&box)[6], uint32_t argb, const float (&m)[16]
// The scene's fog on the device, for a decal at `point` that the world draws
// in its own fog: the WMO's, or the frame's without one. Inside a state push.
void CommitDecalFog(const Backdrop *backdrop, void *frame, const float (&point)[3], const float (&eye)[3]);
// A map's own doodads, outside any WMO (the terrain's MDDF): made, lit
// outdoors, faded and animated as the world does a chunk's doodads, with the
// WMO doodads' code.
struct Props;
Props *NewProps();
void FreeProps(Props *props);
// PlaceDoodadDef (0x00694BC0) for a 36-byte MDDF entry, then
// CreateDoodadModel; returns the prop's index.
uint32_t AddProp(Props &props, void *frame, const char *path, const uint8_t *entry);
// From the scene's update: bounds, fade, the animate list and sounds.
void UpdateProps(Props &props, void *frame, bool sounds);
void SilenceProps(Props &props);
// One box query: BeginPropQuery once, then AddPropFacets for each prop a
// gathered chunk holds (a prop two chunks hold gives its facets once). False
// while the prop's model is loading.
void BeginPropQuery(Props &props);
bool AddPropFacets(Props &props, uint32_t prop, const float (&box)[6], std::vector<Contact::Facet> &out);
// How far below the eye the camera's liquid probe is (0x006809E2): the
// lowest of the eye and the four near corners of the view volume, from the
// paint (the device's view and projection).
float ProbeDrop(const ViewVolume &volume, const float (&eye)[3]);
// The fog half of the frame's light callback: the scene's fog on a lighting
// record, nothing when the scene has none.
void FrameFog(void *record, void *frame);
// The world's facet batches emptied (what every facet query does first), and
// their triangles as facets appended to `out` (the facet emitter 0x00671CC0:
// the vertices through each batch's matrix).
void ResetFacetBatches();
void EmitFacetBatches(std::vector<Contact::Facet> &out);
// While it lives, the water fields of the world's light table (the river and
// ocean colours and alphas) are map `mapID`'s at `eye` (Light.dbc, as the
// world's light build makes them), for a scene's liquids; the world's again
// after. Nothing changes for kNoMap or a map without a light.
class MapWater {
public:
static constexpr size_t kFields = 8;
MapWater(uint32_t mapID, uint32_t cameraLiquid, const float (&eye)[3]);
~MapWater();
MapWater(const MapWater &) = delete;
MapWater &operator=(const MapWater &) = delete;
bool Swapped() const { return swapped_; }
private:
uint32_t saved_[kFields] = {};
bool swapped_ = false;
};
// While it lives, with the camera in a liquid, the frame's fog (which stands
// for the zone's) is map `mapID`'s fog at `eye` for that liquid: the world
// builds its light table with the camera's liquid (the underwater, magma or
// slime light) and takes the scene fog from it (BuildSceneFog 0x006CEE30).
// The scene's own fog again after. Nothing changes while dry, for kNoMap or
// for a map without a light.
class LiquidFog {
public:
LiquidFog(void *frame, uint32_t mapID, uint32_t cameraLiquid, const float (&eye)[3], float farClip);
~LiquidFog();
LiquidFog(const LiquidFog &) = delete;
LiquidFog &operator=(const LiquidFog &) = delete;
private:
void *frame_ = nullptr;
uint32_t saved_[4] = {}; // the fog's flags, colour, near and far
};
} // namespace World
// The scene's terrain (SetWorldTerrain; model/SceneTerrain.cpp).
namespace Terrain {
struct Land;
// The chunks of map `mapID`'s ADT tiles within `radius` of (x, y), in map
// coordinates; null when the map has no directory or no chunk in reach
// opens and reads.
Land *Load(void *frame, uint32_t mapID, float x, float y, float radius);
void Free(Land *land);
// From the paint, after the animate pass and before the WMO: the chunks the
// scene's view shows, as the world's chunk pass draws them. `farClip` is the
// scene's, the fog end of a scene without fog.
void Draw(Land &land, void *frame, const float (&eye)[3], const World::ViewVolume &volume, float farClip);
// From the paint, after the WMO: the chunks' magma (DrawWorldScene's magma
// pass 0x006855A0 follows the map objects).
void DrawMagma(Land &land, void *frame, const float (&eye)[3], const World::ViewVolume &volume, float farClip);
// From the paint, before the WMO's liquids: the chunks' ocean and river
// (RenderLiquid 0x006816D0), in the colours of the terrain's map at the eye
// for the camera's liquid.
void DrawWater(Land &land, void *frame, const float (&eye)[3], const World::ViewVolume &volume, float farClip,
uint32_t cameraLiquid);
// From the scene's update, before it advances: the chunks' doodads.
void Update(Land &land, void *frame, bool sounds);
// Drops the doodads' loop sounds (the scene hid).
void Silence(Land &land);
// The world box test's terrain leg for a decal (TestTerrainBox 0x006AD4E0):
// the triangles of the chunks' cells in `box` (scene space) into the
// client's triangle batches, one per chunk (TestChunkTriangles 0x006AD7E0).
// True when it added any.
bool CollectDecalBatches(const Land &land, const float (&box)[6], uint32_t mask);
// GetGroundEffect (0x0069B1C0) over the scene's chunks: the TerrainType id of
// the dominant texture layer of the cell under `point`. False with no chunk,
// a hole, no layer or no ground effect there.
bool GroundEffect(const Land &land, const float (&point)[3], int32_t &terrainType);
// This terrain among all loaded, for the actors' cached placements.
uint32_t Serial(const Land &land);
// The terrain's map.
uint32_t MapOf(const Land &land);
// The area id of the chunk under a point (0x0069B120), 0 without one.
uint32_t AreaAt(const Land &land, const float (&point)[3]);
// The world's box query's terrain leg (0x006AA8B0, GatherChunkFacets
// 0x006AADC0): the triangles of the chunks' cells in a box (scene space) and
// an impassable chunk's walls, appended to `out`. A box outside the map
// fails, as the world's does; a cell outside the scene's chunks has none.
bool QueryFacets(Land &land, const float (&box)[6], std::vector<Contact::Facet> &out);
// The world's segment trace's terrain leg (TraceTerrainSegment 0x0069C320):
// the nearest triangle of the cells the segment crosses below `fraction`,
// which it lowers, and that triangle's unit normal turned to face `start`.
// False when nothing nearer is hit.
bool Trace(const Land &land, const float (&start)[3], const float (&end)[3], float &fraction,
float (&normal)[3]);
// GetLiquidStatus's terrain leg (0x0069B6D0): the liquid of the chunk's
// slots at a point (scene space), the first whose tile there has a type and
// whose surface is above the point: the type (tile & 3) and the surface's z.
// False when the point is dry or outside the scene's chunks.
bool LiquidAt(const Land &land, const float (&point)[3], uint32_t &type, float &surface);
// The swimmer's box query's terrain leg (GatherChunkFacets 0x006AADC0 with
// class 0x30000): an impassable chunk's walls, then the river and ocean
// slots' cell triangles in `box` (scene space), appended to `out`. A box
// outside the map fails, as the world's does.
bool QueryLiquidFacets(const Land &land, const float (&box)[6], std::vector<Contact::Facet> &out);
} // namespace Terrain
// The water effects around a scene camera (model/SceneWater.cpp).
namespace Water {
struct Effects;
// From the scene's update, once the camera's liquid is known (`before` the
// last update's, `now` this one's): the ripples aged to `clock` (the
// scene's, in seconds), then the drift cloud scattered for a new liquid,
// made the first time, and advected with the camera.
void Update(Effects *&effects, uint32_t before, uint32_t now, const float (&eye)[3], float elapsed, double clock);
// From the paint, after the scene's models: the drift cloud, while the
// camera is in a liquid.
void DrawDrift(const Effects *effects, uint32_t cameraLiquid);
// What a unit's ripple reads of it (CGUnit_C::EmitWaterRipple 0x005FA760).
struct RippleSource {
float position[3]; // the feet
float facing;
float scale; // the object's scale
float height; // the collision height
uint32_t moveFlags;
float speed; // the current speed
float surface; // the liquid's z over the feet
};
// A unit's ring or wake, or with `splash` its swim splash's ring: `nextMs`
// is the unit's next ripple time (+0xC78) on the scene's clock `nowMs`.
void EmitUnitRipple(Effects *&effects, void *frame, const RippleSource &source, uint32_t &nextMs, uint32_t nowMs,
double clock, bool splash);
// From the paint, in the liquid phase after the liquids (UpdateRipples, the
// tail of RenderLiquid 0x006816D0): the live rings and wakes.
void DrawRipples(const Effects *effects, const float (&eye)[3]);
void Free(Effects *effects);
} // namespace Water
// The scene's water effects (made on first use).
Water::Effects *&WaterOf(void *sceneFrame);
// The scene's WMO, or null.
World::Backdrop *WorldOf(void *sceneFrame);
// The scene's terrain, or null.
Terrain::Land *TerrainOf(void *sceneFrame);
// The scene's ground, as the world's queries take map objects then the
// terrain (0x006AA8B0, 0x0069BFF0): the WMO's, then the terrain's.
// The box query a body's sweeps make (Contact::Facets with the scene frame as
// context): false without either, or while the WMO is loading.
bool QueryGroundFacets(void *sceneFrame, const float (&box)[6], std::vector<Contact::Facet> &out);
// The swimmer's liquid query: the WMO's liquids, then the terrain's.
bool QueryGroundLiquidFacets(void *sceneFrame, const float (&box)[6], std::vector<Contact::Facet> &out);
// The liquid at a point (scene space), as the world's liquid query finds it
// (GetLiquidStatus 0x0069B6D0): the WMO's, then the terrain's. The type (0
// water, 1 ocean, 2 magma, 3 slime) and the surface's z, FLT_MAX for a WMO
// group that is liquid throughout. False when the point is dry.
bool LiquidAtPoint(void *sceneFrame, const float (&point)[3], uint32_t &type, float &surface);
// The camera's liquid outside a WMO group (CameraEnvironment 0x006809C0 to
// 0x006723D0): the liquid at the eye lowered by the last paint's probe drop,
// 0xF when dry.
uint32_t OutdoorCameraLiquid(void *sceneFrame);
// The last paint's probe drop (World::ProbeDrop).
float CameraProbeDrop(void *sceneFrame);
// The camera's liquid as the scene's last update found it, 0xF when dry.
uint32_t CameraLiquid(void *sceneFrame);
// The nearest face on a segment of the WMO (its ground or `camera` face set)
// or the terrain: its fraction and its unit normal toward `start`.
bool TraceGround(void *sceneFrame, const float (&start)[3], const float (&end)[3], bool camera, float &fraction,
float (&normal)[3]);
// Whether the scene has any ground to trace against.
bool HasGround(void *sceneFrame);
// The scene light's ambient colour {r, g, b}: the colour an actor's highlight
// takes, as a unit's takes the world light state's ambient.
+46 -12
View File
@@ -392,6 +392,7 @@ void ReleaseModel(Actor &actor) {
actor.areaVisual = nullptr;
Areas::Stop(actor);
actor.unitGuid = 0;
actor.splashSound = 0;
actor.lanes[0] = actor.lanes[1] = Outfit::HeldLane{};
actor.weapons = Outfit::Weapons{};
actor.rangedDrawn = false;
@@ -489,6 +490,10 @@ bool Actor::SetUnitModel(uint64_t guid) {
return false;
unitGuid = guid;
const auto *descriptor = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
// PostInit (0x005FB880): the race's splash.
if (const uint8_t *race = DBC::Record(Offsets::VAR_CHRRACES_RECORDS, Offsets::VAR_CHRRACES_COUNT,
Game::Read<uint8_t>(descriptor, Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE)))
splashSound = Game::Read<int32_t>(race, Offsets::OFF_CHRRACES_SPLASH_SOUND);
armorReady = Game::Read<uint32_t>(descriptor, Offsets::OFF_UNIT_FIELD_DISPLAYID) ==
Game::Read<uint32_t>(descriptor, Offsets::OFF_UNIT_FIELD_NATIVEDISPLAYID);
dresser = Outfit::CloneComponent(copy, unit);
@@ -809,24 +814,49 @@ void LoadNode(Actor &actor, const void *node, void *status) {
Animation::Xml::LoadKeyValues(&actor, node, status);
}
// The movement manager's tick for the actor's body, against the scene's WMO,
// then the unit's step (CGUnit_C::OnMoverStepped 0x005FAC70): the liquid
// CGUnit_C::EmitWaterRipple (0x005FA760) for the actor's body: each update,
// and with `splash` after the swim decision's splash. Only a placed actor in
// a liquid that ripples leaves any.
void EmitRipple(Actor &actor, bool splash) {
void *frame = actor.SceneFrame();
float surface = 0.0f;
if (actor.mover == nullptr || !World::RippleSurface(WorldOf(frame), TerrainOf(frame), *actor.ambience, surface))
return;
const Mover::Body &body = *actor.mover;
const Water::RippleSource source{{body.position[0], body.position[1], body.position[2]},
body.facing,
actor.scale,
body.height,
body.flags,
body.speed,
surface};
const uint32_t now = ClockMs(frame);
Water::EmitUnitRipple(WaterOf(frame), frame, source, actor.nextRippleMs, now, static_cast<double>(now) / 1000.0,
splash);
}
// The movement manager's tick for the actor's body, against the scene's
// ground, then the unit's step (CGUnit_C::OnMoverStepped 0x005FAC70): the liquid
// over the feet, as the unit's liquid query finds it, and the swim decision.
// The client steps only a mover on the manager's list (moving, falling or
// with a queued command); an idle body here keeps its depth from tick to
// tick, so deciding again changes nothing.
void Advance(Actor &actor) {
Mover::Body &body = *actor.mover;
World::Backdrop *world = WorldOf(actor.SceneFrame());
body.facets.context = world;
if (!Mover::Tick(body, ClockMs(actor.SceneFrame())))
return;
uint32_t type = 0;
float surface = 0.0f;
const bool inLiquid = world != nullptr && World::LiquidAt(*world, body.position, type, surface);
const bool inLiquid = LiquidAtPoint(actor.SceneFrame(), body.position, type, surface);
const float depth = inLiquid ? static_cast<float>(static_cast<double>(surface) - body.position[2]) : 0.0f;
const bool swam = (body.flags & Mover::FLAG_SWIMMING) != 0;
Mover::UpdateSwimState(body, inLiquid, depth);
// The splash plays at the unit (PlaySplash 0x006237C0), with the actor's
// other sounds; its ripple is a world effect.
if (Mover::UpdateSwimState(body, inLiquid, depth)) {
if (actor.playAnimationSounds && actor.splashSound > 0)
Sounds::Play(actor.SceneFrame(), actor.splashSound, body.position);
EmitRipple(actor, true);
}
// The client's swim decision queues EnterSwim or StopSwimming, whose
// messages the unit animates.
const bool swims = (body.flags & Mover::FLAG_SWIMMING) != 0;
@@ -1143,6 +1173,8 @@ void Actor::Update(float elapsedSec, float sceneSec) {
MarkTwistBones(*this);
Locomotion::OnBodyTurn(*this, TurnBody(*this));
FaceDisplay(*this);
// CGUnit_C::Update's ripple (0x0060802E).
EmitRipple(*this, false);
}
if (model != nullptr && !loaded) {
bool ready = M2::IsLoaded(model, 0, 0);
@@ -2245,12 +2277,12 @@ int __fastcall Script_SetMovementEnabled(void *L) {
if (actor->mover == nullptr) {
actor->mover = std::make_unique<Mover::Body>();
Mover::Init(*actor->mover, actor->position, actor->yaw, ClockMs(actor->SceneFrame()));
actor->mover->facets.query = &World::QueryFacets;
actor->mover->facets.liquidQuery = &World::QueryLiquidFacets;
actor->mover->facets.query = &QueryGroundFacets;
actor->mover->facets.liquidQuery = &QueryGroundLiquidFacets;
actor->mover->stepped = &Locomotion::OnStep;
actor->mover->steppedContext = actor;
}
actor->mover->facets.context = WorldOf(actor->SceneFrame());
actor->mover->facets.context = actor->SceneFrame();
SizeMover(*actor);
PutDown(*actor);
ResetFacings(*actor);
@@ -3307,8 +3339,9 @@ const Game::Doc::Function kPlaySound{
const Game::Doc::Field kAnimationSoundsArg[] = {Req("enabled", "bool")};
const Game::Doc::Function kSetPlayAnimationSounds{
"Sets whether the model's animations play their sounds, as a unit's do in the world: fidgets, "
"the stand vocal, wing flaps, and on a scene with a ground terrain, footsteps and the death thud. They "
"are heard from the scene's camera. Off by default.",
"the stand vocal, wing flaps, and on a scene with a ground terrain, footsteps and the death thud. "
"Footsteps at the surface of water splash, and a unit's model plays its race's splash as it wades in "
"or out or starts to swim. They are heard from the scene's camera. Off by default.",
kAnimationSoundsArg, {}, nullptr, true};
const Game::Doc::Function kGetPlayAnimationSounds{
"Returns whether the model's animations play their sounds.", {}, kAnimationSoundsArg, nullptr,
@@ -3381,7 +3414,8 @@ const Game::Doc::Function kGetNamePosition{
const Game::Doc::Field kMovementEnabledArg[] = {Req("enabled", "bool")};
const Game::Doc::Function kSetMovementEnabled{
"Sets whether the actor moves as a character moves in the world: it walks and runs over the scene's "
"WMO, steps up and down ledges, slides along walls, falls, jumps, and swims in the WMO's liquids, at "
"WMO, steps up and down ledges, slides along walls, falls, jumps, and swims in the liquids of the WMO "
"and the terrain, at "
"a character's speeds. The body is the size the world gives the actor's creature display, at the "
"actor's scale. When you turn it on, and when you set the position while it is on, the actor falls "
"to the floor below it. While it is on, the actor's position, yaw and pitch follow the movement: "
+7 -6
View File
@@ -37,8 +37,9 @@
// and silences the loop, which starts again when the pause ends.
//
// The random draws are the client's own (its random state 0x00882664). The
// traces are the scene's WMO (World::Trace); a scene without a WMO has no
// floor, so there its shards land at the area's height. The scene clock
// traces are the scene's ground (TraceGround: its WMO, then its terrain); a
// scene without either has no floor, so there its shards land at the area's
// height. The scene clock
// times the shards, so they stop while the scene is paused.
#include "model/Scene.h"
@@ -166,12 +167,12 @@ void Spawn(Actor &actor) {
const float high[3] = {centre[0] + u * direction[0] * radius, centre[1] + us * radius,
centre[2] + Game::Read<float>(Offsets::VAR_AREA_SHARD_DROP_HEIGHT)};
const float low[3] = {centre[0], centre[1], centre[2] + Game::Read<float>(Offsets::VAR_ONE_SIXTH)};
const World::Backdrop *world = WorldOf(frame);
const bool ground = HasGround(frame);
float fraction = 1.0f;
float normal[3];
float start[3] = {high[0], high[1], high[2]};
// Short of a ceiling between the centre and the point up there.
if (world != nullptr && World::Trace(*world, low, high, false, fraction, normal)) {
if (ground && TraceGround(frame, low, high, false, fraction, normal)) {
const float share = fraction * Game::Read<float>(Offsets::VAR_AREA_SHARD_HIT_SHARE);
for (int i = 0; i < 3; ++i)
start[i] = low[i] + (high[i] - low[i]) * share;
@@ -180,11 +181,11 @@ void Spawn(Actor &actor) {
shard->frame = frame;
const float below[3] = {start[0], start[1], start[2] - Game::Read<float>(Offsets::VAR_AREA_SHARD_FALL)};
fraction = 1.0f;
if (world == nullptr) {
if (!ground) {
shard->position[0] = start[0];
shard->position[1] = start[1];
shard->position[2] = centre[2];
} else if (World::Trace(*world, start, below, false, fraction, normal)) {
} else if (TraceGround(frame, start, below, false, fraction, normal)) {
for (int i = 0; i < 3; ++i)
shard->position[i] = start[i] + (below[i] - start[i]) * fraction;
} else {
+16 -8
View File
@@ -32,7 +32,8 @@
// $FSD the footstep: +0x24 by the ground's terrain through the
// engine's GetFootstepSound, with FootstepSounds on
// (0x00623390); the ground is the scene's SetGroundTerrain,
// else the terrain type of the WMO face under the actor
// else the terrain type of the WMO face under the actor; wet
// (the splash) at the surface of a liquid that splashes
// $DTH the death thud by the size class and the ground, from the
// engine's own table (0x006236E0)
// $WNG, $WGG the wing flap +0x2C and glide +0x30 (vocal kinds 7 and 10,
@@ -161,20 +162,27 @@ void PlayStand(const Actor &actor, const uint8_t *soundData) {
}
// The ground the actor stands on: the scene's SetGroundTerrain, else the
// terrain type of the WMO face under it (the unit's +0xC60).
// terrain type of the WMO face or the terrain layer under it (the unit's
// +0xC60).
int Ground(const Actor &actor) {
void *frame = actor.SceneFrame();
const int set = GroundTerrain(frame);
return set >= 0 ? set : World::Ground(WorldOf(frame), *actor.ambience);
return set >= 0 ? set : World::Ground(WorldOf(frame), TerrainOf(frame), *actor.ambience);
}
void PlayFootstep(const Actor &actor, const uint8_t *soundData) {
// The footstep at the event's point: wet when the point is at the surface of
// a liquid that splashes (0x00623406).
void PlayFootstep(const Actor &actor, const uint8_t *soundData, const float *point) {
using GetFootstepSound_t = int(__fastcall *)(int footstepID, int terrainTypeID, int wet);
const int terrain = Ground(actor);
if (terrain < 0 || !CVarOn("FootstepSounds"))
return;
void *frame = actor.SceneFrame();
const bool wet = point != nullptr &&
World::FootstepWet(WorldOf(frame), TerrainOf(frame), *actor.ambience,
{point[0], point[1], point[2]});
Play(actor, reinterpret_cast<GetFootstepSound_t>(Offsets::FUN_GET_FOOTSTEP_SOUND)(
Game::Read<int32_t>(soundData, Offsets::OFF_CREATURESOUNDDATA_FOOTSTEP), terrain, 0));
Game::Read<int32_t>(soundData, Offsets::OFF_CREATURESOUNDDATA_FOOTSTEP), terrain, wet ? 1 : 0));
}
// PlayDeathThud's dry thud: the engine's table by size class and the
@@ -206,7 +214,7 @@ void PlayPrecast(const Actor &actor) {
}
}
void PlaySound(Actor &actor, uint32_t tag, uint32_t data) {
void PlaySound(Actor &actor, uint32_t tag, uint32_t data, const float *point) {
if (tag == kCSD) {
if (CVarOn("EmoteSounds"))
Play(actor, static_cast<int32_t>(data));
@@ -229,7 +237,7 @@ void PlaySound(Actor &actor, uint32_t tag, uint32_t data) {
else if (tag == kFDX)
PlayStand(actor, soundData);
else if (tag == kFSD)
PlayFootstep(actor, soundData);
PlayFootstep(actor, soundData, point);
else if (tag == kWNG)
Play(actor, Game::Read<int32_t>(soundData, Offsets::OFF_CREATURESOUNDDATA_WING_FLAP));
else if (tag == kWGG)
@@ -285,7 +293,7 @@ void __stdcall OnModelEvent(uint32_t tag, uint32_t data, const float *point, uin
return;
}
if (actor->playAnimationSounds)
PlaySound(*actor, tag, data);
PlaySound(*actor, tag, data, point);
}
} // namespace Model::Scene::Events
+70
View File
@@ -0,0 +1,70 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// A file in the client's archives, through SFile: the scene's WMO and terrain
// readers open the world's files the way the client does. A missing file
// opens nothing, with no error.
#include "Offsets.h"
#include <cstdint>
#include <vector>
namespace Model::Scene {
class File {
public:
explicit File(const char *path) {
using Open_t = int(__stdcall *)(const char *path, void **file);
if (reinterpret_cast<Open_t>(Offsets::FUN_SFILE_OPEN)(path, &handle_) == 0)
handle_ = nullptr;
}
~File() {
if (handle_ != nullptr)
reinterpret_cast<uint32_t(__stdcall *)(void *)>(Offsets::FUN_SFILE_CLOSE)(handle_);
}
File(const File &) = delete;
File &operator=(const File &) = delete;
bool IsOpen() const { return handle_ != nullptr; }
uint32_t Size() const {
using Size_t = uint32_t(__stdcall *)(void *file, uint32_t *high);
return reinterpret_cast<Size_t>(Offsets::FUN_SFILE_GET_FILE_SIZE)(handle_, nullptr);
}
bool Read(void *buffer, uint32_t size) {
using Read_t = int(__stdcall *)(void *file, void *buffer, uint32_t size, uint32_t *read,
void *overlapped, int);
uint32_t read = 0;
return handle_ != nullptr &&
reinterpret_cast<Read_t>(Offsets::FUN_SFILE_READ)(handle_, buffer, size, &read, nullptr,
0) != 0 &&
read == size;
}
// The whole file; false for a missing, empty or unsized file.
bool ReadAll(std::vector<uint8_t> &out) {
if (handle_ == nullptr)
return false;
const uint32_t size = Size();
if (size == 0 || size == 0xFFFFFFFFu)
return false;
out.resize(size);
return Read(out.data(), size);
}
private:
void *handle_ = nullptr;
};
} // namespace Model::Scene
+12 -3
View File
@@ -85,6 +85,7 @@ const Live<float> kTwoPi{Offsets::VAR_MOVER_TWO_PI};
const Live<float> kHalfPi{Offsets::VAR_HALF_PI}; // the pitch clamp
const Live<float> kNegHalfPi{Offsets::VAR_MINUS_HALF_PI};
const Live<float> kThreeQuarters{Offsets::VAR_THREE_QUARTERS};
const Live<float> kTwoFifths{Offsets::VAR_TWO_FIFTHS};
constexpr float kFltMax = FLT_MAX;
constexpr float kNegFltMax = -FLT_MAX;
const Live<float> kSnapSq{Offsets::VAR_MOVER_SNAP_DISTANCE_SQ}; // 3 yards squared
@@ -2384,9 +2385,9 @@ void KnockBack(Body &body, float cosine, float sine, float speedXY, float speedZ
body.speed = speedXY;
}
void UpdateSwimState(Body &body, bool inLiquid, float depth) {
bool UpdateSwimState(Body &body, bool inLiquid, float depth) {
if (body.flags & 0x400)
return;
return false;
const double enter = D(body.height) * kThreeQuarters;
const float leave = static_cast<float>(enter - kThirtySixth);
if (!(body.flags & FLAG_SWIMMING)) {
@@ -2396,10 +2397,18 @@ void UpdateSwimState(Body &body, bool inLiquid, float depth) {
StartSwimming(body);
ApplyLatches(body);
}
return;
// The splash (0x0060314A): the depth past two fifths of the height
// now and at the last test differ (a NaN on neither side), then the
// depth is kept.
const double splashAt = D(body.height) * kTwoFifths;
const bool now = D(depth) > splashAt;
const bool before = splashAt < D(body.splashDepth);
body.splashDepth = depth;
return now != before;
}
if (!inLiquid || !(depth >= leave))
StopSwimming(body);
return false;
}
float ModelPitch(const Body &body) {
+5 -2
View File
@@ -79,6 +79,7 @@ struct Body {
std::optional<Spline> spline; // +0xA4, null when absent
float radius; // +0xB0
float height; // +0xB4
float splashDepth; // the unit's +0x9A4: the depth the last splash test saw
float stepRatio; // +0xB8
uint32_t clock; // the movement manager's +0x128: the tick moved to
Contact::Facets facets; // the world query's list (0x00C4E52C)
@@ -175,8 +176,10 @@ bool OnSpline(const Body &body);
// player-controlled unit, after a tick: `depth` is the liquid's surface less
// the feet's z, 0 out of liquid. A body starts swimming deeper than three
// quarters of its height, unless it is still rising from a jump, and stops
// out of the liquid or 1/36 yard shallower than that.
void UpdateSwimState(Body &body, bool inLiquid, float depth);
// out of the liquid or 1/36 yard shallower than that. True when the unit
// splashes: not swimming before, the depth crossed two fifths of its height
// since the last test, either way.
bool UpdateSwimState(Body &body, bool inLiquid, float depth);
// GetCurrentFallSpeed (0x007C5D70): the vertical speed now, down positive.
float FallSpeed(const Body &body);
+1 -2
View File
@@ -135,8 +135,7 @@ void ReleaseLit(void *model, std::unique_ptr<EntityLight> &light) {
void SettleLight(void *model, EntityLight *light, float elapsed) {
if (model == nullptr || light == nullptr || !M2::IsLoaded(model, 0, 0))
return;
if (World::Backdrop *world = WorldOf(light->frame))
World::Settle(*world, *light->ambience, light->frame, model, elapsed);
World::Settle(WorldOf(light->frame), TerrainOf(light->frame), *light->ambience, light->frame, model, elapsed);
}
} // namespace Model::Scene
File diff suppressed because it is too large Load Diff
+487
View File
@@ -0,0 +1,487 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// The water effects the world shows around its liquids, for a scene's camera
// (benilla terrain/waterfx, terrain/liquid#drift-cloud).
//
// The drift cloud: the motes the camera sees in a liquid. The world builds one
// at start-up (0x0066F971: 0xFA40 bytes, CreateDriftCloud 0x0068E5A0 with the
// size base 1/36, the edge 30.0 and Textures\WaterPoop02.blp) and drives it
// from its camera: CameraEnvironment's change leg (0x00680AD9) scatters it in
// the new liquid's mode and sets its size base when the camera's liquid
// changes to water or ocean (1/36) or magma (1/9), and disables it in slime;
// each world frame advects it with the camera's step and its gust
// (0x0066FEE0 to AdvectDriftCloud 0x0068E930), and the world stage draws it
// after the liquids (0x006701E0 to DrawDriftCloud 0x0068EFE0), both while
// the camera is in a liquid and world flag 0x2000000 (waterParticulates) is
// set. A scene owns a cloud of its own, made the same way the first time its
// camera enters a liquid, and runs the same steps from its own camera: the
// advect reads the camera position and the frame seconds from the world's
// globals, so the scene's are staged in them around it. The draw works in
// the device's view, which the scene's paint has set.
//
// The ripples: the foam rings and wakes of units in shallow liquid. The world
// keeps them in one pool of 128 records that its own liquid query fills and
// its liquid pass draws (benilla terrain/waterfx); a scene keeps a pool of
// its own with the same records, filled from the scene's liquid triangles
// (World::LiquidTriangles) and drawn in the scene's liquid phase. The steps
// are transcribed: CGUnit_C::EmitWaterRipple (0x005FA760) for an actor,
// AddSplash's immediate leg (0x0068BDA0), AddRipple (0x0068F9F0), EmitRipple
// (0x0068F5C0), and UpdateRipples (0x0068FAE0) split into the scene's
// update (the ageing) and its paint (the draw). The scene's clock times them,
// so a paused scene holds its ripples.
#include "model/Scene.h"
#include "Game.h"
#include "Offsets.h"
#include "gx/Device.h"
#include "gx/Matrix.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <vector>
namespace Model::Scene::Water {
struct alignas(16) DriftMemory {
uint8_t bytes[Offsets::DRIFT_CLOUD_SIZE];
};
// A ripple record (0x50 bytes in the world's pool).
struct Ripple {
float position[3] = {}; // +0x00
float heading = 0.0f; // +0x0C
float size = 0.0f; // +0x10
float growth = 0.0f; // +0x14
float alpha = 0.0f; // +0x18
float peak = 0.0f; // +0x1C
float rise = 0.0f; // +0x20
float decay = 0.0f; // +0x24
double death = 0.0; // +0x28, on the scene's clock
uint32_t category = 0; // +0x2C: 0 a ring, 1 a wake
std::vector<float> polygon; // +0x44/+0x48: the liquid triangles, nine floats each
};
struct Effects {
DriftMemory *drift = nullptr; // made by CreateDriftCloud, or null
std::array<Ripple, Offsets::RIPPLE_POOL> ripples;
std::vector<uint32_t> live; // the pool's active list, oldest first
uint32_t nextSelf = 0;
uint32_t nextOther = Offsets::RIPPLE_POOL_SELF;
double clock = 0.0; // the scene's clock at the last update, seconds
};
namespace {
double D(float v) { return static_cast<double>(v); }
double F(uintptr_t address) { return static_cast<double>(Game::Read<float>(address)); }
// NTempest::CRandom::uint32 on the client stream, and its mantissa-filled
// float in [1, 2).
uint32_t Draw() {
return reinterpret_cast<uint32_t(__thiscall *)(void *)>(Offsets::FUN_RANDOM_UINT32)(
reinterpret_cast<void *>(static_cast<uintptr_t>(Offsets::VAR_CLIENT_RANDOM)));
}
double Unit(uint32_t bits) {
bits = (bits & 0x7FFFFFu) | 0x3F800000u;
float f;
std::memcpy(&f, &bits, sizeof f);
return f;
}
Effects &Ensure(Effects *&effects, double clock) {
if (effects == nullptr) {
effects = new Effects();
effects->clock = clock;
}
return *effects;
}
using This_t = void(__thiscall *)(void *);
using Word_t = void(__thiscall *)(void *, uint32_t);
bool Particulates() {
return (Game::Read<uint32_t>(Offsets::VAR_WORLD_FLAGS) & Offsets::WORLD_FLAG_PARTICULATES) != 0;
}
void *Drift(Effects &e) {
if (e.drift == nullptr) {
using Create_t = void(__thiscall *)(void *, uint32_t sizeBase, uint32_t edge, const char *texture);
e.drift = new DriftMemory();
reinterpret_cast<Create_t>(Offsets::FUN_CREATE_DRIFT_CLOUD)(
e.drift, Offsets::DRIFT_SIZE_BASE_WATER, Offsets::DRIFT_EDGE,
reinterpret_cast<const char *>(static_cast<uintptr_t>(Offsets::VAR_DRIFT_TEXTURE_PATH)));
}
return e.drift;
}
} // namespace
namespace {
// UpdateRipples' ageing (0x0068FB30): a record past its death time or whose
// alpha has fallen to 0 or less leaves the list; a live one grows, its alpha
// rises to the peak at the rise rate and then falls at the decay rate, the
// rise's overshoot taken from the step.
void Age(Effects &e, double clock) {
const float dt = static_cast<float>(clock - e.clock);
e.clock = clock;
for (auto it = e.live.begin(); it != e.live.end();) {
Ripple &r = e.ripples[*it];
bool alive = false;
if (e.clock < r.death) {
r.size = static_cast<float>(D(dt) * r.growth + r.size);
float step = dt;
if (r.rise != 0.0f) {
const float a = static_cast<float>(D(dt) * r.rise + r.alpha);
r.alpha = a;
if (r.peak < a) {
r.alpha = r.peak;
const float rate = r.rise;
r.rise = 0.0f;
step = static_cast<float>(D(step) - (D(a) - r.peak) / rate);
}
}
if (r.rise == 0.0f)
r.alpha = static_cast<float>(D(step) * r.decay + r.alpha);
alive = !(r.alpha <= 0.0f);
}
if (alive) {
++it;
} else {
r.polygon.clear();
it = e.live.erase(it);
}
}
}
// AddSplash's immediate leg, AddRipple and EmitRipple.
void AddSplash(Effects &e, void *frame, const float (&point)[3], uint32_t category, float heading, float size,
float lifetime, float peakShare, float growth, bool self) {
if (Game::Read<int32_t>(Offsets::VAR_WATER_RIPPLES) == 0 || Game::Read<int32_t>(Offsets::VAR_RIPPLE_MODE) != 0)
return;
float peak = 1.0f;
if (D(peakShare) < F(Offsets::VAR_ONE_SIXTH))
peak = static_cast<float>(D(peakShare) * F(Offsets::VAR_RIPPLE_PEAK_SCALE));
// AddRipple: the next record of the mover's range or the others'.
uint32_t index;
if (self) {
index = e.nextSelf;
if (++e.nextSelf >= Offsets::RIPPLE_POOL_SELF)
e.nextSelf = 0;
} else {
index = e.nextOther;
if (++e.nextOther >= Offsets::RIPPLE_POOL)
e.nextOther = Offsets::RIPPLE_POOL_SELF;
}
Ripple &r = e.ripples[index];
// EmitRipple: the record, then its box's liquid triangles.
r.category = category != 0 ? 1 : 0;
const float final = static_cast<float>(D(lifetime) * growth + size);
std::memcpy(r.position, point, sizeof r.position);
r.heading = -heading;
r.size = size;
r.peak = peak;
r.alpha = 0.0f;
r.growth = static_cast<float>((D(final) - size) / lifetime);
const double share = F(Offsets::VAR_RIPPLE_RISE_SHARES + r.category * sizeof(float));
r.rise = static_cast<float>(D(peak) / (D(lifetime) * share));
r.decay = static_cast<float>(-(D(peak) / ((F(Offsets::VAR_ONE) - share) * lifetime)));
r.death = e.clock + lifetime;
const float one = Game::Read<float>(Offsets::VAR_ONE);
const float box[6] = {static_cast<float>(D(point[0]) - final), static_cast<float>(D(point[1]) - final),
static_cast<float>(D(point[2]) - one), static_cast<float>(D(point[0]) + final),
static_cast<float>(D(point[1]) + final), static_cast<float>(D(point[2]) + one)};
r.polygon.clear();
World::LiquidTriangles(WorldOf(frame), TerrainOf(frame), box, r.polygon);
e.live.erase(std::remove(e.live.begin(), e.live.end(), index), e.live.end());
e.live.push_back(index);
}
} // namespace
void EmitUnitRipple(Effects *&effects, void *frame, const RippleSource &source, uint32_t &nextMs, uint32_t nowMs,
double clock, bool splash) {
// CGUnit_C::EmitWaterRipple (0x005FA760): the kind.
uint32_t kind = 0;
if (splash) {
nextMs = 0;
kind = Offsets::RIPPLE_KIND_SPLASH_INDEX;
} else if ((source.moveFlags & Offsets::RIPPLE_MOVE_FLAGS) != 0) {
kind = Offsets::RIPPLE_KIND_MOVING_INDEX;
} else if ((source.moveFlags & Offsets::RIPPLE_TURN_FLAGS) != 0) {
kind = Offsets::RIPPLE_KIND_TURNING_INDEX;
}
// The depth under twice the collision height, at least 1.0 (0x005FA7FC).
const double twice = D(source.height) + source.height;
const float limit = twice > F(Offsets::VAR_ONE) ? static_cast<float>(twice) : 1.0f;
const float half = static_cast<float>(D(limit) * F(Offsets::VAR_HALF));
const float depth = static_cast<float>(D(source.surface) - source.position[2]);
if (D(depth) >= limit)
return;
if (nextMs != 0 && static_cast<int32_t>(nowMs - nextMs) < 0)
return;
const float point[3] = {source.position[0], source.position[1], source.surface};
const bool wake = Game::Read<uint8_t>(Offsets::VAR_RIPPLE_KIND_WAKE + kind) != 0;
float moveScale = 1.0f;
if (wake && D(source.speed) > F(Offsets::VAR_CAMERA_PORTAL_EPSILON)) {
const double cap = F(Offsets::VAR_RIPPLE_SPEED_CAP);
const double v = D(source.speed) < cap ? D(source.speed) : cap;
moveScale = static_cast<float>(F(Offsets::VAR_RIPPLE_SPEED_SCALE) / v);
}
const float interval = static_cast<float>(F(Offsets::VAR_ONE) / moveScale);
float heading = source.facing;
const float base = static_cast<float>(D(source.scale) * F(Offsets::VAR_ONE_THIRD));
// The size: 0.9 to 1.1 of a third of the scale, kept in [1/3, 5/3].
const double sized = ((Unit(Draw()) - F(Offsets::VAR_ONE)) * F(Offsets::VAR_UNIT_NAME_SCALE_BASE) -
F(Offsets::VAR_ONE_TENTH) + F(Offsets::VAR_ONE)) *
base;
float size = static_cast<float>(sized);
if (sized < F(Offsets::VAR_ONE_THIRD))
size = Game::Read<float>(Offsets::VAR_ONE_THIRD);
else if (!(D(size) < F(Offsets::VAR_RIPPLE_SIZE_MAX)))
size = Game::Read<float>(Offsets::VAR_RIPPLE_SIZE_MAX);
float lifetime = static_cast<float>((Unit(Draw()) - F(Offsets::VAR_ONE)) * F(Offsets::VAR_ONE_TENTH) -
F(Offsets::VAR_RIPPLE_LIFE_SPREAD) + F(Offsets::VAR_RIPPLE_LIFE_BASE));
float growth = static_cast<float>(((Unit(Draw()) - F(Offsets::VAR_ONE)) * F(Offsets::VAR_RIPPLE_GROWTH_SPAN) -
F(Offsets::VAR_THREE_QUARTERS) + F(Offsets::VAR_RIPPLE_GROWTH_BASE)) *
F(Offsets::VAR_RIPPLE_GROWTH_SCALE) * F(Offsets::VAR_ONE_THIRTY_SIXTH) *
interval);
float weight = 1.0f;
float peakShare = Game::Read<float>(Offsets::VAR_ONE_SIXTH);
// Deeper than half the limit, the ripple fades toward the limit
// (0x005FA9E6).
if (D(depth) > half) {
const double w = (D(limit) - depth) / (D(limit) - half) * F(Offsets::VAR_HALF) + F(Offsets::VAR_HALF);
weight = static_cast<float>(w);
peakShare = static_cast<float>(w * F(Offsets::VAR_ONE_SIXTH));
lifetime = static_cast<float>(D(weight) * lifetime);
size = static_cast<float>(D(weight) * size);
}
// A still unit's (0x005FAA2F).
if (kind == 0) {
const uintptr_t scales = Offsets::VAR_RIPPLE_STILL_SCALES;
peakShare = static_cast<float>(F(scales) * peakShare);
lifetime = static_cast<float>(F(scales + 4) * lifetime);
growth = static_cast<float>(F(scales + 8) * growth);
size = static_cast<float>(F(scales + 12) * size);
}
// A ring at a random heading; a wake along the motion (0x005FAA5F).
if (!wake) {
heading = static_cast<float>((Unit(Draw()) - F(Offsets::VAR_ONE)) * F(Offsets::VAR_HEADING_TWO_PI));
} else {
const uint32_t f = source.moveFlags;
const double quarter = F(Offsets::VAR_QUARTER_PI), threeQuarters = F(Offsets::VAR_RIPPLE_THREE_QUARTER_PI),
halfPi = F(Offsets::VAR_HALF_PI);
if ((f & Offsets::MOVEFLAG_STRAFE_LEFT) != 0)
heading = static_cast<float>(D(heading) + ((f & Offsets::MOVEFLAG_FORWARD) != 0 ? quarter
: (f & Offsets::MOVEFLAG_BACKWARD) != 0 ? threeQuarters
: halfPi));
else if ((f & Offsets::MOVEFLAG_STRAFE_RIGHT) != 0)
heading = static_cast<float>(D(heading) - ((f & Offsets::MOVEFLAG_FORWARD) != 0 ? quarter
: (f & Offsets::MOVEFLAG_BACKWARD) != 0 ? threeQuarters
: halfPi));
else if ((f & Offsets::MOVEFLAG_BACKWARD) != 0)
heading = static_cast<float>(D(heading) + F(Offsets::VAR_RIPPLE_BACK_TURN));
}
Effects &e = Ensure(effects, clock);
// The scene's actor is the active mover: the first range of the pool.
AddSplash(e, frame, point, wake ? 1 : 0, heading, size, lifetime, peakShare, growth, true);
// The next time (0x005FAB46): a wake by its weight and speed, else 400 ms
// and a draw's share of 50.
if (wake) {
const double ms = D(weight) * moveScale * F(Offsets::VAR_RIPPLE_NEXT_SHARE) * F(Offsets::VAR_RIPPLE_NEXT_MS);
nextMs = nowMs - static_cast<uint32_t>(static_cast<int32_t>(ms));
} else {
const uint32_t spread = static_cast<uint32_t>((uint64_t{Offsets::RIPPLE_STILL_DELAY_SPREAD} * Draw()) >> 32);
nextMs = spread + Offsets::RIPPLE_STILL_DELAY_MS + nowMs;
}
}
void DrawRipples(const Effects *effects, const float (&eye)[3]) {
using Create_t = void *(__fastcall *)(int kind, int itemSize, int count);
using Lock_t = void *(__fastcall *)(void *buffer);
using Unlock_t = void(__fastcall *)(void *buffer, int unused);
using VertexBind_t = void(__fastcall *)(void *buffer, int format);
using IndexBind_t = void(__fastcall *)(void *buffer);
using Texture_t = void *(__fastcall *)(void *handle, int load, void *callback);
using Build_t = void(__fastcall *)(float *tex0, float *tex1, const float *box, const float *m, float fade,
int absolute);
struct Prim {
uint32_t type, base;
uint16_t count, min, max, unused;
};
using Render_t = void(__fastcall *)(const Prim *prim, int count);
if (effects == nullptr)
return;
// UpdateRipples' draw (0x0068FCB1): the vertices of each category's live
// records.
uint32_t counts[2] = {};
for (uint32_t index : effects->live) {
const Ripple &r = effects->ripples[index];
counts[r.category] += static_cast<uint32_t>(r.polygon.size() / 3);
}
if (counts[0] == 0 && counts[1] == 0)
return;
const auto *bias = Game::Read<const uint8_t *>(Offsets::VAR_FOOTSTEP_BIAS_CVAR);
const float biasValue = bias != nullptr ? Game::Read<float>(bias, Offsets::OFF_CVAR_VALUE_FLOAT) : 0.0f;
Gx::PushStates();
Gx::SetState(Offsets::GXRS_BLEND, Offsets::GXRS_BLEND_ADD);
Gx::SetState(Offsets::GXRS_FOG, 0);
Gx::SetState(Offsets::GXRS_DEPTH_WRITE, 0);
Gx::SetState(Offsets::GXRS_CULL, 0);
Gx::SetState(Offsets::GXRS_LIGHTING, 0);
Gx::SetStateFloat(Offsets::GXRS_DEPTH_BIAS, static_cast<float>(D(biasValue) * F(Offsets::VAR_RIPPLE_BIAS_SCALE)));
Gx::Matrix world;
Gx::Identity(world);
Gx::Translate(world, {-eye[0], -eye[1], -eye[2]});
Gx::PushTransform(Offsets::GX_XFORM_WORLD, world);
const bool swap = Game::Read<int32_t>(reinterpret_cast<void *(__fastcall *)()>(Offsets::FUN_GX_DEVICE_CAPS)(),
Offsets::OFF_GXCAPS_COLOR_SWAP) == 1;
for (uint32_t category = 0; category < 2; ++category) {
if (counts[category] == 0)
continue;
void *handle = Game::Read<void *>(Offsets::VAR_RIPPLE_TEXTURES + category * sizeof(void *));
void *texture = handle != nullptr ? reinterpret_cast<Texture_t>(Offsets::FUN_TEXTURE_GET_RENDERABLE)(
handle, 1, nullptr)
: nullptr;
if (texture == nullptr)
continue;
const int n = static_cast<int>(counts[category]);
void *vertices = reinterpret_cast<Create_t>(Offsets::FUN_GX_BUF_CREATE)(Offsets::GX_BUF_VERTICES,
Offsets::GX_PNCT_SIZE, n);
auto *v = static_cast<float *>(reinterpret_cast<Lock_t>(Offsets::FUN_GX_BUF_LOCK)(vertices));
void *indices = reinterpret_cast<Create_t>(Offsets::FUN_GX_BUF_CREATE)(Offsets::GX_BUF_INDICES,
sizeof(uint16_t), n);
auto *ix = static_cast<uint16_t *>(reinterpret_cast<Lock_t>(Offsets::FUN_GX_BUF_LOCK)(indices));
uint16_t written = 0;
for (uint32_t index : effects->live) {
const Ripple &r = effects->ripples[index];
if (r.category != category || r.polygon.empty())
continue;
// The texture matrix of the ripple's box turned by its heading
// (0x0068FFD0 to 0x0069014B).
Gx::Matrix turn, tex0, tex1;
Gx::Identity(turn);
Gx::Rotate(turn, r.heading, {0.0f, 0.0f, 1.0f});
Gx::Identity(tex0);
Gx::Identity(tex1);
const float one = Game::Read<float>(Offsets::VAR_ONE);
const float box[6] = {static_cast<float>(D(r.position[0]) - r.size),
static_cast<float>(D(r.position[1]) - r.size),
static_cast<float>(D(r.position[2]) - one),
static_cast<float>(D(r.size) + r.position[0]),
static_cast<float>(D(r.size) + r.position[1]),
static_cast<float>(D(r.position[2]) + one)};
reinterpret_cast<Build_t>(Offsets::FUN_BUILD_DECAL_TRANSFORMS)(tex0, tex1, box, turn,
Game::Read<float>(Offsets::VAR_HALF), 1);
// The colour: white under the alpha's byte (0x00690150).
const float packed = static_cast<float>(D(r.alpha) * F(Offsets::VAR_TWO_FIFTY_FIVE) +
F(Offsets::VAR_FIVE_TWELVE));
uint32_t bits;
std::memcpy(&bits, &packed, sizeof bits);
uint32_t colour = ((bits >> 14) & 0xFFu) << 24 | 0xFFFFFFu;
if (swap)
colour = (colour & 0xFF00FF00u) | ((colour >> 16) & 0xFFu) | ((colour & 0xFFu) << 16);
for (size_t k = 0; k + 2 < r.polygon.size(); k += 3) {
const float p[3] = {r.polygon[k], r.polygon[k + 1], r.polygon[k + 2]};
float uv[3];
Gx::TransformPoint(p, tex0, uv);
v[0] = p[0];
v[1] = p[1];
v[2] = p[2];
v[3] = 0.0f;
v[4] = 0.0f;
v[5] = 1.0f;
std::memcpy(&v[6], &colour, sizeof colour);
v[7] = uv[0];
v[8] = uv[1];
v += Offsets::GX_PNCT_SIZE / sizeof(float);
*ix++ = written++;
}
}
reinterpret_cast<Unlock_t>(Offsets::FUN_GX_BUF_UNLOCK)(indices, 0);
reinterpret_cast<Unlock_t>(Offsets::FUN_GX_BUF_UNLOCK)(vertices, 0);
if (written == 0)
continue;
reinterpret_cast<VertexBind_t>(Offsets::FUN_GX_VERTEX_BUF_BIND)(vertices, Offsets::GX_FORMAT_PNCT);
reinterpret_cast<IndexBind_t>(Offsets::FUN_GX_INDEX_BUF_BIND)(indices);
Gx::SetState(Offsets::GXRS_TEXTURE0, texture);
const Prim prim{Offsets::GX_PRIM_TRIANGLES, 0, written, 0, static_cast<uint16_t>(written - 1), 0};
reinterpret_cast<Render_t>(Offsets::FUN_GX_PRIM_RENDER_ONE)(&prim, 1);
}
Gx::PopTransform(Offsets::GX_XFORM_WORLD);
Gx::PopStates();
}
void Update(Effects *&effects, uint32_t before, uint32_t now, const float (&eye)[3], float elapsed, double clock) {
if (effects != nullptr)
Age(*effects, clock);
// CameraEnvironment's change leg (0x00680AD9): by the new liquid & 3.
if (now != Offsets::LIQUID_DRY) {
const uint32_t kind = now & Offsets::LIQUID_TYPE_MASK;
if (kind == Offsets::LIQUID_SLIME) {
if (effects != nullptr && effects->drift != nullptr)
Game::Ref<uint8_t>(effects->drift, Offsets::OFF_DRIFT_CLOUD_ENABLED) = 0;
} else if (now != before && Particulates()) {
void *drift = Drift(Ensure(effects, clock));
reinterpret_cast<Word_t>(Offsets::FUN_SCATTER_DRIFT_CLOUD)(drift, now);
reinterpret_cast<Word_t>(Offsets::FUN_SET_DRIFT_CLOUD_SIZE_BASE)(
drift, kind == Offsets::LIQUID_MAGMA ? Offsets::DRIFT_SIZE_BASE_MAGMA : Offsets::DRIFT_SIZE_BASE_WATER);
Game::Ref<uint8_t>(drift, Offsets::OFF_DRIFT_CLOUD_ENABLED) = 1;
}
}
// The world frame's advect (0x0066FEE0), from the scene's camera and
// frame time.
if (effects == nullptr || effects->drift == nullptr || !Particulates() || now == Offsets::LIQUID_DRY)
return;
float *camera = &Game::Ref<float>(Offsets::VAR_CAMERA_POSITION);
float &seconds = Game::Ref<float>(Offsets::VAR_FRAME_SECONDS);
const float savedCamera[3] = {camera[0], camera[1], camera[2]};
const float savedSeconds = seconds;
for (int i = 0; i < 3; ++i)
camera[i] = eye[i];
seconds = elapsed;
reinterpret_cast<This_t>(Offsets::FUN_ADVECT_DRIFT_CLOUD)(effects->drift);
for (int i = 0; i < 3; ++i)
camera[i] = savedCamera[i];
seconds = savedSeconds;
}
void DrawDrift(const Effects *effects, uint32_t cameraLiquid) {
// The world stage's particulates (0x006701E0).
if (effects == nullptr || effects->drift == nullptr || !Particulates() || cameraLiquid == Offsets::LIQUID_DRY)
return;
// The draw's quads are in view space; the world's stage reaches it with
// the world transform identity.
Gx::Matrix identity;
Gx::Identity(identity);
Gx::PushTransform(Offsets::GX_XFORM_WORLD, identity);
reinterpret_cast<This_t>(Offsets::FUN_DRAW_DRIFT_CLOUD)(effects->drift);
Gx::PopTransform(Offsets::GX_XFORM_WORLD);
}
void Free(Effects *effects) {
if (effects == nullptr)
return;
if (effects->drift != nullptr) {
reinterpret_cast<This_t>(Offsets::FUN_FREE_DRIFT_CLOUD_TEXTURE)(effects->drift);
delete effects->drift;
}
delete effects;
}
} // namespace Model::Scene::Water
+442 -235
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -122,7 +122,7 @@ bool Project(uint64_t guid, void *object, void *worldFrame, float screen[2]) {
world[2] += Game::Read<float>(object, Offsets::OFF_CGOBJECT_SCALE) *
Game::Read<float>(object, Offsets::OFF_CGOBJECT_BOX_HEIGHT) *
Game::Read<float>(Offsets::VAR_NAME_HEIGHT_FACTOR) +
Game::Read<float>(Offsets::VAR_NAMEPLATE_LIFT);
Game::Read<float>(Offsets::VAR_TWO_THIRDS);
return reinterpret_cast<ScreenCoords_t>(Offsets::FUN_WORLDFRAME_GET_SCREEN_COORDS)(
worldFrame, world, screen);
}