feat(model): add PlayerModel:SetItem

PlayerModel:SetItem(itemID [, appearanceModID, itemVisualID]) shows one
item's own model in the pane: a weapon, shield, helm, shoulder, quiver or
ammo, with its skin texture and its glow.

A 1.12.1 pane shows whatever its display record names (+0x3F0).
SetDisplay (0x00505CB0) stores the record and builds it when the pane is
visible, the show path (0x00505D49) builds it again, and the SetUnit
helper (0x00505B50) clears it. SetItem gives the pane a record of its
own and passes it to SetDisplay. The AttachDisplayModel (0x00505A70)
co-hook in Model::DisplayInfo then builds the item instead of a
creature. So hide, show, SetUnit, SetDisplayInfo and SetKeepModelOnHide
handle an item with no new hooks.

The model is built from 1.12.1's own steps:
- the item folder by inventory type, from the engine's own
  Item\ObjectComponents\ path strings (0x00838CD8 on);
- model name 0, else name 1, with the texture of the same index;
- for a head item, the helm builder's (0x004799A0) path, with the
  player's race prefix (ChrRaces +0x18) and sex letter (0x00838AB4);
- AttachItemModelFile's (0x004798C0) steps: create the model in the
  pane's scene, load the skin with the item-model status (0x00B424F0)
  and filter 3, bind it as texture type 2 (0x00710EC0), and attach the
  item visuals (0x00479700);
- AttachDisplayModel's install: SetModelInstance (vtable +0x90), the
  pane's done callback (0x005062A0 through 0x00711BB0), Stand, and the
  release of the creation reference.

The display's own ItemVisuals row wins over itemVisualID, the rule
GetItemDescVisual (0x0062EC70) applies to enchants. The visual's child
models get the frame's light and fog callback, now a shared
LightChildren helper, so they are not drawn black. An uncached item is
queued through Item::Data::WarmCache, and each visible pane that still
shows it is built again when the item loads.
This commit is contained in:
Brues
2026-11-04 14:21:58 -06:00
parent 43a989dd56
commit 8f4f574174
7 changed files with 416 additions and 20 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` (`displayID`), `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:FreezeAnimation`, `PlayerModel:HasAnimation`, `PlayerModel:SetDoBlend` / `GetDoBlend`, `PlayerModel:SetKeepModelOnHide` / `GetKeepModelOnHide` |
| [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:SetItem`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:FreezeAnimation`, `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` |
+32 -4
View File
@@ -781,6 +781,7 @@ build instructions.
- [Model](#model)
- [`model:SetDisplayInfo(displayID)`](#modelsetdisplayinfodisplayid)
- [`model:SetCreature(creatureID [, displayID])`](#modelsetcreaturecreatureid--displayid)
- [`model:SetItem(itemID [, appearanceModID, itemVisualID])`](#modelsetitemitemid--appearancemodid-itemvisualid)
- [`model:SetPortraitZoom(zoom)`](#modelsetportraitzoomzoom)
- [`model:SetCamDistanceScale(scale)`](#modelsetcamdistancescalescale)
- [`model:RefreshCamera()`](#modelrefreshcamera)
@@ -18555,6 +18556,31 @@ f:SetScript("OnEvent", function()
end)
```
### `model:SetItem(itemID [, appearanceModID, itemVisualID])`
Shows the model of one item: a weapon, a shield, a helm, a shoulder, a
quiver, or ammo. The item has its skin texture and its glow, if it has one.
`appearanceModID` has no effect.
`itemVisualID` is an `ItemVisuals` ID, for example the glow of a weapon
enchant. The model shows it only when the item has no glow of its own.
A helm model is made for one race and sex. The model shows the helm for the
race and sex of your character.
An item that has no model of its own, for example a chest item, shows
nothing. If the item is not in the client item cache, the model shows
nothing at first. The client requests the item, and the model shows it when
the data arrives.
The model shows the item until the next `SetItem`, `SetDisplayInfo`,
`SetCreature`, or `SetUnit` call. This is also true when you hide the frame
and show it again.
```lua
PreviewModel:SetItem(19019) -- Thunderfury
```
### `model:SetPortraitZoom(zoom)`
Moves the camera between the full-body view and the model's portrait view.
@@ -18624,7 +18650,8 @@ Returns the display ID (a `CreatureDisplayInfo` ID) that the model shows:
- 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.
Returns 0 when the model shows nothing or an item, or when its unit no
longer exists.
### `model:SetAnimation(animation [, variation])`
@@ -18635,7 +18662,8 @@ 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
`SetDisplayInfo`, `SetCreature`, `SetItem`, 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)`.
@@ -18692,12 +18720,12 @@ Returns the value that `SetDoBlend` set.
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.
the model again from its unit, display, or item 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
`SetItem`, or `DressUpModel:Dress()` while the frame is hidden, the frame loads the
new model when it shows, the same as with `false`.
```lua
+31
View File
@@ -4064,6 +4064,24 @@ enum Offsets {
// 0x004797B0), installs it (vtable +0x90), registers the done callback
// and arms Stand. No character compositor: 4.3.4's SetUpModel adds it.
FUN_PLAYERMODEL_ATTACH_DISPLAY_MODEL = 0x00505A70,
// The pane's animation-done callback, which AttachDisplayModel registers
// with the pane as user data (0x00505AFB) and which arms Stand when a
// whole-body track completes.
FUN_PLAYERMODEL_ON_ANIM_DONE = 0x005062A0,
// CM2Model::SetAnimDoneCallback — `__thiscall(model, callback, uint64
// userData)`, ret 0xC. AttachDisplayModel passes the pane sign-extended
// (`cdq` at 0x00505AF8).
FUN_M2MODEL_SET_ANIM_DONE_CALLBACK = 0x00711BB0,
// CM2Model::ReplaceTexture — `__thiscall(model, type, HTEXTURE)`, ret 8.
// Queues the bind when the model has not loaded yet (0x00710FFF). Item
// models take their skin as type 2 (0x00479933 in AttachItemModelFile,
// 0x0047A043 in the loose ammo model).
FUN_M2MODEL_REPLACE_TEXTURE = 0x00710EC0,
// The CStatus every item-model texture load reports to: AttachItemModelFile
// (0x00479912) and the loose ammo model (0x0047A01A) pass it to
// FUN_TEXTURE_LOAD_BY_PATH as `(path, status, flags, 1, 0)`, with flags
// from FUN_GX_TEXFLAGS_INIT(3, 0,0, 0,0,0, 1, 0).
VAR_ITEM_MODEL_TEXTURE_STATUS = 0x00B424F0,
// ConfigureCamera — `__thiscall(pane)`, run by the model-ready hook
// (0x00505B30): a frozen copy of the model's camera index 1, or a fixed
// camera at the bounding-box centre. 4.3.4's ConfigureCamera is this plus
@@ -5961,6 +5979,12 @@ enum Offsets {
// ItemVisuals ID (column 22). Read by AttachItemModel (0x0047A200) and
// GetItemDescVisual (0x0062EC93); 365 stock rows set it.
OFF_ITEMDISPLAYINFO_ITEM_VISUAL = 0x58,
// The two model names (+0x04, +0x08) and their two textures (+0x0C,
// +0x10), char* each. AttachItemModel (0x0047A0C0) builds a held item
// from name 0 and texture 0 (0x0047A192, 0x0047A1CF); the loose ammo
// model (0x00479F40) from name 1 and texture 1 (0x00479F58, 0x00479F64).
OFF_ITEMDISPLAYINFO_MODEL_NAME = 0x04,
OFF_ITEMDISPLAYINFO_MODEL_TEXTURE = 0x0C,
// ItemVisuals.dbc — an ID plus five ItemVisualEffects IDs (6 columns),
// the glow AttachItemVisuals hangs on a weapon. Instance 0x00C0DB68.
@@ -6091,6 +6115,13 @@ enum Offsets {
VAR_CHRRACES_COUNT = 0x00C0DEE4,
OFF_CHRRACES_FILENAME = 0x3C,
OFF_CHRRACES_NAMES = 0x44,
// The client prefix ("Hu", "Or", ...) the head-item path takes: the helm
// builder (0x004799A0) reads it at 0x00479A21 for
// `Item\ObjectComponents\Head\<name>_<prefix><sex letter>.mdx`.
OFF_CHRRACES_CLIENT_PREFIX = 0x18,
// The helm builder's sex letters, `const char *[3]` = "M", "F", "U",
// indexed by the sex byte (0x00479A1A).
VAR_SEX_LETTERS = 0x00838AB4,
// CreatureFamily.dbc — pet/beast family metadata. Standard 5-DWORD
// class shape (instance 0x00C0DE74; records 0x00C0DE7C, count
+21 -15
View File
@@ -54,6 +54,7 @@
#include "creature/Info.h"
#include "dbc/Lookup.h"
#include "model/DisplayInfo.h"
#include "model/ItemDisplay.h"
#include "tick/WorldTick.h"
#include <cstdint>
@@ -186,21 +187,8 @@ void StepJob(DressJob &job) {
return;
}
// The helm/shoulder feeds attach child model instances. The frame
// only lights its OWN instance (pre-render callback registered by
// SetModelInstance), so the children would render unlit — pure black.
// Give each child the frame's own light/fog callback; it reads the
// frame's live light struct, so later SetLight calls reach them too.
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, job.frame);
// The helm/shoulder feeds attach child model instances.
LightChildren(job.frame, instance);
job.configured = true;
}
@@ -224,12 +212,15 @@ AttachDisplayModel_t g_origAttach = nullptr;
// 4.3.4's SetUpModel order: build the model, then dress it when the display
// has an Extra record.
// An item record (SetItem) builds the item's model instead.
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);
if (Model::ItemDisplay::Attach(pane, record))
return;
g_origAttach(pane, record);
if (record == nullptr)
@@ -395,6 +386,21 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
return builder;
}
// The frame only lights its OWN instance (the pre-render callback
// SetModelInstance registers), so attached children would render unlit, pure
// 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);
}
bool PumpDresser(void *dresser) {
return reinterpret_cast<CharCompPump_t>(Offsets::FUN_CHARCOMP_PUMP)(dresser, nullptr) != 0;
}
+4
View File
@@ -27,6 +27,10 @@ void *CreateDresser(void *instance, const uint8_t *extraRec);
bool PumpDresser(void *dresser);
void DestroyDresser(void *dresser);
// Gives every model attached to `instance` the light and fog callback that
// `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.
+302
View File
@@ -0,0 +1,302 @@
// 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:SetItem(itemID [, appearanceModID, itemVisualID])`: the pane
// shows one item's own model, the model a unit holds or wears.
//
// A pane shows what its display record names (OFF_PLAYERMODEL_DISPLAY_RECORD).
// SetDisplay stores the record and AttachDisplayModel builds it, at once when
// the pane is visible and again on every show; the SetUnit helper clears it.
// An item is one more kind of record: SetItem passes SetDisplay a record of
// the pane's own, and Model::DisplayInfo's AttachDisplayModel co-hook hands
// that record to Attach below. So hide, show, SetUnit, SetDisplayInfo and
// SetKeepModelOnHide treat an item the way they treat a creature display.
//
// The model is 4.3.4's CGUnit_C::CreateItemModel, built with 1.12's own
// steps:
// - The directory comes from the inventory type: head, shoulder, shield,
// ammo, quiver, else weapon (the engine's strings at 0x00838CD8..).
// - Model name 0, else name 1, with the texture of the same index.
// - A head item is `<name less its extension>_<race prefix><sex
// letter>.mdx`, as the helm builder (0x004799A0) makes it. 4.3.4 takes
// race and sex from the unit that makes the model; a pane has no unit,
// so they are the player's.
// - Then AttachItemModelFile's (0x004798C0) steps: the model in the pane's
// scene, the `%s%s.blp` skin bound as texture type 2, and the item
// visuals.
// - Then AttachDisplayModel's (0x00505A70) install: SetModelInstance
// (vtable +0x90), the pane's done callback, Stand, and the release of the
// creation reference.
//
// The glow is the display's own ItemVisuals row, else itemVisualID: the rule
// GetItemDescVisual applies to enchants, which TryOn uses too.
// appearanceModID has no meaning without item appearance mods.
//
// An item that is not in the cache is queued with Item::Data::WarmCache, and
// the pane shows nothing until it loads. Then the pane is built again if it
// still shows that item and is visible.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "item/Data.h"
#include "item/Record.h"
#include "model/DisplayInfo.h"
#include "model/ItemDisplay.h"
#include "texture/Load.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <unordered_map>
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;
// AttachItemModelFile's texture filter.
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.
struct ItemRecord {
uint8_t display[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)];
uint32_t itemID;
uint32_t visual;
};
std::unordered_map<void *, ItemRecord> g_records;
const char *Directory(uint32_t inventoryType) {
switch (inventoryType) {
case kInvTypeHead:
return "Item\\ObjectComponents\\Head\\";
case kInvTypeShoulders:
return "Item\\ObjectComponents\\Shoulder\\";
case kInvTypeShield:
return "Item\\ObjectComponents\\Shield\\";
case kInvTypeAmmo:
return "Item\\ObjectComponents\\Ammo\\";
case kInvTypeQuiver:
return "Item\\ObjectComponents\\Quiver\\";
default:
return "Item\\ObjectComponents\\Weapon\\";
}
}
bool Empty(const char *s) {
return s == nullptr || s[0] == '\0';
}
const char *DisplayString(const uint8_t *display, int base, int index) {
return Game::Read<const char *>(display, base + index * static_cast<int>(sizeof(uint32_t)));
}
// The helm builder's path: the name cut at its last dot, then the player's
// race prefix and sex letter.
bool HeadPath(char *path, const char *dir, const char *name) {
const uint8_t sex = Game::Read<uint8_t>(Offsets::VAR_PLAYER_SEX_BYTE);
const uint8_t *race =
DBC::Record(Offsets::VAR_CHRRACES_RECORDS, Offsets::VAR_CHRRACES_COUNT,
Game::Read<uint8_t>(Offsets::VAR_PLAYER_RACE_BYTE));
if (race == nullptr || sex >= kSexLetters)
return false;
char base[kPathSize];
std::snprintf(base, sizeof(base), "%s", name);
if (char *dot = std::strrchr(base, '.'))
*dot = '\0';
std::snprintf(path, kPathSize, "%s%s_%s%s.mdx", dir, base,
Game::Read<const char *>(race, Offsets::OFF_CHRRACES_CLIENT_PREFIX),
Game::Read<const char *>(Offsets::VAR_SEX_LETTERS + sex * sizeof(uint32_t)));
return true;
}
// 4.3.4's CreateItemModel: a new model the caller owns one reference on, or
// null when the item is not cached or has no model.
void *CreateItemModel(void *pane, const ItemRecord &rec) {
const uint8_t *item = ::Item::PeekRecord(rec.itemID);
if (item == nullptr)
return nullptr;
const uint8_t *display =
DBC::Record(Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
Game::Read<uint32_t>(item, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID));
if (display == nullptr)
return nullptr;
int index = 0;
if (Empty(DisplayString(display, Offsets::OFF_ITEMDISPLAYINFO_MODEL_NAME, 0))) {
if (Empty(DisplayString(display, Offsets::OFF_ITEMDISPLAYINFO_MODEL_NAME, 1)))
return nullptr;
index = 1;
}
const char *name = DisplayString(display, Offsets::OFF_ITEMDISPLAYINFO_MODEL_NAME, index);
const uint32_t inventoryType = Game::Read<uint32_t>(item, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
const char *dir = Directory(inventoryType);
char path[kPathSize];
if (inventoryType == kInvTypeHead) {
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);
if (model == nullptr)
return nullptr;
const char *texture =
DisplayString(display, Offsets::OFF_ITEMDISPLAYINFO_MODEL_TEXTURE, index);
if (!Empty(texture)) {
std::snprintf(path, sizeof(path), "%s%s.blp", dir, texture);
void *handle = reinterpret_cast<TextureLoad_t>(Offsets::FUN_TEXTURE_LOAD_BY_PATH)(
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);
return nullptr;
}
reinterpret_cast<ReplaceTexture_t>(Offsets::FUN_M2MODEL_REPLACE_TEXTURE)(
model, kItemSkinTexture, handle);
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;
if (visual != 0) {
reinterpret_cast<AttachItemVisuals_t>(Offsets::FUN_ATTACH_ITEM_VISUALS)(model, visual);
Model::DisplayInfo::LightChildren(pane, model);
}
return model;
}
// AttachDisplayModel's install, with the item's model. A pane with no model
// 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);
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);
}
// A queued item has loaded: build each visible pane that still shows it,
// through AttachDisplayModel as a show would.
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)
reinterpret_cast<PaneRecordFn_t>(Offsets::FUN_PLAYERMODEL_ATTACH_DISPLAY_MODEL)(
pane, rec.display);
}
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);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: SetItem(itemID [, appearanceModID, itemVisualID])");
return 0;
}
ItemRecord &rec = g_records[pane];
rec = ItemRecord{};
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);
return 0;
}
const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetItem", &Script_SetItem},
};
const Game::Doc::Field kSetItemArgs[] = {
Game::Doc::Req("itemID", "number", "The item to show."),
Game::Doc::Opt("appearanceModID", "number", nullptr, "Has no effect."),
Game::Doc::Opt("itemVisualID", "number", nullptr,
"An ItemVisuals ID to show on the item when the item has no glow of its own."),
};
const Game::Doc::Function kSetItem{"Shows the model of an item on the model.", kSetItemArgs, {}};
const Game::Doc::Method g_methodDocs[] = {
{"SetItem", &kSetItem},
};
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};
// The reload tears every pane down; a later load must not walk them.
void PrepareForReload() {
g_records.clear();
}
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
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)
return false;
if (::Item::PeekRecord(it->second.itemID) == nullptr)
::Item::Data::WarmCache(it->second.itemID);
Show(pane, it->second);
return true;
}
} // namespace Model::ItemDisplay
+25
View File
@@ -0,0 +1,25 @@
// 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
#include <cstdint>
namespace Model::ItemDisplay {
// AttachDisplayModel's item step: when `record` is the item record SetItem
// gave `pane`, builds the item's model in the pane and returns true. Any
// other record returns false and is the engine's to build.
bool Attach(void *pane, const uint8_t *record);
} // namespace Model::ItemDisplay