refactor(model): share the engine model calls across the model modules

The model modules each declared their own pointers to the same 1.12.1
engine functions: the instance gates FUN_MODEL_INSTANCE_LOADED
(0x007103D0) and FUN_MODEL_INSTANCE_READY, AddRef / Release
(0x00710390 / 0x007103A0), FUN_M2SCENE_CREATE_MODEL,
FUN_M2MODEL_PLAY_ANIMATION, the done callback, the item visuals, the
render callback, the M2 data lookup FUN_M2_LOOKUP_INDEX (0x00710310),
and the pane's model, scene, SetModelInstance virtual (vtable +0x90)
and SetDisplay. model/M2.h now holds one typed wrapper for each, and
every module calls through it.

- Game::Lua::ResolvePlayerModel / ResolveDressUpModel join the
  per-type resolvers, replacing four ResolvePane copies and five
  inline ResolveTypedObject calls. Game::Lua::TYPE_NONE replaces the
  local kTypeNone in Rotation and frame/CreateFrame.
- Offsets.h has the full INVTYPE list, 0..28, named by the engine's
  own key table at 0x0083DDB0. It replaces the four scattered
  INVTYPE_* entries and the local copies in DressUp and ItemDisplay,
  and the try-on result switch reads by name.
- DisplayInfo keeps its dress jobs in a map by frame, like the other
  modules, in place of the fixed 8-slot pool and its eviction.
- DressUp names the held lane's bytes (item class, inventory type,
  sheathe type) and detaches a lane in one place.
- ItemDisplay::OwnVisual is the one test for a display's own
  ItemVisuals row (GetItemDescVisual 0x0062EC70), used by SetItem and
  by TryOn's enchant glow. Both own-record panes share
  Pane::DisplayRecord, and the SetDisplayInfo records are cleared on
  /reload as the SetItem records are.
This commit is contained in:
Brues
2026-11-06 11:11:18 -06:00
parent ebfd9d034d
commit 7b21453469
18 changed files with 467 additions and 412 deletions
+6
View File
@@ -252,6 +252,12 @@ void *ResolveEditBox(void *L, int idx, bool raiseError) {
void *ResolveModel(void *L, int idx, bool raiseError) {
return ResolveTypedObject(L, idx, Offsets::VAR_MODEL_LUA_TYPE_ID, raiseError);
}
void *ResolvePlayerModel(void *L, int idx, bool raiseError) {
return ResolveTypedObject(L, idx, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID, raiseError);
}
void *ResolveDressUpModel(void *L, int idx, bool raiseError) {
return ResolveTypedObject(L, idx, Offsets::VAR_DRESSUPMODEL_LUA_TYPE_ID, raiseError);
}
// Looks up `_G[name]`. If absent, creates a fresh table and binds it.
// Leaves the resulting table on top of the stack.
+5 -1
View File
@@ -240,7 +240,9 @@ constexpr int UpvalueIndex(int i) { return GLOBALS_INDEX - i; }
// `lua_call` / `lua_pcall` nresults sentinel meaning "all".
constexpr int MULTRET = -1;
// Type tag values returned by `Type()` (lua_type).
// Type tag values returned by `Type()` (lua_type). TYPE_NONE is an absent
// argument (past the top of the stack).
constexpr int TYPE_NONE = -1;
constexpr int TYPE_NIL = 0;
constexpr int TYPE_BOOLEAN = 1;
constexpr int TYPE_LIGHTUSERDATA = 2;
@@ -479,6 +481,8 @@ void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
void *ResolvePlayerModel(void *L, int idx = 1, bool raiseError = true);
void *ResolveDressUpModel(void *L, int idx = 1, bool raiseError = true);
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
+29 -11
View File
@@ -6168,15 +6168,37 @@ enum Offsets {
// (21) set only the main-hand bit, off-hand-only (22) only the off-hand.
VAR_INVTYPE_SLOT_MASKS = 0x00809200,
// The two `m_inventoryType` values that mean "consumed when used", so
// both the item-consumable test (`C_Item.IsConsumableItem`) and the
// action-slot one (`IsConsumableAction`) single them out. Indices into
// the table above: 24 = `INVTYPE_AMMO`, 25 = `INVTYPE_THROWN`.
// `m_inventoryType` (`OFF_ITEMSTATS_INVENTORY_TYPE`) values, named by
// their key in the string table above (read from WoW.exe).
INVTYPE_NON_EQUIP = 0,
INVTYPE_HEAD = 1,
INVTYPE_NECK = 2,
INVTYPE_SHOULDER = 3,
INVTYPE_BODY = 4,
INVTYPE_CHEST = 5,
INVTYPE_WAIST = 6,
INVTYPE_LEGS = 7,
INVTYPE_FEET = 8,
INVTYPE_WRIST = 9,
INVTYPE_HAND = 10,
INVTYPE_FINGER = 11,
INVTYPE_TRINKET = 12,
INVTYPE_WEAPON = 13,
INVTYPE_SHIELD = 14,
INVTYPE_RANGED = 15,
INVTYPE_CLOAK = 16,
INVTYPE_2HWEAPON = 17,
INVTYPE_BAG = 18,
INVTYPE_TABARD = 19,
INVTYPE_ROBE = 20,
INVTYPE_WEAPONMAINHAND = 21,
INVTYPE_WEAPONOFFHAND = 22,
INVTYPE_HOLDABLE = 23,
INVTYPE_AMMO = 24,
INVTYPE_THROWN = 25,
// Index 16 of the table above (read from WoW.exe). The bag search skips
// a cloak's subclass name ("Cloth"), as 4.3.4's does.
INVTYPE_CLOAK = 16,
INVTYPE_RANGEDRIGHT = 26,
INVTYPE_QUIVER = 27,
INVTYPE_RELIC = 28,
// Faction "displayed list" — the engine maintains a sorted/visible list
// of factions the player has rep with. `Script_GetNumFactions` (at
@@ -10938,10 +10960,6 @@ enum Offsets {
INVSLOT_OFFHAND = 17,
INVSLOT_MAINHAND = 16,
// INVTYPE (`OFF_ITEMSTATS_INVENTORY_TYPE`) of a two-handed weapon,
// which occupies the main-hand slot and blocks the off-hand.
INVTYPE_2HWEAPON = 17,
// Character-pane equipment slot range: 1..19 (head=1, neck=2, …,
// tabard=19). Used by `C_Item.IsEquippedItem` to walk every slot
// looking for a matching item.
+1 -4
View File
@@ -53,9 +53,6 @@ using SetAttr_t = void(__thiscall *)(void *node, const char *name, const char *v
using Build_t = void *(__fastcall *)(const void *node, void *parent, void *status);
using StatusReset_t = void(__thiscall *)(void *self);
// LUA_TNONE: lua_type of an index past the top.
constexpr int kTypeNone = -1;
// 4.3.4's usage line; vanilla's (0x00872F60) has no `id`.
constexpr const char *kUsage =
"Usage: CreateFrame(\"frameType\" [, \"name\"] [, parent] [, \"template\"] [, id])";
@@ -102,7 +99,7 @@ int __fastcall CreateFrame_h(void *L) {
const int parentType = Type(L, 3);
if (!IsString(L, 1) ||
(parentType != kTypeNone && parentType != TYPE_NIL && parentType != TYPE_TABLE)) {
(parentType != TYPE_NONE && parentType != TYPE_NIL && parentType != TYPE_TABLE)) {
Error(L, kUsage);
return 0;
}
+30 -46
View File
@@ -48,6 +48,7 @@
#include "Game.h"
#include "Offsets.h"
#include "model/M2.h"
#include "time/Clock.h"
#include <cstdint>
@@ -58,11 +59,7 @@ 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;
@@ -78,21 +75,13 @@ 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;
}
uint32_t KeyBoneAnim(void *model) {
return reinterpret_cast<GetKeyBoneAnim_t>(Offsets::FUN_M2MODEL_GET_KEY_BONE_ANIM)(model, -1);
}
void Arm(void *model, const PaneState &s) {
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(
model, -1, s.animation, s.variation, 0, s.frozen ? 0.0f : 1.0f, s.doBlend ? 1 : 0, 1);
M2::PlayAnimation(model, -1, s.animation, s.variation, 0, s.frozen ? 0.0f : 1.0f,
s.doBlend ? 1 : 0, 1);
// PlayAnimation stores the id only when it found a sequence to play.
if (!s.frozen || KeyBoneAnim(model) != s.animation)
return;
@@ -109,11 +98,11 @@ void __fastcall UpdateModel_h(void *pane, void * /*edx*/) {
}
PaneState &s = it->second;
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEMODEL_UPDATE_MODEL)(pane);
void *model = PaneModel(pane);
void *model = Pane::Model(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) &&
(s.pending || KeyBoneAnim(model) != s.animation)) {
(deadline == 0 || Time::Clock::Reached(deadline)) && model != nullptr &&
M2::IsLoaded(model, 0, 0) && (s.pending || KeyBoneAnim(model) != s.animation)) {
Arm(model, s);
s.pending = false;
Game::Ref<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE) = 0;
@@ -135,29 +124,33 @@ const Game::HookAutoRegister _setModelHook{Offsets::FUN_SIMPLEMODEL_SET_MODEL_IN
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);
// SetAnimation / FreezeAnimation: the pane rests on `animation`, re-armed on
// the next update.
void Rest(void *pane, uint32_t animation, int variation, bool frozen, uint32_t frame) {
PaneState &s = g_panes[pane];
s.resting = true;
s.pending = true;
s.frozen = frozen;
s.animation = animation;
s.variation = variation;
s.frame = frame;
}
int __fastcall Script_SetAnimation(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(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.pending = true;
s.frozen = false;
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;
Rest(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)),
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0, false, 0);
return 0;
}
int __fastcall Script_FreezeAnimation(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
@@ -165,24 +158,20 @@ int __fastcall Script_FreezeAnimation(void *L) {
return 0;
}
const double frame = Game::Lua::ToNumber(L, 4);
PaneState &s = g_panes[pane];
s.resting = true;
s.pending = true;
s.frozen = true;
s.animation = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
s.variation = static_cast<int>(Game::Lua::ToNumber(L, 3));
s.frame = frame > 0.0 ? static_cast<uint32_t>(frame) : 0;
Rest(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)),
static_cast<int>(Game::Lua::ToNumber(L, 3)), true,
frame > 0.0 ? static_cast<uint32_t>(frame) : 0);
return 0;
}
int __fastcall Script_SetDoBlend(void *L) {
if (void *pane = ResolvePane(L))
if (void *pane = Game::Lua::ResolvePlayerModel(L))
g_panes[pane].doBlend = Game::Lua::ToBoolean(L, 2) != 0;
return 0;
}
int __fastcall Script_GetDoBlend(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
const auto it = g_panes.find(pane);
@@ -191,7 +180,7 @@ int __fastcall Script_GetDoBlend(void *L) {
}
int __fastcall Script_HasAnimation(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -200,15 +189,10 @@ int __fastcall Script_HasAnimation(void *L) {
}
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);
}
void *model = Pane::Model(pane);
if (model != nullptr && M2::IsLoaded(model, 0, 0))
has = M2::LookupIndex(model, Offsets::OFF_M2DATA_ANIMATION_LOOKUP, id) <
Game::Read<uint32_t>(M2::Data(model), Offsets::OFF_M2DATA_SEQUENCE_COUNT);
Game::Lua::PushBool(L, has);
return 1;
}
+14 -31
View File
@@ -41,6 +41,7 @@
#include "Game.h"
#include "Offsets.h"
#include "model/M2.h"
#include <cmath>
#include <cstdint>
@@ -55,19 +56,13 @@ struct Settings {
};
std::unordered_map<void *, Settings> g_settings;
using GetVector_t = void(__fastcall *)(void *camera, uint32_t id, float *out);
using GetScalar_t = float(__fastcall *)(void *camera, uint32_t id);
using SetVector_t = void(__fastcall *)(void *camera, uint32_t id, const float *value,
uint32_t keep);
using SetScalar_t = void(__fastcall *)(void *camera, uint32_t id, float value);
using GetCameraCount_t = uint32_t(__thiscall *)(void *model);
using GetCamera_t = void *(__thiscall *)(void *model, uint32_t index);
using LookupIndex_t = uint16_t(__fastcall *)(uint32_t id, const void *table);
using SetAnimated_t = void(__thiscall *)(void *model, int animated);
using GetScene_t = void *(__thiscall *)(void *frame);
using SceneAnimate_t = void(__thiscall *)(void *scene, const float *cameraPos);
using UpdateModel_t = void(__thiscall *)(void *frame);
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
using ConfigureCamera_t = void(__thiscall *)(void *pane);
} // namespace
@@ -76,27 +71,21 @@ using ConfigureCamera_t = void(__thiscall *)(void *pane);
void *CameraById(void *model, uint32_t id) {
const uint32_t count =
reinterpret_cast<GetCameraCount_t>(Offsets::FUN_M2MODEL_GET_CAMERA_COUNT)(model);
const uint8_t *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_CAMERA_ID_LOOKUP);
const uint16_t index = M2::LookupIndex(model, Offsets::OFF_M2DATA_CAMERA_ID_LOOKUP, id);
if (index >= count)
return nullptr;
return reinterpret_cast<GetCamera_t>(Offsets::FUN_M2MODEL_GET_CAMERA)(model, index);
}
namespace {
struct Vec3 {
float x, y, z;
};
Vec3 GetVector(void *camera, uint32_t id) {
using Fn = void(__fastcall *)(void *camera, uint32_t id, float *out);
Vec3 v{};
reinterpret_cast<GetVector_t>(Offsets::FUN_CAMERA_GET_VECTOR)(camera, id, &v.x);
reinterpret_cast<Fn>(Offsets::FUN_CAMERA_GET_VECTOR)(camera, id, &v.x);
return v;
}
namespace {
void SetVector(void *camera, uint32_t id, const Vec3 &v) {
reinterpret_cast<SetVector_t>(Offsets::FUN_CAMERA_SET_VECTOR)(camera, id, &v.x, 0);
}
@@ -117,10 +106,9 @@ void AnimateUnrotated(void *pane, void *model) {
facing = 0.0f;
const void *vtable = Game::Read<const void *>(pane, 0);
Game::Read<UpdateModel_t>(vtable, Offsets::VTBL_SIMPLEMODEL_UPDATE_MODEL)(pane);
reinterpret_cast<SetAnimated_t>(Offsets::FUN_M2MODEL_SET_ANIMATED)(model, 1);
M2::SetAnimated(model, true);
const float origin[3] = {0.0f, 0.0f, 0.0f};
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
reinterpret_cast<SceneAnimate_t>(Offsets::FUN_M2SCENE_ANIMATE)(scene, origin);
M2::AnimateScene(Pane::Scene(pane), origin);
facing = saved;
}
@@ -131,7 +119,7 @@ Vec3 Lerp(const Vec3 &from, const Vec3 &to, float t) {
// 4.3.4 ConfigureCamera's last two steps, on the camera vanilla installed.
void ApplySettings(void *pane, const Settings &s) {
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
void *model = Pane::Model(pane);
void *camera = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODEL_CAMERA);
if (model == nullptr || camera == nullptr)
return;
@@ -190,19 +178,14 @@ const Game::HookAutoRegister _cameraHook{
// be loaded: ConfigureCamera reads its cameras and bounds, and an unloaded
// model is configured by the ready hook when it arrives, settings included.
void Reconfigure(void *pane) {
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
if (model == nullptr ||
!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, 0, 0))
void *model = Pane::Model(pane);
if (model == nullptr || !M2::IsLoaded(model, 0, 0))
return;
reinterpret_cast<ConfigureCamera_t>(Offsets::FUN_PLAYERMODEL_CONFIGURE_CAMERA)(pane);
}
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
}
int __fastcall Script_SetPortraitZoom(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -219,7 +202,7 @@ int __fastcall Script_SetPortraitZoom(void *L) {
}
int __fastcall Script_SetCamDistanceScale(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -241,7 +224,7 @@ int __fastcall Script_SetCamDistanceScale(void *L) {
}
int __fastcall Script_RefreshCamera(void *L) {
if (void *pane = ResolvePane(L))
if (void *pane = Game::Lua::ResolvePlayerModel(L))
Reconfigure(pane);
return 0;
}
+7
View File
@@ -14,7 +14,14 @@
namespace Model::Camera {
struct Vec3 {
float x, y, z;
};
// The M2 model's camera with id `id` (0 = portrait), or null when it has none.
void *CameraById(void *model, uint32_t id);
// A camera's vector property (Offsets::CAMERA_PROP_EYE / _TARGET).
Vec3 GetVector(void *camera, uint32_t id);
} // namespace Model::Camera
+53 -105
View File
@@ -55,6 +55,7 @@
#include "dbc/Lookup.h"
#include "model/DisplayInfo.h"
#include "model/ItemDisplay.h"
#include "model/M2.h"
#include "tick/WorldTick.h"
#include <cstdint>
@@ -96,8 +97,6 @@ using CharCompDestroy_t = void(__fastcall *)(void *builder);
using CharCompSetInfo_t = char(__thiscall *)(void *builder, const CharCompInfo *info);
using CharCompSetItem_t = void(__thiscall *)(void *builder, int slot, int itemDisplayID);
using CharCompPump_t = char(__thiscall *)(void *builder, int *unused);
using ModelAddRef_t = void(__fastcall *)(void *instance);
using ModelLoaded_t = int(__thiscall *)(void *instance, int tryLoad, int recurseChildren);
// A pending / finished dress for one Model frame. The model loads async, and
// the engine only dresses a LOADED model (its poll gate FUN_00607da0 checks
@@ -107,100 +106,59 @@ using ModelLoaded_t = int(__thiscall *)(void *instance, int tryLoad, int recurse
// lifetime, and it holds the ref on the model instance — until the frame
// loads another display or the reload teardown.
struct DressJob {
void *frame = nullptr; // CSimpleModelFFX that requested the dress
void *instance = nullptr; // model instance the job is bound to
void *instance = nullptr; // model instance the job is bound to
const uint8_t *extraRec = nullptr; // CreatureDisplayInfoExtra (DBC memory, stable)
void *builder = nullptr; // engine compositor (owned)
void *builder = nullptr; // engine compositor (owned)
bool configured = false;
bool done = false;
int ticks = 0;
};
// Model frames are few and permanent; SetDisplayInfo users fewer still.
DressJob g_jobs[8];
std::unordered_map<void *, DressJob> g_jobs; // by frame
// Give up on a model that never finishes loading (bad path, missing file).
constexpr int kMaxDressTicks = 600;
void CancelJob(DressJob &job) {
DestroyDresser(job.builder);
job = DressJob{};
}
DressJob *FindJob(void *frame) {
for (auto &job : g_jobs)
if (job.frame == frame)
return &job;
return nullptr;
}
// Claim a slot for `frame`: its own existing slot, a free one, or — all busy —
// a finished one (the dressed model keeps its geosets and texture refs; only
// the kept-alive builder is dropped early).
DressJob *ClaimJob(void *frame) {
DressJob *slot = FindJob(frame);
if (slot == nullptr)
for (auto &job : g_jobs)
if (job.frame == nullptr) {
slot = &job;
break;
}
if (slot == nullptr)
for (auto &job : g_jobs)
if (job.done) {
slot = &job;
break;
}
if (slot == nullptr)
slot = &g_jobs[0];
CancelJob(*slot);
return slot;
}
// Queue a dress for a character display just loaded into `frame`.
void ScheduleDress(void *frame, const uint8_t *extraRec) {
DressJob *job = ClaimJob(frame);
job->frame = frame;
job->instance = Game::Read<void *>(frame,
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
job->extraRec = extraRec;
void CancelJob(void *frame) {
const auto it = g_jobs.find(frame);
if (it == g_jobs.end())
return;
DestroyDresser(it->second.builder);
g_jobs.erase(it);
}
// One WorldTick step for one job: wait for the load, then configure + feed
// equipment (the engine's order: SetInfo, then the 10 Extra equipment slots),
// then pump until done. Field-for-field mirror of FUN_005fb200's NPC branch.
void StepJob(DressJob &job) {
void *instance = Game::Read<void *>(job.frame,
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
// equipment, then pump until done. False once the job is dead.
bool StepJob(void *frame, DressJob &job) {
void *instance = Pane::Model(frame);
if (job.instance == nullptr)
job.instance = instance;
if (instance == nullptr || instance != job.instance || ++job.ticks > kMaxDressTicks) {
CancelJob(job); // model swapped out from under us / never loaded
return;
}
if (instance == nullptr || instance != job.instance || ++job.ticks > kMaxDressTicks)
return false; // model swapped out from under us / never loaded
if (!job.configured) {
if (!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(instance, 0, 0))
return;
if (!M2::IsLoaded(instance, 0, 0))
return true;
job.builder = CreateDresser(instance, job.extraRec);
if (job.builder == nullptr) {
CancelJob(job);
return;
}
if (job.builder == nullptr)
return false;
// The helm/shoulder feeds attach child model instances.
LightChildren(job.frame, instance);
LightChildren(frame, instance);
job.configured = true;
}
if (PumpDresser(job.builder))
job.done = true; // builder kept — it owns the model ref for the dressed model
return true;
}
void OnWorldTick() {
for (auto &job : g_jobs)
if (job.frame != nullptr && !job.done)
StepJob(job);
for (auto it = g_jobs.begin(); it != g_jobs.end();) {
if (it->second.done || StepJob(it->first, it->second)) {
++it;
continue;
}
DestroyDresser(it->second.builder);
it = g_jobs.erase(it);
}
}
const Tick::WorldTick::AutoSubscribe g_tick{&OnWorldTick};
@@ -216,8 +174,7 @@ AttachDisplayModel_t g_origAttach = nullptr;
void __fastcall AttachDisplayModel_h(void *pane, void * /*edx*/, const uint8_t *record) {
// Any pending or kept dress for this pane is about to point at the wrong
// model — drop it before the original swaps the instance.
if (DressJob *job = FindJob(pane))
CancelJob(*job);
CancelJob(pane);
if (Model::ItemDisplay::Attach(pane, record))
return;
@@ -227,7 +184,7 @@ void __fastcall AttachDisplayModel_h(void *pane, void * /*edx*/, const uint8_t *
return;
if (const uint8_t *extraRec =
DressRecord(Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID)))
ScheduleDress(pane, extraRec);
g_jobs[pane] = DressJob{Pane::Model(pane), extraRec};
}
const Game::HookAutoRegister _attachHook{
@@ -237,31 +194,20 @@ const Game::HookAutoRegister _attachHook{
// ---- Lua methods --------------------------------------------------------------
using SetDisplay_t = void(__thiscall *)(void *pane, const uint8_t *record);
void SetDisplay(void *pane, const uint8_t *record) {
reinterpret_cast<SetDisplay_t>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, record);
}
// The record a pane shows for SetDisplayInfo: only the display-ID field is
// ever read. One per pane, never freed — the pane keeps the pointer and
// rebuilds from it on every show, and panes live as long as the UI.
struct DisplayRecord {
uint8_t bytes[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)];
};
std::unordered_map<void *, DisplayRecord> g_displayRecords;
// The record each pane shows for SetDisplayInfo.
std::unordered_map<void *, Pane::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);
Pane::DisplayRecord &rec = g_displayRecords[pane];
rec = {};
Game::Ref<uint32_t>(rec.bytes, Offsets::OFF_CREATURE_DISPLAYID) = displayID;
Pane::SetDisplay(pane, rec.bytes);
}
int __fastcall Script_SetDisplayInfo(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -273,7 +219,7 @@ int __fastcall Script_SetDisplayInfo(void *L) {
}
int __fastcall Script_SetCreature(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -287,7 +233,7 @@ int __fastcall Script_SetCreature(void *L) {
if (Game::Lua::IsNumber(L, 3))
ShowDisplayID(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 3)));
else
SetDisplay(pane, record);
Pane::SetDisplay(pane, record);
return 0;
}
@@ -322,6 +268,17 @@ void RegisterLuaFunctions() {
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
// The reload teardown destroys every Model frame, and with it the model
// instances the dress jobs point at: destroy their compositors first.
void PrepareForReload() {
for (auto &[frame, job] : g_jobs)
DestroyDresser(job.builder);
g_jobs.clear();
g_displayRecords.clear();
}
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
// The engine's own early-out: an Extra record with no bake name gets no
@@ -371,7 +328,7 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
std::memcpy(info.geosetDefaults, kGeosetGroupDefaults, sizeof(kGeosetGroupDefaults));
// The builder owns this ref (releases it on destroy / re-configure).
reinterpret_cast<ModelAddRef_t>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(instance);
M2::AddRef(instance);
if (!reinterpret_cast<CharCompSetInfo_t>(Offsets::FUN_CHARCOMP_SET_INFO)(builder, &info)) {
DestroyDresser(builder); // releases the model ref already copied in
return nullptr;
@@ -391,14 +348,12 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
// black. Each child gets the frame's own light/fog callback; it reads the
// frame's live light struct, so later SetLight calls reach them too.
void LightChildren(void *frame, void *instance) {
using SetRenderCb_t = void(__thiscall *)(void *instance, const void *cb, void *userData);
auto setRenderCb = reinterpret_cast<SetRenderCb_t>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB);
const void *lightFogCb =
reinterpret_cast<const void *>(Offsets::FUN_SIMPLEMODELFFX_LIGHT_FOG_CB);
for (auto *child = Game::Read<uint8_t *>(instance, Offsets::OFF_MODEL_INSTANCE_CHILD_HEAD);
child != nullptr;
child = Game::Read<uint8_t *>(child, Offsets::OFF_MODEL_INSTANCE_CHILD_NEXT))
setRenderCb(child, lightFogCb, frame);
M2::SetRenderCallback(child, lightFogCb, frame);
}
bool PumpDresser(void *dresser) {
@@ -410,11 +365,4 @@ void DestroyDresser(void *dresser) {
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(dresser);
}
void PrepareForReload() {
for (auto &job : g_jobs)
CancelJob(job);
}
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace Model::DisplayInfo
-5
View File
@@ -31,9 +31,4 @@ void DestroyDresser(void *dresser);
// `frame` gives its own model, so the children are lit like it.
void LightChildren(void *frame, void *instance);
// The reload teardown destroys every Model frame (and with it the model
// instances our pending character-dress jobs point at). Drop all jobs and
// destroy their engine compositors before the engine starts tearing down.
void PrepareForReload();
} // namespace Model::DisplayInfo
+77 -55
View File
@@ -73,6 +73,8 @@
#include "item/ID.h"
#include "item/Location.h"
#include "item/Record.h"
#include "model/ItemDisplay.h"
#include "model/M2.h"
#include "object/Resolve.h"
#include <cstdint>
@@ -94,9 +96,7 @@ using TryOnItem_t = void(__thiscall *)(void *pane, uint32_t itemID);
using SetModelInstance_t = void(__thiscall *)(void *pane, void *model);
using ClearModel_t = void(__thiscall *)(void *pane);
using GetHeldItemDisplay_t = const void *(__thiscall *)(void *unit, uint32_t hand);
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
using FindChild_t = void *(__thiscall *)(void *model, int attachId);
using AttachItemVisuals_t = void(__fastcall *)(void *model, uint32_t visualID);
using GetVisibleItem_t = const void *(__thiscall *)(void *player, uint32_t slot);
using GetDescVisual_t = int(__thiscall *)(const void *visibleItem);
using DisplayError_t = void(__cdecl *)(uint32_t id, ...);
@@ -104,8 +104,6 @@ using DisplayError_t = void(__cdecl *)(uint32_t id, ...);
// Body slots of the two held lanes.
constexpr uint32_t kBodySlotMainHand = 0xF;
constexpr uint32_t kBodySlotOffHand = 0x10;
constexpr uint8_t kInvTypeShield = 14;
constexpr uint8_t kInvTypeRangedRight = 26;
// Retail's Enum.ItemTryOnReason.
constexpr int kTryOnSuccess = 0;
@@ -113,6 +111,16 @@ constexpr int kTryOnWrongRace = 1;
constexpr int kTryOnNotEquippable = 2;
constexpr int kTryOnDataPending = 3;
// A held lane (OFF_DRESSUPMODEL_*_HAND_LANE).
struct HeldLane {
uint8_t itemClass; // 0 = empty
uint8_t unknown1[2];
uint8_t inventoryType;
uint8_t sheatheType;
uint8_t unknown5[3];
};
static_assert(sizeof(HeldLane) == 8, "ClearModel zeroes 8 bytes per lane");
struct Lane {
uintptr_t offset;
uint32_t bodySlot;
@@ -140,12 +148,8 @@ bool g_dressing = false; // Dress() is rebuilding the pane
uintptr_t g_mainHandPath = Offsets::TRY_ON_MAIN_HAND_PATH;
uintptr_t g_offHandPath = Offsets::TRY_ON_OFF_HAND_PATH;
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_DRESSUPMODEL_LUA_TYPE_ID);
}
void *PaneModel(void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
HeldLane &LaneOf(void *pane, int i) {
return Game::Ref<HeldLane>(pane, kLanes[i].offset);
}
int LaneIndex(uint32_t bodySlot) {
@@ -153,12 +157,10 @@ int LaneIndex(uint32_t bodySlot) {
}
// The glow `enchantID` gives an item shown with `display`: none when the
// display has its own ItemVisuals row (GetItemDescVisual 0x0062EC70).
// display has its own.
uint32_t EnchantVisual(const void *display, uint32_t enchantID) {
if (display == nullptr ||
DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS, Offsets::VAR_ITEMVISUALS_COUNT,
Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL)) !=
nullptr)
Model::ItemDisplay::OwnVisual(static_cast<const uint8_t *>(display)) != 0)
return 0;
const uint8_t *enchant = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
@@ -174,27 +176,33 @@ void HangVisual(void *model, int attachId, uint32_t visual) {
return;
if (void *item = reinterpret_cast<FindChild_t>(Offsets::FUN_M2MODEL_FIND_CHILD)(model,
attachId))
reinterpret_cast<AttachItemVisuals_t>(Offsets::FUN_ATTACH_ITEM_VISUALS)(item, visual);
M2::AttachItemVisuals(item, visual);
}
// DetachHeldItemSlot on lane `i`'s item.
void DetachLane(void *model, int i, const HeldLane &lane) {
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
model, kLanes[i].bodySlot, lane.sheatheType,
lane.inventoryType == Offsets::INVTYPE_SHIELD);
}
// The unit's AttachHandItem step on each occupied lane: detach it, then hang
// it in the hand or the sheath, or leave it off.
void ApplyWeapons(void *pane, const PaneState &s) {
void *model = PaneModel(pane);
void *model = Pane::Model(pane);
if (model == nullptr)
return;
for (int i = 0; i < 2; ++i) {
const uint8_t *lane = Game::Ptr<uint8_t>(pane, kLanes[i].offset);
if (lane[0] == 0 || s.laneDisplay[i] == nullptr)
const HeldLane &lane = LaneOf(pane, i);
if (lane.itemClass == 0 || s.laneDisplay[i] == nullptr)
continue;
const bool shield = lane[3] == kInvTypeShield;
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
model, kLanes[i].bodySlot, lane[4], shield);
DetachLane(model, i, lane);
if (s.hideWeapons)
continue;
const int attachId = reinterpret_cast<AttachItemModel_t>(Offsets::FUN_ATTACH_ITEM_MODEL)(
model, s.laneDisplay[i], kLanes[i].bodySlot, lane[4], s.sheathed, shield,
lane[3] == kInvTypeRangedRight);
model, s.laneDisplay[i], kLanes[i].bodySlot, lane.sheatheType, s.sheathed,
lane.inventoryType == Offsets::INVTYPE_SHIELD,
lane.inventoryType == Offsets::INVTYPE_RANGEDRIGHT);
HangVisual(model, attachId, s.laneVisual[i]);
}
}
@@ -265,8 +273,9 @@ int __fastcall TryOnAttach(void *model, const void *record, uint32_t bodySlot,
void __fastcall TryOnEvict(void *model, uint32_t bodySlot, uint32_t sheatheType, bool shield) {
if (g_tryOnPane != nullptr) {
PaneState &s = g_panes[g_tryOnPane];
s.laneDisplay[LaneIndex(bodySlot)] = nullptr;
s.laneVisual[LaneIndex(bodySlot)] = 0;
const int lane = LaneIndex(bodySlot);
s.laneDisplay[lane] = nullptr;
s.laneVisual[lane] = 0;
}
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(model, bodySlot,
sheatheType, shield);
@@ -294,14 +303,12 @@ __declspec(naked) void HandChoiceThunk() {
}
// ClearModel's lane step: detach the held item and zero the lane.
void ClearLane(void *pane, uintptr_t laneOffset, uint32_t bodySlot) {
uint8_t *lane = Game::Ptr<uint8_t>(pane, laneOffset);
if (lane[0] == 0)
void ClearLane(void *pane, int i) {
HeldLane &lane = LaneOf(pane, i);
if (lane.itemClass == 0)
return;
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE), bodySlot, lane[4],
lane[3] == kInvTypeShield);
std::memset(lane, 0, 8);
DetachLane(Pane::Model(pane), i, lane);
lane = HeldLane{};
}
// Component slot for a 0-based inventory slot, or -1 (4.3.4 UndressSlot).
@@ -323,7 +330,7 @@ int ComponentSlot(int slot0) {
}
int __fastcall Script_UndressSlot(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -332,13 +339,12 @@ int __fastcall Script_UndressSlot(void *L) {
}
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1;
void *component = Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT);
if (Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE) == nullptr ||
component == nullptr)
if (Pane::Model(pane) == nullptr || component == nullptr)
return 0;
if (slot0 == 15) {
ClearLane(pane, Offsets::OFF_DRESSUPMODEL_MAIN_HAND_LANE, kBodySlotMainHand);
} else if (slot0 == 16) {
ClearLane(pane, Offsets::OFF_DRESSUPMODEL_OFF_HAND_LANE, kBodySlotOffHand);
if (slot0 + 1 == Offsets::INVSLOT_MAINHAND) {
ClearLane(pane, 0);
} else if (slot0 + 1 == Offsets::INVSLOT_OFFHAND) {
ClearLane(pane, 1);
} else {
const int compSlot = ComponentSlot(slot0);
if (compSlot >= 0)
@@ -399,8 +405,8 @@ uint32_t NamedHand(const char *name) {
name) != 0)
continue;
switch (Game::Read<int>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER)) {
case 16: return kBodySlotMainHand;
case 17: return kBodySlotOffHand;
case Offsets::INVSLOT_MAINHAND: return kBodySlotMainHand;
case Offsets::INVSLOT_OFFHAND: return kBodySlotOffHand;
default: return 0;
}
}
@@ -413,17 +419,35 @@ int TryOnResult(void *pane, uint32_t itemID) {
if (record == nullptr)
return kTryOnDataPending;
if (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID) == 0 ||
PaneModel(pane) == nullptr)
Pane::Model(pane) == nullptr)
return kTryOnNotEquippable;
switch (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE)) {
case 1: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10:
case 16: case 19: case 20:
case Offsets::INVTYPE_HEAD:
case Offsets::INVTYPE_SHOULDER:
case Offsets::INVTYPE_BODY:
case Offsets::INVTYPE_CHEST:
case Offsets::INVTYPE_WAIST:
case Offsets::INVTYPE_LEGS:
case Offsets::INVTYPE_FEET:
case Offsets::INVTYPE_WRIST:
case Offsets::INVTYPE_HAND:
case Offsets::INVTYPE_CLOAK:
case Offsets::INVTYPE_TABARD:
case Offsets::INVTYPE_ROBE:
// The armor arms: the component and the ready byte.
return Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT) != nullptr &&
Game::Read<uint8_t>(pane, Offsets::OFF_DRESSUPMODEL_READY) != 0
? kTryOnSuccess
: kTryOnNotEquippable;
case 13: case 14: case 15: case 17: case 21: case 22: case 23: case 25: case 26:
case Offsets::INVTYPE_WEAPON:
case Offsets::INVTYPE_SHIELD:
case Offsets::INVTYPE_RANGED:
case Offsets::INVTYPE_2HWEAPON:
case Offsets::INVTYPE_WEAPONMAINHAND:
case Offsets::INVTYPE_WEAPONOFFHAND:
case Offsets::INVTYPE_HOLDABLE:
case Offsets::INVTYPE_THROWN:
case Offsets::INVTYPE_RANGEDRIGHT:
return kTryOnSuccess; // the held arm
default:
return kTryOnNotEquippable;
@@ -431,7 +455,7 @@ int TryOnResult(void *pane, uint32_t itemID) {
}
int __fastcall Script_TryOn(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
// Read before the name search below, which stomps the Lua stack.
@@ -484,18 +508,16 @@ int __fastcall Script_Dress(void *L) {
}
int __fastcall Script_IsGeoReady(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
void *model = PaneModel(pane);
Game::Lua::PushBool(L, model != nullptr &&
reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(
model, 0, 1) != 0);
void *model = Pane::Model(pane);
Game::Lua::PushBool(L, model != nullptr && M2::IsLoaded(model, 0, 1));
return 1;
}
int __fastcall Script_SetSheathed(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
PaneState &s = g_panes[pane];
@@ -507,7 +529,7 @@ int __fastcall Script_SetSheathed(void *L) {
}
int __fastcall Script_GetSheathed(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
const auto it = g_panes.find(pane);
@@ -516,13 +538,13 @@ int __fastcall Script_GetSheathed(void *L) {
}
int __fastcall Script_SetAutoDress(void *L) {
if (void *pane = ResolvePane(L))
if (void *pane = Game::Lua::ResolveDressUpModel(L))
g_panes[pane].autoDress = Game::Lua::ToBoolean(L, 2) != 0;
return 0;
}
int __fastcall Script_GetAutoDress(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
const auto it = g_panes.find(pane);
+36 -53
View File
@@ -53,6 +53,7 @@
#include "item/Record.h"
#include "model/DisplayInfo.h"
#include "model/ItemDisplay.h"
#include "model/M2.h"
#include "texture/Load.h"
#include <cstdint>
@@ -64,24 +65,9 @@ namespace Model::ItemDisplay {
namespace {
using PaneRecordFn_t = void(__thiscall *)(void *pane, const uint8_t *record);
using SetModelInstance_t = void(__thiscall *)(void *frame, void *model);
using GetScene_t = void *(__thiscall *)(void *frame);
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
using ModelRelease_t = void(__fastcall *)(void *model);
using TextureLoad_t = void *(__fastcall *)(const char *path, void *status, uint32_t flags,
int arg4, int arg5);
using ReplaceTexture_t = void(__thiscall *)(void *model, uint32_t type, void *texture);
using AttachItemVisuals_t = void(__fastcall *)(void *model, uint32_t visualID);
using SetAnimDone_t = void(__thiscall *)(void *model, const void *callback, uint32_t userLow,
uint32_t userHigh);
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
int offset, float speed, int blend, int primary);
constexpr uint32_t kInvTypeHead = 1;
constexpr uint32_t kInvTypeShoulders = 3;
constexpr uint32_t kInvTypeShield = 14;
constexpr uint32_t kInvTypeAmmo = 24;
constexpr uint32_t kInvTypeQuiver = 27;
// The texture type an item model takes its skin as.
constexpr uint32_t kItemSkinTexture = 2;
@@ -90,27 +76,25 @@ constexpr uint32_t kItemTextureFilter = 3;
constexpr int kSexLetters = 3;
constexpr size_t kPathSize = 260;
// The record a pane shows for SetItem. The pane reads only the display-ID
// field, which stays 0. One per pane, never freed while the UI lives: the pane
// keeps the pointer and builds from it on every show.
// The record a pane shows for SetItem; its display ID stays 0.
struct ItemRecord {
uint8_t display[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)];
uint32_t itemID;
uint32_t visual;
Pane::DisplayRecord display;
uint32_t itemID = 0;
uint32_t visual = 0;
};
std::unordered_map<void *, ItemRecord> g_records;
const char *Directory(uint32_t inventoryType) {
switch (inventoryType) {
case kInvTypeHead:
case Offsets::INVTYPE_HEAD:
return "Item\\ObjectComponents\\Head\\";
case kInvTypeShoulders:
case Offsets::INVTYPE_SHOULDER:
return "Item\\ObjectComponents\\Shoulder\\";
case kInvTypeShield:
case Offsets::INVTYPE_SHIELD:
return "Item\\ObjectComponents\\Shield\\";
case kInvTypeAmmo:
case Offsets::INVTYPE_AMMO:
return "Item\\ObjectComponents\\Ammo\\";
case kInvTypeQuiver:
case Offsets::INVTYPE_QUIVER:
return "Item\\ObjectComponents\\Quiver\\";
default:
return "Item\\ObjectComponents\\Weapon\\";
@@ -166,15 +150,13 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
const char *dir = Directory(inventoryType);
char path[kPathSize];
if (inventoryType == kInvTypeHead) {
if (inventoryType == Offsets::INVTYPE_HEAD) {
if (!HeadPath(path, dir, name))
return nullptr;
} else {
std::snprintf(path, sizeof(path), "%s%s", dir, name);
}
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
void *model =
reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(scene, path, 0);
void *model = M2::CreateModel(Pane::Scene(pane), path);
if (model == nullptr)
return nullptr;
@@ -186,7 +168,7 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
path, reinterpret_cast<void *>(Offsets::VAR_ITEM_MODEL_TEXTURE_STATUS),
Texture::Flags(kItemTextureFilter, false, false), 1, 0);
if (handle == nullptr) {
reinterpret_cast<ModelRelease_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
M2::Release(model);
return nullptr;
}
reinterpret_cast<ReplaceTexture_t>(Offsets::FUN_M2MODEL_REPLACE_TEXTURE)(
@@ -194,13 +176,10 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
Texture::ReleaseHandle(handle);
}
const uint32_t own = Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL);
const uint32_t visual = DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS,
Offsets::VAR_ITEMVISUALS_COUNT, own) != nullptr
? own
: rec.visual;
const uint32_t own = OwnVisual(display);
const uint32_t visual = own != 0 ? own : rec.visual;
if (visual != 0) {
reinterpret_cast<AttachItemVisuals_t>(Offsets::FUN_ATTACH_ITEM_VISUALS)(model, visual);
M2::AttachItemVisuals(model, visual);
Model::DisplayInfo::LightChildren(pane, model);
}
return model;
@@ -210,17 +189,15 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
// for the item shows nothing.
void Show(void *pane, const ItemRecord &rec) {
void *model = CreateItemModel(pane, rec);
Game::Read<SetModelInstance_t>(Game::Read<const void *>(pane, 0),
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, model);
Pane::SetModel(pane, model);
if (model == nullptr)
return;
const auto user = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(pane));
reinterpret_cast<SetAnimDone_t>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(
model, reinterpret_cast<const void *>(Offsets::FUN_PLAYERMODEL_ON_ANIM_DONE), user,
static_cast<int32_t>(user) < 0 ? 0xFFFFFFFFu : 0u);
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, -1, 0, -1, 0,
1.0f, 1, 1);
reinterpret_cast<ModelRelease_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
M2::SetAnimDoneCallback(model,
reinterpret_cast<const void *>(Offsets::FUN_PLAYERMODEL_ON_ANIM_DONE),
user, static_cast<int32_t>(user) < 0 ? 0xFFFFFFFFu : 0u);
M2::PlayAnimation(model, -1, 0, -1, 0, 1.0f, 1, 1);
M2::Release(model);
}
// A queued item has loaded: build each visible pane that still shows it,
@@ -229,18 +206,16 @@ void OnItemLoaded(uint32_t itemID, bool success) {
if (!success)
return;
for (auto &[pane, rec] : g_records)
if (rec.itemID == itemID &&
Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD) ==
rec.display &&
Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) != 0)
if (rec.itemID == itemID && Pane::DisplayRecordOf(pane) == rec.display.bytes &&
Pane::IsVisible(pane))
reinterpret_cast<PaneRecordFn_t>(Offsets::FUN_PLAYERMODEL_ATTACH_DISPLAY_MODEL)(
pane, rec.display);
pane, rec.display.bytes);
}
const ::Item::Data::LoadSubscriber _loadSub{&OnItemLoaded};
int __fastcall Script_SetItem(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -252,7 +227,7 @@ int __fastcall Script_SetItem(void *L) {
rec.itemID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
if (Game::Lua::IsNumber(L, 4))
rec.visual = static_cast<uint32_t>(Game::Lua::ToNumber(L, 4));
reinterpret_cast<PaneRecordFn_t>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, rec.display);
Pane::SetDisplay(pane, rec.display.bytes);
return 0;
}
@@ -289,9 +264,17 @@ const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
uint32_t OwnVisual(const uint8_t *display) {
const uint32_t own = Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL);
return DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS, Offsets::VAR_ITEMVISUALS_COUNT, own) !=
nullptr
? own
: 0;
}
bool Attach(void *pane, const uint8_t *record) {
const auto it = g_records.find(pane);
if (record == nullptr || it == g_records.end() || record != it->second.display)
if (record == nullptr || it == g_records.end() || record != it->second.display.bytes)
return false;
if (::Item::PeekRecord(it->second.itemID) == nullptr)
::Item::Data::WarmCache(it->second.itemID);
+5
View File
@@ -22,4 +22,9 @@ namespace Model::ItemDisplay {
// other record returns false and is the engine's to build.
bool Attach(void *pane, const uint8_t *record);
// The ItemVisuals row an ItemDisplayInfo `display` hangs itself, or 0 when it
// names none that exists. An enchant's glow shows only when this is 0
// (GetItemDescVisual 0x0062EC70).
uint32_t OwnVisual(const uint8_t *display);
} // namespace Model::ItemDisplay
+13 -21
View File
@@ -30,6 +30,7 @@
#include "Game.h"
#include "Offsets.h"
#include "model/M2.h"
#include <cstdint>
#include <unordered_set>
@@ -40,7 +41,6 @@ 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;
@@ -53,19 +53,16 @@ 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);
// Hide's own drop (slot 36 with null), for a hidden pane still holding a
// model.
void DropIfHidden(void *pane) {
if (!Pane::IsVisible(pane) && Pane::Model(pane) != nullptr)
Pane::SetModel(pane, nullptr);
}
void DropIfKeptWhileHidden(void *pane) {
if (Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 && HasModel(pane) && Keeps(pane))
DropModel(pane);
if (Keeps(pane))
DropIfHidden(pane);
}
void __fastcall Hide_h(void *pane, void * /*edx*/) {
@@ -77,7 +74,7 @@ void __fastcall Hide_h(void *pane, void * /*edx*/) {
}
void __fastcall Show_h(void *pane, void * /*edx*/) {
if (Keeps(pane) && HasModel(pane)) {
if (Keeps(pane) && Pane::Model(pane) != nullptr) {
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEFRAME_SHOW)(pane);
return;
}
@@ -107,26 +104,21 @@ const Game::HookAutoRegister _setDisplayHook{Offsets::FUN_PLAYERMODEL_SET_DISPLA
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);
void *pane = Game::Lua::ResolvePlayerModel(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);
if (g_keeping.erase(pane) != 0)
DropIfHidden(pane);
return 0;
}
int __fastcall Script_GetKeepModelOnHide(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
Game::Lua::PushBool(L, Keeps(pane));
+148
View File
@@ -0,0 +1,148 @@
// 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/>.
// Typed calls into the engine's M2 model instances and the Model frames
// ("panes") that show them, shared by every model/ module.
#pragma once
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Model::M2 {
inline bool IsLoaded(void *model, int tryLoad, int recurseChildren) {
using Fn = int(__thiscall *)(void *, int, int);
return reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, tryLoad,
recurseChildren) != 0;
}
// The draw gate: also makes the device textures when `wait` is set.
inline bool IsReady(void *model, int wait, int recurseChildren) {
using Fn = int(__thiscall *)(void *, int, int);
return reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_READY)(model, wait,
recurseChildren) != 0;
}
inline void AddRef(void *model) {
reinterpret_cast<void(__fastcall *)(void *)>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(model);
}
inline void Release(void *model) {
reinterpret_cast<void(__fastcall *)(void *)>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
}
// A new model in `scene` that the caller owns one reference on, or null.
inline void *CreateModel(void *scene, const char *path) {
using Fn = void *(__thiscall *)(void *, const char *, uint32_t);
return reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_CREATE_MODEL)(scene, path, 0);
}
inline void PlayAnimation(void *model, int keyBone, uint32_t id, int variation, int offset,
float speed, int blend, int primary) {
using Fn = void(__thiscall *)(void *, int, uint32_t, int, int, float, int, int);
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, keyBone, id, variation,
offset, speed, blend, primary);
}
inline void SetAnimDoneCallback(void *model, const void *callback, uint32_t userLow,
uint32_t userHigh) {
using Fn = void(__thiscall *)(void *, const void *, uint32_t, uint32_t);
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(model, callback, userLow,
userHigh);
}
inline void SetVisible(void *model, bool visible) {
using Fn = void(__thiscall *)(void *, int);
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_VISIBLE)(model, visible ? 1 : 0);
}
inline void SetAnimated(void *model, bool animated) {
using Fn = void(__thiscall *)(void *, int);
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_ANIMATED)(model, animated ? 1 : 0);
}
inline void AnimateScene(void *scene, const float *cameraPos) {
using Fn = void(__thiscall *)(void *, const float *);
reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_ANIMATE)(scene, cameraPos);
}
inline void AttachItemVisuals(void *model, uint32_t visualID) {
using Fn = void(__fastcall *)(void *, uint32_t);
reinterpret_cast<Fn>(Offsets::FUN_ATTACH_ITEM_VISUALS)(model, visualID);
}
inline void SetRenderCallback(void *model, const void *callback, void *userData) {
using Fn = void(__thiscall *)(void *, const void *, void *);
reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB)(model, callback, userData);
}
// The model's shared M2 data (the file's header and tables).
inline const uint8_t *Data(void *model) {
return Game::Read<const uint8_t *>(Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED),
Offsets::OFF_M2SHARED_DATA);
}
// The index `id` maps to in one of the data's lookup tables
// (OFF_M2DATA_*_LOOKUP); the caller bounds it by that table's count.
inline uint16_t LookupIndex(void *model, uintptr_t tableOffset, uint32_t id) {
using Fn = uint16_t(__fastcall *)(uint32_t, const void *);
return reinterpret_cast<Fn>(Offsets::FUN_M2_LOOKUP_INDEX)(id, Data(model) + tableOffset);
}
} // namespace Model::M2
namespace Model::Pane {
// The model instance a Model frame shows, or null.
inline void *Model(const void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
}
inline void *Scene(void *pane) {
using Fn = void *(__thiscall *)(void *);
return reinterpret_cast<Fn>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
}
// The frame's SetModelInstance virtual; null drops its model.
inline void SetModel(void *pane, void *model) {
using Fn = void(__thiscall *)(void *, void *);
Game::Read<Fn>(Game::Read<const void *>(pane, 0),
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, model);
}
// PlayerModel's SetDisplay: stores the display record the pane builds from.
inline void SetDisplay(void *pane, const uint8_t *record) {
using Fn = void(__thiscall *)(void *, const uint8_t *);
reinterpret_cast<Fn>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, record);
}
inline bool IsVisible(const void *pane) {
return Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) != 0;
}
// The display record the pane shows (SetDisplay's), or null.
inline const uint8_t *DisplayRecordOf(const void *pane) {
return Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD);
}
// A display record of our own for SetDisplay. The pane reads only its
// display-ID field, keeps the pointer and builds from it on every show, so
// the record lives as long as the pane.
struct DisplayRecord {
uint8_t bytes[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)] = {};
};
} // namespace Model::Pane
+18 -33
View File
@@ -54,6 +54,7 @@
#include "dbc/Lookup.h"
#include "model/Camera.h"
#include "model/DisplayInfo.h"
#include "model/M2.h"
#include "texture/Load.h"
#include "tick/FrameTick.h"
@@ -71,19 +72,11 @@ constexpr uint32_t kTexels = kSize * kSize;
using CreateScene_t = void *(__cdecl *)();
using ReleaseScene_t = void(__thiscall *)(void *scene);
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
using CopyModel_t = void *(__thiscall *)(void *scene, void *source, uint32_t flags);
using ApplyDisplay_t = void(__fastcall *)(void *model, const uint8_t *display,
const uint8_t *modelData);
using ReleaseModel_t = void(__fastcall *)(void *model);
using ModelState_t = int(__thiscall *)(void *model, int wait, int recurseChildren);
using SetFlag_t = void(__thiscall *)(void *model, int on);
using PlayAnimation_t = void(__thiscall *)(void *model, int, int, int, int, float, int, int);
using SetRenderCb_t = void(__thiscall *)(void *model, const void *cb, void *userData);
using DetachAll_t = void(__fastcall *)(void *model);
using AnimateScene_t = void(__thiscall *)(void *scene, const float *cameraPos);
using DrawScene_t = void(__thiscall *)(void *scene, uint32_t pass);
using GetVector_t = void(__fastcall *)(void *camera, uint32_t id, float *out);
using SetTransforms_t = void(__fastcall *)(void *camera, const float *rect, uint32_t unused);
using GetMatrix_t = void(__fastcall *)(float *m);
using SetMatrix_t = void(__fastcall *)(const float *m);
@@ -130,7 +123,7 @@ std::vector<Portrait *> g_loading;
void ReleaseModel(Portrait &p) {
Model::DisplayInfo::DestroyDresser(p.dresser);
reinterpret_cast<ReleaseModel_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(p.model);
M2::Release(p.model);
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(p.scene);
p.dresser = p.model = p.scene = nullptr;
p.dressRecord = nullptr;
@@ -166,8 +159,7 @@ bool Bake(Portrait &p) {
}
reinterpret_cast<DetachAll_t>(Offsets::FUN_DETACH_ALL_ITEM_MODELS)(copy);
void *camera = Model::Camera::CameraById(copy, 0);
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(copy, -1, 0, 0, 0,
1.0f, 0, 1);
M2::PlayAnimation(copy, -1, 0, 0, 0, 1.0f, 0, 1);
float projection[16];
float view[16];
@@ -188,19 +180,12 @@ bool Bake(Portrait &p) {
toNdc(&x1, &y1, x1, y1);
// First collection: evaluate the portrait camera.
auto isLoaded = reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_LOADED);
auto isReady = reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_READY);
auto setAnimated = reinterpret_cast<SetFlag_t>(Offsets::FUN_M2MODEL_SET_ANIMATED);
auto animate = reinterpret_cast<AnimateScene_t>(Offsets::FUN_M2SCENE_ANIMATE);
auto getVector = reinterpret_cast<GetVector_t>(Offsets::FUN_CAMERA_GET_VECTOR);
isLoaded(copy, 1, 1);
setAnimated(copy, 1);
M2::IsLoaded(copy, 1, 1);
M2::SetAnimated(copy, true);
const float origin[3] = {0.0f, 0.0f, 0.0f};
animate(scene, origin);
float eye[3] = {};
float target[3] = {};
getVector(camera, Offsets::CAMERA_PROP_EYE, eye);
getVector(camera, Offsets::CAMERA_PROP_TARGET, target);
M2::AnimateScene(scene, origin);
const Model::Camera::Vec3 eye = Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_EYE);
Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_TARGET); // the bake reads it too
const float rect[4] = {0.0f, 0.0f, 1.0f, aspect};
reinterpret_cast<SetTransforms_t>(Offsets::FUN_CAMERA_SET_DEVICE_TRANSFORMS)(camera, rect, 0);
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(x0, x1, y0, y1, 0.0f, 1.0f);
@@ -209,12 +194,12 @@ bool Bake(Portrait &p) {
clear(3);
// Second collection from the portrait eye, then both passes.
isReady(copy, 1, 1);
setAnimated(copy, 1);
reinterpret_cast<SetFlag_t>(Offsets::FUN_M2MODEL_SET_VISIBLE)(copy, 1);
reinterpret_cast<SetRenderCb_t>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB)(
copy, reinterpret_cast<const void *>(Offsets::FUN_PORTRAIT_LIGHTING), nullptr);
animate(scene, eye);
M2::IsReady(copy, 1, 1);
M2::SetAnimated(copy, true);
M2::SetVisible(copy, true);
M2::SetRenderCallback(copy, reinterpret_cast<const void *>(Offsets::FUN_PORTRAIT_LIGHTING),
nullptr);
M2::AnimateScene(scene, &eye.x);
auto draw = reinterpret_cast<DrawScene_t>(Offsets::FUN_M2SCENE_DRAW);
draw(scene, 0);
draw(scene, 1);
@@ -239,14 +224,14 @@ bool Bake(Portrait &p) {
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(
viewport[0], viewport[1], viewport[2], viewport[3], viewport[4], viewport[5]);
setMaster(Offsets::GX_MASTER_FOG, fog ? 1 : 0);
reinterpret_cast<ReleaseModel_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(copy);
M2::Release(copy);
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
return true;
}
// One frame for one pending display; true once it is baked (or cannot be).
bool Step(Portrait &p) {
if (!reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(p.model, 0, 1))
if (!M2::IsLoaded(p.model, 0, 1))
return false;
if (p.dressRecord != nullptr) {
// A dress the engine rejects bakes undressed, as a unit with no
@@ -298,8 +283,8 @@ Portrait *Acquire(uint32_t displayID) {
void *scene = reinterpret_cast<CreateScene_t>(Offsets::FUN_M2_CREATE_SCENE)();
if (scene == nullptr)
return nullptr;
void *model = reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(
scene, Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH), 0);
void *model = M2::CreateModel(
scene, Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH));
if (model == nullptr) {
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
return nullptr;
+5 -7
View File
@@ -28,17 +28,15 @@
namespace Model::Rotation {
namespace {
constexpr int kTypeNone = -1; // lua_type of an absent argument
using LuaMethod_t = int(__fastcall *)(void *L);
int __fastcall Script_SetRotation(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
const bool animate = Game::Lua::Type(L, 3) == kTypeNone || Game::Lua::ToBoolean(L, 3) != 0;
const bool animate =
Game::Lua::Type(L, 3) == Game::Lua::TYPE_NONE || Game::Lua::ToBoolean(L, 3) != 0;
if (animate && !Model::Animation::HasRestingAnimation(pane))
return reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
return reinterpret_cast<Game::Lua::CFunction>(
Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: SetRotation(rotation (in radians))");
return 0;
+4 -10
View File
@@ -33,20 +33,14 @@
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);
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_UNIT)(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
Game::Lua::PushBool(L, PaneUnit(pane) != nullptr);
@@ -54,7 +48,7 @@ int __fastcall Script_SetUnit(void *L) {
}
int __fastcall Script_CanSetUnit(void *L) {
if (ResolvePane(L) == nullptr)
if (Game::Lua::ResolvePlayerModel(L) == nullptr)
return 0;
if (!Game::Lua::IsString(L, 2)) {
Game::Lua::Error(L, "Usage: CanSetUnit(unit)");
@@ -65,7 +59,7 @@ int __fastcall Script_CanSetUnit(void *L) {
}
int __fastcall Script_GetDisplayInfo(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
uint32_t displayID = 0;
+16 -30
View File
@@ -48,6 +48,7 @@
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "model/DisplayInfo.h"
#include "model/M2.h"
#include "tick/FrameTick.h"
#include <cstdint>
@@ -56,19 +57,12 @@
namespace Model::VisualKit {
namespace {
using GetScene_t = void *(__thiscall *)(void *frame);
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
using ModelFn_t = void(__fastcall *)(void *model);
using HasAttachment_t = int(__thiscall *)(void *model, uint32_t id);
using Attach_t = void(__thiscall *)(void *child, void *parent, uint32_t id);
using SetVisible_t = void(__thiscall *)(void *model, int visible);
using SetAlpha_t = void(__thiscall *)(void *model, float alpha);
using SetDiffuse_t = void(__thiscall *)(void *model, const float *rgb);
using HasLiveParticles_t = int(__thiscall *)(void *model);
using SetAnimDone_t = void(__thiscall *)(void *model, const void *callback, uint32_t userLow,
uint32_t userHigh);
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
int offset, float speed, int blend, int primary);
// The attachment of each effect column, in the ghost kit's order.
constexpr uint32_t kAttachments[] = {20, 34, 19, 21, 22, 17, 23, 24, 25};
@@ -81,24 +75,15 @@ constexpr float kByteScale = 1.0f / 255.0f;
// Stopped one-shot effects, each with one reference of ours.
std::vector<void *> g_stopped;
void Release(void *model) {
reinterpret_cast<ModelFn_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
}
void SetDoneCallback(void *model, const void *callback) {
reinterpret_cast<SetAnimDone_t>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(model, callback,
0, 0);
}
// EffectDoneCallback's test, then CEffect::Stop's model steps.
void __fastcall OnEffectDone(void *model, uint32_t /*keyBone*/, uint32_t /*animID*/,
uint32_t reason, uint32_t /*overshoot*/, uint32_t /*userLow*/,
uint32_t /*userHigh*/) {
if (reason != 0)
return;
SetDoneCallback(model, nullptr);
reinterpret_cast<SetVisible_t>(Offsets::FUN_M2MODEL_SET_VISIBLE)(model, 0);
reinterpret_cast<ModelFn_t>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(model);
M2::SetAnimDoneCallback(model, nullptr, 0, 0);
M2::SetVisible(model, false);
M2::AddRef(model);
g_stopped.push_back(model);
}
@@ -108,8 +93,7 @@ void __fastcall OnEffectLoop(void *model, uint32_t /*keyBone*/, uint32_t animID,
uint32_t /*userHigh*/) {
if (reason != 0)
return;
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, -1, animID, -1,
0, 1.0f, 1, 1);
M2::PlayAnimation(model, -1, animID, -1, 0, 1.0f, 1, 1);
}
// Detaches a stopped effect that is still on its model, then drops our
@@ -117,7 +101,7 @@ void __fastcall OnEffectLoop(void *model, uint32_t /*keyBone*/, uint32_t animID,
void Discard(void *model) {
if (Game::Read<void *>(model, Offsets::OFF_M2MODEL_PARENT) != nullptr)
reinterpret_cast<ModelFn_t>(Offsets::FUN_M2MODEL_DETACH)(model);
Release(model);
M2::Release(model);
}
// The dying-effect drain: a stopped effect goes once its particles are gone,
@@ -157,10 +141,10 @@ void ApplyProcedures(void *model, const uint8_t *kit) {
}
void Apply(void *pane, const uint8_t *kit, bool oneShot) {
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
void *model = Pane::Model(pane);
if (model == nullptr)
return;
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
void *scene = Pane::Scene(pane);
for (int i = 0; i < static_cast<int>(sizeof(kAttachments) / sizeof(kAttachments[0])); ++i) {
const uint8_t *effect = DBC::Record(
Offsets::VAR_SPELLVISUALEFFECTNAME_RECORDS, Offsets::VAR_SPELLVISUALEFFECTNAME_COUNT,
@@ -169,21 +153,23 @@ void Apply(void *pane, const uint8_t *kit, bool oneShot) {
!reinterpret_cast<HasAttachment_t>(Offsets::FUN_M2MODEL_HAS_ATTACHMENT)(
model, kAttachments[i]))
continue;
void *child = reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(
scene, Game::Read<const char *>(effect, Offsets::OFF_SPELLVISUALEFFECTNAME_FILE), 0);
void *child = M2::CreateModel(
scene, Game::Read<const char *>(effect, Offsets::OFF_SPELLVISUALEFFECTNAME_FILE));
if (child == nullptr)
continue;
SetDoneCallback(child, oneShot ? reinterpret_cast<const void *>(&OnEffectDone)
: reinterpret_cast<const void *>(&OnEffectLoop));
M2::SetAnimDoneCallback(child,
oneShot ? reinterpret_cast<const void *>(&OnEffectDone)
: reinterpret_cast<const void *>(&OnEffectLoop),
0, 0);
reinterpret_cast<Attach_t>(Offsets::FUN_M2MODEL_ATTACH)(child, model, kAttachments[i]);
Release(child);
M2::Release(child);
}
Model::DisplayInfo::LightChildren(pane, model);
ApplyProcedures(model, kit);
}
int __fastcall Script_ApplySpellVisualKit(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {