mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae69fd144 | ||
|
|
454be4ce49 | ||
|
|
717d75cd33 | ||
|
|
d4af7c4c6a | ||
|
|
1b267039a6 | ||
|
|
930f969cd6 | ||
|
|
d2e52ea6dd | ||
|
|
6422401ca7 | ||
|
|
b5e97d7d98 | ||
|
|
ca61a831f8 | ||
|
|
e39b87ab04 | ||
|
|
fb17670e37 | ||
|
|
0963a4c38f |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: "
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user