feat(model): add PlayerModel animation, keep-on-hide and unit methods

Adds SetAnimation, SetDoBlend/GetDoBlend, HasAnimation,
SetKeepModelOnHide/GetKeepModelOnHide, CanSetUnit and GetDisplayInfo
to PlayerModel (and so to DressUpModel and TabardModel). SetUnit now
returns whether the unit exists, and SetCreature takes an optional
display ID.

Animation. A 1.12.1 pane rests on Stand: its per-paint UpdateModel
(0x00505C50) arms Stand (PlayAnimation 0x007121A0 with id 0) whenever
key bone -1 plays anything else and no turn is pending. A deadline of 0
counts as passed, so any other animation lasted one frame. SetAnimation
gives the pane a resting animation and variation of its own. With one
set, UpdateModel is hooked to arm that animation in Stand's place, using
the pane's blend setting. Every other pane runs the stock function. The
pane's done callback (0x005062A0) arms Stand when the animation ends,
so the next paint starts it again and it loops. CSimpleModelFFX's
SetModelInstance (0x0076CD30), which every pane model build and hide
goes through, clears it, so a new model rests on Stand again.
SetRotation turns without the shuffle while a resting animation other
than Stand is set. HasAnimation is PlayAnimation's own test on the raw
id: the lookup at shared data +0x24 must give an index below the
sequence count at +0x1C. It is false until the model has loaded.

Keep on hide. The pane's Hide (0x00505CE0) drops its model through
slot 36, and its Show (0x00505D00) builds a new one from the unit or
display record. With keep on, Hide runs only the base hide (0x0076AD50)
and Show runs only the base show (0x0076AE10) while the pane still
holds its model. The model then comes back with its try-ons and
animation. The SetUnit helper (0x00505B50, behind SetUnit, RefreshUnit
and Dress) and SetDisplay (0x00505CB0) only store the new subject while
the pane is hidden, so they drop a kept model first. The next Show then
builds the new one. Off is the default and matches the stock client.

Units and displays. SetUnit runs the engine's method (0x00505D70,
which Turtle patches) unchanged. It then reports whether the GUID it
stored resolves to a unit (type mask 8), the same test the helper uses
before it builds. CanSetUnit is that resolve without the set.
GetDisplayInfo reads the display record's ID, or the unit's
UNIT_FIELD_DISPLAYID (+0x1F4, field 0x7D). SetCreature with a display
ID shows that display through the SetDisplayInfo record.
This commit is contained in:
Brues
2026-11-04 13:11:45 -06:00
parent 8e603a7a0b
commit 0caddb8fe6
9 changed files with 738 additions and 19 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredAreaIDsAtPosition`, `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems`, `GetBuybackItemLink` |
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature` (`displayID`), `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:HasAnimation`, `PlayerModel:SetDoBlend` / `GetDoBlend`, `PlayerModel:SetKeepModelOnHide` / `GetKeepModelOnHide` |
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
+108 -3
View File
@@ -780,11 +780,20 @@ build instructions.
- [Model](#model)
- [`model:SetDisplayInfo(displayID)`](#modelsetdisplayinfodisplayid)
- [`model:SetCreature(creatureID)`](#modelsetcreaturecreatureid)
- [`model:SetCreature(creatureID [, displayID])`](#modelsetcreaturecreatureid--displayid)
- [`model:SetPortraitZoom(zoom)`](#modelsetportraitzoomzoom)
- [`model:SetCamDistanceScale(scale)`](#modelsetcamdistancescalescale)
- [`model:RefreshCamera()`](#modelrefreshcamera)
- [`model:SetRotation(rotation [, animate])`](#modelsetrotationrotation--animate)
- [`model:SetUnit(unit)`](#modelsetunitunit)
- [`model:CanSetUnit(unit)`](#modelcansetunitunit)
- [`model:GetDisplayInfo()`](#modelgetdisplayinfo)
- [`model:SetAnimation(animation [, variation])`](#modelsetanimationanimation--variation)
- [`model:HasAnimation(animation)`](#modelhasanimationanimation)
- [`model:SetDoBlend([doBlend])`](#modelsetdoblenddoblend)
- [`model:GetDoBlend()`](#modelgetdoblend)
- [`model:SetKeepModelOnHide(keepModelOnHide)`](#modelsetkeepmodelonhidekeepmodelonhide)
- [`model:GetKeepModelOnHide()`](#modelgetkeepmodelonhide)
- [`DressUpModel:UndressSlot(slot)`](#dressupmodelundressslotslot)
- [`DressUpModel:TryOn(item [, handSlotName, spellEnchantID])`](#dressupmodeltryonitem--handslotname-spellenchantid)
- [`DressUpModel:IsGeoReady()`](#dressupmodelisgeoready)
@@ -18518,11 +18527,12 @@ m:SetPoint("CENTER", 0, 0)
m:SetDisplayInfo(330) -- a human NPC
```
### `model:SetCreature(creatureID)`
### `model:SetCreature(creatureID [, displayID])`
Shows a creature on the model by its creature ID (the creature's database
ID, not a display ID). The model is the same as `SetDisplayInfo` with the
creature's display ID.
creature's display ID. If you give `displayID`, the model shows that
display in place of the creature's own display.
The creature must be in the client creature cache. The client adds a
creature to the cache when you see it, target it, or put the mouse on it.
@@ -18588,6 +18598,101 @@ with no animation.
PreviewModel:SetRotation(math.pi / 2, false)
```
### `model:SetUnit(unit)`
Shows a unit on the model. Returns `true` when the unit exists, and `false`
when it does not. An unknown unit token is an error.
```lua
if not PreviewModel:SetUnit("target") then
PreviewModel:SetUnit("player")
end
```
### `model:CanSetUnit(unit)`
Returns `true` when the unit exists, so `SetUnit` can show it. The call
does not change the model. An unknown unit token is an error, the same as
in `SetUnit`.
### `model:GetDisplayInfo()`
Returns the display ID (a `CreatureDisplayInfo` ID) that the model shows:
- After `SetDisplayInfo` or `SetCreature`, the display that the model got.
- After `SetUnit`, the display that the unit has now. For a shapeshifted
druid, this is the display of the form.
Returns 0 when the model shows nothing, or when its unit no longer exists.
### `model:SetAnimation(animation [, variation])`
Sets the animation that the model plays. `animation` is an `AnimationData`
ID, for example 0 Stand, 4 Walk, 5 Run, 60 EmoteTalk, 67 EmoteWave, 69
EmoteDance. `variation` selects one of the model's versions of the
animation. The default is 0. The value -1 selects a version at random.
The model plays the animation again each time it ends. The model goes back
to Stand when the frame loads a new model: after `SetUnit`, `RefreshUnit`,
`SetDisplayInfo`, `SetCreature`, or `DressUpModel:Dress()`, and when the
frame shows again after it was hidden (see `SetKeepModelOnHide`). To go
back to Stand at any time, call `SetAnimation(0)`.
If the model does not have the animation, it plays the fallback animation
that `AnimationData` gives, or Stand. Use `HasAnimation` to find out first.
While the animation is not Stand, `SetRotation` turns the model without the
step animation.
Call `SetAnimation` after `SetUnit`, `SetDisplayInfo`, or `SetCreature`. The
animation starts when the model finishes loading.
```lua
PreviewModel:SetUnit("player")
PreviewModel:SetAnimation(69) -- dance
```
### `model:HasAnimation(animation)`
Returns `true` when the model has the animation (an `AnimationData` ID).
Returns `false` until the model finishes loading. A model file that the
client already loaded in this session is ready at once.
```lua
local anim = PreviewModel:HasAnimation(69) and 69 or 0
PreviewModel:SetAnimation(anim)
```
### `model:SetDoBlend([doBlend])`
Sets how the model starts the animation that `SetAnimation` set. If
`doBlend` is `true`, the model blends from the animation that plays now into
that animation. If `doBlend` is `false` or not given, the animation starts
at once. The default is `false`.
### `model:GetDoBlend()`
Returns the value that `SetDoBlend` set.
### `model:SetKeepModelOnHide(keepModelOnHide)`
Sets whether the frame keeps its model while the frame is hidden. The
default is `false`: the frame removes the model when it hides, and loads
the model again from its unit or display when it shows.
If `keepModelOnHide` is `true`, the model comes back as it was when the
frame shows again, with its animation and the items that `TryOn` put on
it. If you call `SetUnit`, `RefreshUnit`, `SetDisplayInfo`, `SetCreature`,
or `DressUpModel:Dress()` while the frame is hidden, the frame loads the
new model when it shows, the same as with `false`.
```lua
DressUpModel:SetKeepModelOnHide(true)
```
### `model:GetKeepModelOnHide()`
Returns the value that `SetKeepModelOnHide` set.
### `DressUpModel:UndressSlot(slot)`
Removes the item in one inventory slot from a `DressUpModel`. `slot` is an
+51
View File
@@ -4029,6 +4029,13 @@ enum Offsets {
VTBL_SIMPLEMODEL_UPDATE_MODEL = 0x98,
// `CM2Scene *__thiscall(frame)` — the frame's scene, made on first use.
FUN_SIMPLEMODEL_GET_SCENE = 0x0076CFC0,
// CSimpleModelFFX::SetModelInstance — `__thiscall(frame, model)`, ret 4:
// AddRefs the new model, releases the old one, stores it at +0x318.
// Slot 36 of the Model and pane vtables; DressUpModel's own slot 36
// (0x00504350) calls it first.
FUN_SIMPLEMODEL_SET_MODEL_INSTANCE = 0x0076CD30,
// The Model frame's per-paint update — `__thiscall(frame)`, slot 38.
FUN_SIMPLEMODEL_UPDATE_MODEL = 0x0076D1A0,
// ---- PlayerModel / DressUpModel / TabardModel (benilla ui/playermodel) --
// CGCharacterModelBase: a Model frame (0x3F8 bytes) that shows a copy of a
@@ -4064,6 +4071,37 @@ enum Offsets {
FUN_PLAYERMODEL_CONFIGURE_CAMERA = 0x00505890,
// The engine's Lua PlayerModel:SetRotation(angle) — `int __fastcall(L)`.
FUN_SCRIPT_PLAYERMODEL_SET_ROTATION = 0x00505F00,
// The engine's Lua PlayerModel:SetUnit(unit) — `int __fastcall(L)`,
// returns nothing: FUN_TOKEN_TO_GUID, then the SetUnit helper below.
// Turtle's client patches its tail (0x00505E19) to also take a number.
FUN_SCRIPT_PLAYERMODEL_SET_UNIT = 0x00505D70,
// SetUnit helper — `__thiscall(pane, uint64 guid)`, ret 8: stores the
// GUID, clears the display record and, if the frame is visible
// (OFF_FRAME_VISIBLE), builds the model from the unit (type mask 8).
// Its callers: SetUnit, RefreshUnit (0x00505E40), DressUpModel:Dress.
FUN_PLAYERMODEL_SET_UNIT_GUID = 0x00505B50,
// Hide and Show — `__thiscall(pane)`, slots 33 and 34 of all three pane
// vtables. Hide drops the model (slot 36 with null), then jumps to the
// base hide. Show, while the frame is not yet visible, builds the model
// from the GUID or the display record, then jumps to the base show.
FUN_PLAYERMODEL_HIDE = 0x00505CE0,
FUN_PLAYERMODEL_SHOW = 0x00505D00,
FUN_SIMPLEFRAME_HIDE = 0x0076AD50,
FUN_SIMPLEFRAME_SHOW = 0x0076AE10,
// The turn latch (SetRotation sets it, the next paint clears it) and the
// turn deadline in OS ms (now + 100 on a turn, 0 once Stand is armed).
OFF_PLAYERMODEL_TURNING = 0x3E8,
OFF_PLAYERMODEL_TURN_DEADLINE = 0x3EC,
// UpdateModel — `__thiscall(pane)`, slot 38 (DressUpModel's 0x00504470
// calls it first): the base update, then, with no turn latched and the
// deadline passed (0 counts as passed), arms Stand (-1, 0, -1, 0, 1.0, 1,
// 1) when key bone -1 plays anything else, and zeroes the deadline.
// 4.3.4's UpdateModel is the same with the pane's m_sequence and
// m_sequenceVariation in place of 0 and -1, and a loaded-model gate.
FUN_PLAYERMODEL_UPDATE_MODEL = 0x00505C50,
// vtable slot 36: SetModelInstance `__thiscall(frame, model)`; the pane
// Hide passes null to drop the model (0x00505CE7).
VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE = 0x90,
// DressUpModel fields (benilla ui/dressup): the character component, and
// the main- and off-hand lanes, 8 bytes each: byte 0 item class (0 =
@@ -4173,6 +4211,11 @@ enum Offsets {
OFF_M2MODEL_SHARED = 0x30,
OFF_M2SHARED_DATA = 0x130,
OFF_M2DATA_CAMERA_ID_LOOKUP = 0x12C,
// The sequence count, and the {count, uint16 *} lookup from animation id
// to sequence. PlayAnimation (0x007121A0) maps a resolved id through the
// lookup and drops it when the index is not below the count.
OFF_M2DATA_SEQUENCE_COUNT = 0x1C,
OFF_M2DATA_ANIMATION_LOOKUP = 0x24,
// SetAnimated — `__thiscall(model, bool)`; M2SceneAnimateAndCollect —
// `__thiscall(scene, const float *cameraPos)`. ConfigureCamera runs both
// (with the frame's facing zeroed) before it copies a model camera.
@@ -10630,6 +10673,10 @@ enum Offsets {
// descriptor — though we only expose `IsMounted()` for the
// player to match modern API semantics.
OFF_UNIT_FIELD_MOUNTDISPLAYID = 0x1FC,
// UNIT_FIELD_DISPLAYID: the display the unit shows now (a shapeshift
// form's while shapeshifted). Field 0x7D in the update-field name table
// (0x0083A73C).
OFF_UNIT_FIELD_DISPLAYID = 0x1F4,
// Local player movement-flags u32. Lives directly on the
// CGPlayer object (not the broadcast descriptor) — this is
@@ -12850,6 +12897,10 @@ enum Offsets {
// CM2Model::PlayAnimation — `__thiscall(model, 7 args)`, `ret 0x1C`. The
// bake passes (-1, 0, 0, 0, 1.0f, 0, 1).
FUN_M2MODEL_PLAY_ANIMATION = 0x007121A0,
// CM2Model::GetKeyBoneAnimId — `__thiscall(model, keyBone)`, ret 4: the id
// last requested on the key bone (bone state +0xF8, the id as given, not
// its fallback). Blocks on the shared data when the model is not loaded.
FUN_M2MODEL_GET_KEY_BONE_ANIM = 0x00712090,
// ApplyCreatureDisplay — `__fastcall(model, displayRec, modelDataRec)`,
// `ret 4`: the CreatureDisplayInfo row's three skins as the model's
// replaceable textures 11..13 (AttachDisplayModel 0x00505AE6).
+230
View File
@@ -0,0 +1,230 @@
// 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/>.
// `PlayerModel:SetAnimation(anim [, variation])`, `SetDoBlend` / `GetDoBlend`
// and `HasAnimation(anim)` — retail's CharacterModelBase animation methods.
//
// A pane rests on Stand: its per-paint UpdateModel (FUN_PLAYERMODEL_UPDATE_MODEL)
// arms Stand whenever key bone -1 plays anything else and no turn is pending,
// so any other animation lasts one frame. 4.3.4's CGCharacterModelBase has
// the same UpdateModel with the pane's m_sequence / m_sequenceVariation in
// place of Stand, reset to Stand each time the pane sets up a new model
// (SetUpModel, the display setup, SetCustomRace), and a RotateModel that
// skips the shuffle while m_sequence is not Stand. Retail's SetAnimation sets
// that pair. 1.12.1 has no such fields, so:
// - SetAnimation stores the pair per pane; UpdateModel is hooked to run
// 4.3.4's version for a pane that has one, and stock otherwise.
// - SetModelInstance (FUN_SIMPLEMODEL_SET_MODEL_INSTANCE, which every pane
// model build and drop goes through) clears it.
// - Model::Rotation asks HasRestingAnimation before it shuffles.
// The re-arm also restarts the animation after the pane's done callback
// (0x005062A0) arms Stand at its end, so it loops as in 4.3.4.
//
// HasAnimation is PlayAnimation's own test on the raw id: the lookup index
// must be below the sequence count.
#include "model/Animation.h"
#include "Game.h"
#include "Offsets.h"
#include "time/Clock.h"
#include <cstdint>
#include <unordered_map>
namespace Model::Animation {
namespace {
using PaneFn_t = void(__thiscall *)(void *pane);
using SetModelInstance_t = void(__thiscall *)(void *frame, void *model);
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
int offset, float speed, int blend, int primary);
using GetKeyBoneAnim_t = uint32_t(__thiscall *)(void *model, int keyBone);
using LookupIndex_t = uint16_t(__fastcall *)(uint32_t id, const void *table);
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
struct PaneState {
bool doBlend = false;
bool resting = false; // SetAnimation gave the pane a resting animation
uint32_t animation = 0;
int variation = 0;
};
std::unordered_map<void *, PaneState> g_panes;
PaneFn_t g_origUpdateModel = nullptr;
SetModelInstance_t g_origSetModelInstance = nullptr;
void *PaneModel(void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
}
bool IsLoaded(void *model) {
return reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, 0, 0) != 0;
}
void __fastcall UpdateModel_h(void *pane, void * /*edx*/) {
const auto it = g_panes.find(pane);
if (it == g_panes.end() || !it->second.resting) {
g_origUpdateModel(pane);
return;
}
const PaneState &s = it->second;
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEMODEL_UPDATE_MODEL)(pane);
void *model = PaneModel(pane);
const uint32_t deadline = Game::Read<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE);
if (Game::Read<int>(pane, Offsets::OFF_PLAYERMODEL_TURNING) == 0 &&
(deadline == 0 || Time::Clock::Reached(deadline)) && model != nullptr && IsLoaded(model) &&
reinterpret_cast<GetKeyBoneAnim_t>(Offsets::FUN_M2MODEL_GET_KEY_BONE_ANIM)(model, -1) !=
s.animation) {
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(
model, -1, s.animation, s.variation, 0, 1.0f, s.doBlend ? 1 : 0, 1);
Game::Ref<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE) = 0;
}
Game::Ref<int>(pane, Offsets::OFF_PLAYERMODEL_TURNING) = 0;
}
void __fastcall SetModelInstance_h(void *frame, void * /*edx*/, void *model) {
const auto it = g_panes.find(frame);
if (it != g_panes.end())
it->second.resting = false;
g_origSetModelInstance(frame, model);
}
const Game::HookAutoRegister _updateHook{Offsets::FUN_PLAYERMODEL_UPDATE_MODEL,
reinterpret_cast<void *>(&UpdateModel_h),
reinterpret_cast<void **>(&g_origUpdateModel)};
const Game::HookAutoRegister _setModelHook{Offsets::FUN_SIMPLEMODEL_SET_MODEL_INSTANCE,
reinterpret_cast<void *>(&SetModelInstance_h),
reinterpret_cast<void **>(&g_origSetModelInstance)};
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
}
int __fastcall Script_SetAnimation(void *L) {
void *pane = ResolvePane(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: SetAnimation(animation [, variation])");
return 0;
}
PaneState &s = g_panes[pane];
s.resting = true;
s.animation = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
s.variation = Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
return 0;
}
int __fastcall Script_SetDoBlend(void *L) {
if (void *pane = ResolvePane(L))
g_panes[pane].doBlend = Game::Lua::ToBoolean(L, 2) != 0;
return 0;
}
int __fastcall Script_GetDoBlend(void *L) {
void *pane = ResolvePane(L);
if (pane == nullptr)
return 0;
const auto it = g_panes.find(pane);
Game::Lua::PushBool(L, it != g_panes.end() && it->second.doBlend);
return 1;
}
int __fastcall Script_HasAnimation(void *L) {
void *pane = ResolvePane(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: HasAnimation(animation)");
return 0;
}
const auto id = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
bool has = false;
void *model = PaneModel(pane);
if (model != nullptr && IsLoaded(model)) {
const auto *data = Game::Read<const uint8_t *>(
Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED),
Offsets::OFF_M2SHARED_DATA);
const uint16_t index = reinterpret_cast<LookupIndex_t>(Offsets::FUN_M2_LOOKUP_INDEX)(
id, data + Offsets::OFF_M2DATA_ANIMATION_LOOKUP);
has = index < Game::Read<uint32_t>(data, Offsets::OFF_M2DATA_SEQUENCE_COUNT);
}
Game::Lua::PushBool(L, has);
return 1;
}
const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetAnimation", &Script_SetAnimation},
{"SetDoBlend", &Script_SetDoBlend},
{"GetDoBlend", &Script_GetDoBlend},
{"HasAnimation", &Script_HasAnimation},
};
const Game::Doc::Field kSetAnimationArgs[] = {
Game::Doc::Req("animation", "number", "An AnimationData ID."),
Game::Doc::Opt("variation", "number", "0", "The variation to play; -1 picks one at random."),
};
const Game::Doc::Function kSetAnimation{
"Sets the animation the model plays until it loads a new model.", kSetAnimationArgs, {}};
const Game::Doc::Field kDoBlendArgs[] = {
Game::Doc::Opt("doBlend", "bool", "false"),
};
const Game::Doc::Function kSetDoBlend{
"Sets whether the model blends into the animation that SetAnimation set.", kDoBlendArgs,
{}};
const Game::Doc::Field kDoBlendRets[] = {
Game::Doc::Req("doBlend", "bool"),
};
const Game::Doc::Function kGetDoBlend{"Returns whether the model blends into its animation.",
{}, kDoBlendRets};
const Game::Doc::Field kHasAnimationArgs[] = {
Game::Doc::Req("animation", "number", "An AnimationData ID."),
};
const Game::Doc::Field kHasAnimationRets[] = {
Game::Doc::Req("hasAnimation", "bool"),
};
const Game::Doc::Function kHasAnimation{
"Returns whether the model has the animation. False until the model has loaded.",
kHasAnimationArgs, kHasAnimationRets};
const Game::Doc::Method g_methodDocs[] = {
{"SetAnimation", &kSetAnimation},
{"SetDoBlend", &kSetDoBlend},
{"GetDoBlend", &kGetDoBlend},
{"HasAnimation", &kHasAnimation},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
// Frames are rebuilt on /reload and a new frame can reuse an old address.
void PrepareForReload() {
g_panes.clear();
}
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
bool HasRestingAnimation(void *pane) {
const auto it = g_panes.find(pane);
return it != g_panes.end() && it->second.resting && it->second.animation != 0;
}
} // namespace Model::Animation
+23
View File
@@ -0,0 +1,23 @@
// 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
namespace Model::Animation {
// True when SetAnimation gave the pane a resting animation other than Stand
// (4.3.4's `m_sequence != 0`). RotateModel turns such a pane without the
// shuffle animation.
bool HasRestingAnimation(void *pane);
} // namespace Model::Animation
+22 -10
View File
@@ -11,7 +11,8 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `PlayerModel:SetDisplayInfo(displayID)` and `PlayerModel:SetCreature(creatureID)`
// `PlayerModel:SetDisplayInfo(displayID)` and
// `PlayerModel:SetCreature(creatureID [, displayID])`
// — 4.3.4's CGCharacterModelBase methods (CharacterModelBase_Scripts.cpp), on
// vanilla's own pane path.
//
@@ -32,6 +33,8 @@
// path CGUnit_C::UpdateCharacterCustomization).
// - SetCreature passes the creature's cache record straight to SetDisplay —
// the same record 4.3.4 passes (CreatureStats_C) and the stable passes.
// Retail's optional displayID shows that display in its place, through
// the SetDisplayInfo record below.
// - SetDisplayInfo has a display ID and no cache record. The pane only ever
// reads the record's display-ID field (OFF_PLAYERMODEL_DISPLAY_RECORD), so
// each pane gets a record of its own holding just that field, which lives
@@ -257,6 +260,15 @@ struct DisplayRecord {
};
std::unordered_map<void *, DisplayRecord> g_displayRecords;
void ShowDisplayID(void *pane, uint32_t displayID) {
if (displayID == 0)
return;
DisplayRecord &rec = g_displayRecords[pane];
std::memset(rec.bytes, 0, sizeof(rec.bytes));
std::memcpy(rec.bytes + Offsets::OFF_CREATURE_DISPLAYID, &displayID, sizeof(displayID));
SetDisplay(pane, rec.bytes);
}
int __fastcall Script_SetDisplayInfo(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
if (pane == nullptr)
@@ -265,13 +277,7 @@ int __fastcall Script_SetDisplayInfo(void *L) {
Game::Lua::Error(L, "Usage: SetDisplayInfo(displayID)");
return 0;
}
const auto displayID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
if (displayID == 0)
return 0;
DisplayRecord &rec = g_displayRecords[pane];
std::memset(rec.bytes, 0, sizeof(rec.bytes));
std::memcpy(rec.bytes + Offsets::OFF_CREATURE_DISPLAYID, &displayID, sizeof(displayID));
SetDisplay(pane, rec.bytes);
ShowDisplayID(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)));
return 0;
}
@@ -280,12 +286,16 @@ int __fastcall Script_SetCreature(void *L) {
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: SetCreature(creatureID)");
Game::Lua::Error(L, "Usage: SetCreature(creatureID [, displayID])");
return 0;
}
const uint8_t *record =
Creature::Info::CacheRecord(static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)));
if (record != nullptr)
if (record == nullptr)
return 0;
if (Game::Lua::IsNumber(L, 3))
ShowDisplayID(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 3)));
else
SetDisplay(pane, record);
return 0;
}
@@ -302,6 +312,8 @@ const Game::Doc::Function kSetDisplayInfo{
"Shows a creature display on the model, dressed like the creature.", kDisplayArgs, {}};
const Game::Doc::Field kCreatureArgs[] = {
Game::Doc::Req("creatureID", "number", "A creature ID that is in the creature cache."),
Game::Doc::Opt("displayID", "number", nullptr,
"The CreatureDisplayInfo ID to show in place of the creature's own."),
};
const Game::Doc::Function kSetCreature{
"Shows a creature on the model by its creature ID.", kCreatureArgs, {}};
+173
View File
@@ -0,0 +1,173 @@
// 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/>.
// `PlayerModel:SetKeepModelOnHide(keep)` / `GetKeepModelOnHide()` — retail's
// CharacterModelBase methods.
//
// A vanilla pane drops its model when it hides (FUN_PLAYERMODEL_HIDE: slot 36
// with null) and builds a new one from its unit or display record when it
// shows (FUN_PLAYERMODEL_SHOW). That is retail's behavior with keep off, the
// default. With keep on:
// - Hide skips the drop and runs only the base hide.
// - Show skips the build while the pane still holds its model, so the
// model comes back as it was (tried-on items, animation).
// - SetUnit, RefreshUnit, Dress (FUN_PLAYERMODEL_SET_UNIT_GUID) and
// SetDisplay (FUN_PLAYERMODEL_SET_DISPLAY) only store the new subject
// while the pane is hidden. A kept model would then be the old subject,
// so they drop it first, and the next show builds the new one as it
// does with keep off.
// - Turning keep off while hidden drops the kept model.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <unordered_set>
namespace Model::KeepOnHide {
namespace {
using PaneFn_t = void(__thiscall *)(void *pane);
using SetUnitGuid_t = void(__thiscall *)(void *pane, uint64_t guid);
using SetDisplay_t = void(__thiscall *)(void *pane, const uint8_t *record);
using SetModelInstance_t = void(__thiscall *)(void *pane, void *model);
PaneFn_t g_origHide = nullptr;
PaneFn_t g_origShow = nullptr;
SetUnitGuid_t g_origSetUnitGuid = nullptr;
SetDisplay_t g_origSetDisplay = nullptr;
std::unordered_set<void *> g_keeping; // panes with SetKeepModelOnHide(true)
bool Keeps(void *pane) {
return g_keeping.count(pane) != 0;
}
bool HasModel(void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE) != nullptr;
}
// The pane Hide's own drop: slot 36 with null.
void DropModel(void *pane) {
Game::Read<SetModelInstance_t>(Game::Read<const void *>(pane, 0),
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, nullptr);
}
void DropIfKeptWhileHidden(void *pane) {
if (Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 && HasModel(pane) && Keeps(pane))
DropModel(pane);
}
void __fastcall Hide_h(void *pane, void * /*edx*/) {
if (Keeps(pane)) {
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEFRAME_HIDE)(pane);
return;
}
g_origHide(pane);
}
void __fastcall Show_h(void *pane, void * /*edx*/) {
if (Keeps(pane) && HasModel(pane)) {
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEFRAME_SHOW)(pane);
return;
}
g_origShow(pane);
}
void __fastcall SetUnitGuid_h(void *pane, void * /*edx*/, uint64_t guid) {
DropIfKeptWhileHidden(pane);
g_origSetUnitGuid(pane, guid);
}
void __fastcall SetDisplay_h(void *pane, void * /*edx*/, const uint8_t *record) {
DropIfKeptWhileHidden(pane);
g_origSetDisplay(pane, record);
}
const Game::HookAutoRegister _hideHook{Offsets::FUN_PLAYERMODEL_HIDE,
reinterpret_cast<void *>(&Hide_h),
reinterpret_cast<void **>(&g_origHide)};
const Game::HookAutoRegister _showHook{Offsets::FUN_PLAYERMODEL_SHOW,
reinterpret_cast<void *>(&Show_h),
reinterpret_cast<void **>(&g_origShow)};
const Game::HookAutoRegister _setUnitHook{Offsets::FUN_PLAYERMODEL_SET_UNIT_GUID,
reinterpret_cast<void *>(&SetUnitGuid_h),
reinterpret_cast<void **>(&g_origSetUnitGuid)};
const Game::HookAutoRegister _setDisplayHook{Offsets::FUN_PLAYERMODEL_SET_DISPLAY,
reinterpret_cast<void *>(&SetDisplay_h),
reinterpret_cast<void **>(&g_origSetDisplay)};
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
}
int __fastcall Script_SetKeepModelOnHide(void *L) {
void *pane = ResolvePane(L);
if (pane == nullptr)
return 0;
if (Game::Lua::ToBoolean(L, 2) != 0) {
g_keeping.insert(pane);
return 0;
}
if (g_keeping.erase(pane) != 0 && Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 &&
HasModel(pane))
DropModel(pane);
return 0;
}
int __fastcall Script_GetKeepModelOnHide(void *L) {
void *pane = ResolvePane(L);
if (pane == nullptr)
return 0;
Game::Lua::PushBool(L, Keeps(pane));
return 1;
}
const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetKeepModelOnHide", &Script_SetKeepModelOnHide},
{"GetKeepModelOnHide", &Script_GetKeepModelOnHide},
};
const Game::Doc::Field kKeepArgs[] = {
Game::Doc::Req("keepModelOnHide", "bool"),
};
const Game::Doc::Function kSetKeep{
"Sets whether the model stays loaded while the frame is hidden.", kKeepArgs, {}};
const Game::Doc::Field kKeepRets[] = {
Game::Doc::Req("keepModelOnHide", "bool"),
};
const Game::Doc::Function kGetKeep{
"Returns whether the model stays loaded while the frame is hidden.", {}, kKeepRets};
const Game::Doc::Method g_methodDocs[] = {
{"SetKeepModelOnHide", &kSetKeep},
{"GetKeepModelOnHide", &kGetKeep},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
// Frames are rebuilt on /reload and a new frame can reuse an old address.
void PrepareForReload() {
g_keeping.clear();
}
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
} // namespace Model::KeepOnHide
+8 -5
View File
@@ -16,12 +16,14 @@
// Vanilla's SetRotation (FUN_SCRIPT_PLAYERMODEL_SET_ROTATION) always turns the
// model with a shuffle animation (TurnTo 0x00505BB0). 4.3.4's RotateModel
// (CharacterModelBase.cpp) does the same when `animate` is true or absent, and
// with `animate` false only stores the facing: no shuffle, no turn latch. So a
// false `animate` stores the facing here and anything else runs vanilla's
// method unchanged.
// with `animate` false only stores the facing: no shuffle, no turn latch. It
// also only stores the facing while the pane rests on an animation other than
// Stand (`m_sequence`, set by SetAnimation, see Model::Animation). Otherwise
// vanilla's method runs unchanged.
#include "Game.h"
#include "Offsets.h"
#include "model/Animation.h"
namespace Model::Rotation {
namespace {
@@ -31,11 +33,12 @@ constexpr int kTypeNone = -1; // lua_type of an absent argument
using LuaMethod_t = int(__fastcall *)(void *L);
int __fastcall Script_SetRotation(void *L) {
if (Game::Lua::Type(L, 3) == kTypeNone || Game::Lua::ToBoolean(L, 3) != 0)
return reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
if (pane == nullptr)
return 0;
const bool animate = Game::Lua::Type(L, 3) == kTypeNone || Game::Lua::ToBoolean(L, 3) != 0;
if (animate && !Model::Animation::HasRestingAnimation(pane))
return reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: SetRotation(rotation (in radians))");
return 0;
+122
View File
@@ -0,0 +1,122 @@
// 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/>.
// `PlayerModel:SetUnit(unit)` → success, `CanSetUnit(unit)` and
// `GetDisplayInfo()` — retail's CharacterModelBase unit methods.
//
// Vanilla's SetUnit (FUN_SCRIPT_PLAYERMODEL_SET_UNIT) returns nothing. It
// stores the token's GUID and builds the model only when that GUID resolves
// to a unit (type mask 8, FUN_PLAYERMODEL_SET_UNIT_GUID). So SetUnit runs
// the engine's method unchanged (Turtle patches it) and reports whether the
// stored GUID resolves; CanSetUnit is the same resolve without the set
// (FUN_RESOLVE_UNIT_TOKEN = token to GUID, then type mask 8).
//
// GetDisplayInfo reads what the pane shows: the display record's ID
// (SetDisplayInfo / SetCreature), or the unit's UNIT_FIELD_DISPLAYID.
#include "Game.h"
#include "Offsets.h"
#include "object/Resolve.h"
#include <cstdint>
namespace Model::Subject {
namespace {
using LuaMethod_t = int(__fastcall *)(void *L);
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
}
void *PaneUnit(void *pane) {
return Object::ByGuid(Offsets::TYPEMASK_UNIT,
Game::Read<uint64_t>(pane, Offsets::OFF_PLAYERMODEL_UNIT_GUID));
}
int __fastcall Script_SetUnit(void *L) {
reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_UNIT)(L);
void *pane = ResolvePane(L);
if (pane == nullptr)
return 0;
Game::Lua::PushBool(L, PaneUnit(pane) != nullptr);
return 1;
}
int __fastcall Script_CanSetUnit(void *L) {
if (ResolvePane(L) == nullptr)
return 0;
if (!Game::Lua::IsString(L, 2)) {
Game::Lua::Error(L, "Usage: CanSetUnit(unit)");
return 0;
}
Game::Lua::PushBool(L, Game::ResolveUnitToken(Game::Lua::ToString(L, 2)) != nullptr);
return 1;
}
int __fastcall Script_GetDisplayInfo(void *L) {
void *pane = ResolvePane(L);
if (pane == nullptr)
return 0;
uint32_t displayID = 0;
if (const auto *record =
Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD)) {
displayID = Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID);
} else if (void *unit = PaneUnit(pane)) {
displayID = Game::Read<uint32_t>(Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR),
Offsets::OFF_UNIT_FIELD_DISPLAYID);
}
Game::Lua::PushNumber(L, displayID);
return 1;
}
const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetUnit", &Script_SetUnit},
{"CanSetUnit", &Script_CanSetUnit},
{"GetDisplayInfo", &Script_GetDisplayInfo},
};
const Game::Doc::Field kUnitArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
};
const Game::Doc::Field kSuccessRets[] = {
Game::Doc::Req("success", "bool", "True when the unit exists."),
};
const Game::Doc::Function kSetUnit{"Shows a unit on the model.", kUnitArgs, kSuccessRets};
const Game::Doc::Field kCanSetUnitRets[] = {
Game::Doc::Req("canSetUnit", "bool"),
};
const Game::Doc::Function kCanSetUnit{"Returns whether SetUnit can show the unit.", kUnitArgs,
kCanSetUnitRets};
const Game::Doc::Field kDisplayRets[] = {
Game::Doc::Req("displayID", "number", "The CreatureDisplayInfo ID, or 0."),
};
const Game::Doc::Function kGetDisplayInfo{"Returns the display the model shows.", {},
kDisplayRets};
const Game::Doc::Method g_methodDocs[] = {
{"SetUnit", &kSetUnit},
{"CanSetUnit", &kCanSetUnit},
{"GetDisplayInfo", &kGetDisplayInfo},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Model::Subject