mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
feat(model): add DressUpModel try-on hand, enchant glow, sheathing and auto-dress
TryOn now takes retail's (item [, handSlotName, spellEnchantID]) and returns an Enum.ItemTryOnReason. The pane gains IsGeoReady, SetSheathed / GetSheathed and SetAutoDress / GetAutoDress. Hand: TryOnItem (0x00504540) picks a lane at one site, 0x0050492D (mov edx,[edi+0x2C]; cmp edx,0x0D). A 6-byte call there jumps a named hand to the worker's own main-hand (0x0050495F) or off-hand (0x005049B1) path, so its CanHoldBoth (0x00504BC0) eviction, lane store and attach all still run; with no hand it replays the two instructions and the jnz reads their flags. The name is matched like GetInventorySlotInfo matches it: SStrCmpI over the PaperDollItemFrame rows, slot 16 or 17. Enchant glow: the player's AttachEnchantVisual step (0x005EED50) on the attachment AttachItemModel (0x0047A0C0) returns: FindChild (0x00712F00), then AttachItemVisuals (0x00479700) with SpellItemEnchantment +0x58, skipped when the display's own ItemVisuals row exists (GetItemDescVisual's rule, 0x0062EC70). The enchant is the argument, else the link's enchant field, which Item::Arg now parses. Sheathing re-runs the unit's AttachHandItem step on the pane's lanes with the sheathed flag. The lanes keep no display row, so the pane records one: the unit's vtable +0x9C row when SetModelInstance (slot 0x00808438) clones it, then whatever the worker's attach (0x00504AE6) installs; its two evictions (0x00504A0C, 0x00504A55) drop theirs. Each lane keeps its glow too, a player's read through GetVisibleItem + GetItemDescVisual, so a re-hung weapon keeps it. Auto-dress off runs ClearModel (0x00504490) after SetModelInstance unless Dress() is the one rebuilding. TryOn's result reads the gates the worker passes: an uncached record is DataPending, a held type installs, armor needs the component and the ready byte (+0x40C). DressUpItemLink now passes the whole link to DressUpItem, so a Control-click shows the link's enchant, and skips a cached item that IsDressableItem rejects. DressUpItem stays, since Blizzard_AuctionUI and Turtle both replace it.
This commit is contained in:
@@ -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), `DressUpModel:UndressSlot`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [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` |
|
||||
|
||||
+95
-4
@@ -786,7 +786,12 @@ build instructions.
|
||||
- [`model:RefreshCamera()`](#modelrefreshcamera)
|
||||
- [`model:SetRotation(rotation [, animate])`](#modelsetrotationrotation--animate)
|
||||
- [`DressUpModel:UndressSlot(slot)`](#dressupmodelundressslotslot)
|
||||
- [`DressUpModel:TryOn(item)`](#dressupmodeltryonitem)
|
||||
- [`DressUpModel:TryOn(item [, handSlotName, spellEnchantID])`](#dressupmodeltryonitem--handslotname-spellenchantid)
|
||||
- [`DressUpModel:IsGeoReady()`](#dressupmodelisgeoready)
|
||||
- [`DressUpModel:SetSheathed([sheathed, hideWeapons])`](#dressupmodelsetsheathedsheathed-hideweapons)
|
||||
- [`DressUpModel:GetSheathed()`](#dressupmodelgetsheathed)
|
||||
- [`DressUpModel:SetAutoDress([enabled])`](#dressupmodelsetautodressenabled)
|
||||
- [`DressUpModel:GetAutoDress()`](#dressupmodelgetautodress)
|
||||
- [`Model:SetGlow(value)`](#modelsetglowvalue)
|
||||
|
||||
- [ChatBubbles](#chatbubbles)
|
||||
@@ -18594,7 +18599,7 @@ inventory slot number: 1 head, 3 shoulder, 4 shirt, 5 chest, 6 waist,
|
||||
DressUpModel:UndressSlot(1) -- remove the helm
|
||||
```
|
||||
|
||||
### `DressUpModel:TryOn(item)`
|
||||
### `DressUpModel:TryOn(item [, handSlotName, spellEnchantID])`
|
||||
|
||||
Puts an item on a `DressUpModel`. `item` is one of these:
|
||||
|
||||
@@ -18609,12 +18614,98 @@ the item that the recipe makes. Thus Control-click on a pattern opens the
|
||||
dressing room with the item that the pattern makes.
|
||||
|
||||
If `item` is none of these, the game shows the error "That item can't be
|
||||
equipped". The model chooses the hand for a weapon itself.
|
||||
equipped".
|
||||
|
||||
`handSlotName` sets the hand that holds a weapon, a shield, or an off-hand
|
||||
item. Give `"MAINHANDSLOT"` or `"SECONDARYHANDSLOT"`. Upper and lower case
|
||||
are the same. If the item in the other hand cannot be held with the new
|
||||
item, the model removes it. Armor ignores `handSlotName`.
|
||||
|
||||
If `handSlotName` is `nil` or another name, the model chooses the hand. A
|
||||
one-hand weapon goes into the off hand only when the main hand holds a
|
||||
one-hand weapon and the unit can dual wield.
|
||||
|
||||
`spellEnchantID` is an enchant ID from `SpellItemEnchantment.dbc`, for
|
||||
example 1900 (Crusader). The weapon shows the glow of that enchant. If
|
||||
`spellEnchantID` is `nil`, the weapon shows the glow of the enchant in the
|
||||
item link. An item that has its own glow keeps its own glow.
|
||||
|
||||
If the item is not in the item cache, the model wears it when the data
|
||||
arrives. But then the model chooses the hand, and the weapon shows no
|
||||
enchant glow.
|
||||
|
||||
`TryOn` returns an `Enum.ItemTryOnReason` value:
|
||||
|
||||
- `Success` (0): the model wears the item.
|
||||
- `NotEquippable` (2): the model cannot wear the item. Necklaces, rings,
|
||||
trinkets, and items that are not equipment give this result. Armor gives
|
||||
this result when the model shows a shapeshifted unit or a creature.
|
||||
- `DataPending` (3): the item is not in the item cache. The game requests
|
||||
the item. When the data arrives, the model wears the item.
|
||||
|
||||
`WrongRace` (1) is in the enum, but `TryOn` does not return it.
|
||||
|
||||
```lua
|
||||
DressUpModel:TryOn("|cff1eff00|Hitem:2077:0:0:0|h[Magician Staff]|h|r")
|
||||
local result = DressUpModel:TryOn("|cff1eff00|Hitem:2077:0:0:0|h[Magician Staff]|h|r")
|
||||
if result == Enum.ItemTryOnReason.DataPending then
|
||||
print("The item loads now.")
|
||||
end
|
||||
```
|
||||
|
||||
### `DressUpModel:IsGeoReady()`
|
||||
|
||||
Returns `true` when the model file and all item models on it have
|
||||
loaded. Otherwise it returns `false`. A model that the frame does not show
|
||||
also gives `false`.
|
||||
|
||||
### `DressUpModel:SetSheathed([sheathed, hideWeapons])`
|
||||
|
||||
Moves the main-hand and off-hand weapons of the model. If `sheathed` is
|
||||
`true`, the weapons go to their sheath positions. If `sheathed` is `false`
|
||||
or `nil`, the weapons go into the hands. If `hideWeapons` is `true`, the
|
||||
model shows no main-hand or off-hand weapon.
|
||||
|
||||
The setting stays on the frame. It applies to weapons that `TryOn` adds
|
||||
later. It also applies when the model is built again: when you call
|
||||
`SetUnit` or `Dress`, or when the frame shows again. `SetSheathed` does not
|
||||
move a ranged weapon. The weapons keep their enchant glow.
|
||||
|
||||
Before the first `SetSheathed` call, the model holds its weapons the same
|
||||
way as its unit. Weapons that `TryOn` adds then go into the hands.
|
||||
|
||||
```lua
|
||||
DressUpModel:SetSheathed(true) -- weapons on the back and hips
|
||||
DressUpModel:SetSheathed(false, true) -- no weapons
|
||||
DressUpModel:SetSheathed() -- weapons in the hands
|
||||
```
|
||||
|
||||
### `DressUpModel:GetSheathed()`
|
||||
|
||||
Returns the `sheathed` value of the last `SetSheathed` call on the frame.
|
||||
Before the first call, it returns `false`.
|
||||
|
||||
### `DressUpModel:SetAutoDress([enabled])`
|
||||
|
||||
Sets whether the model wears the equipment of its unit. If `enabled` is
|
||||
`true`, `SetUnit` shows the unit with its equipment. If `enabled` is
|
||||
`false` or `nil`, `SetUnit` shows the unit with no equipment. The default
|
||||
is `true`.
|
||||
|
||||
The setting applies the next time that the model is built: when you call
|
||||
`SetUnit`, or when the frame shows again. `Dress()` always dresses the model
|
||||
in the equipment of its unit.
|
||||
|
||||
```lua
|
||||
DressUpModel:SetAutoDress(false)
|
||||
DressUpModel:SetUnit("player") -- no equipment
|
||||
DressUpModel:TryOn(2077) -- only the staff
|
||||
```
|
||||
|
||||
### `DressUpModel:GetAutoDress()`
|
||||
|
||||
Returns the value of the last `SetAutoDress` call on the frame. Before the
|
||||
first call, it returns `true`.
|
||||
|
||||
### `Model:SetGlow(value)`
|
||||
|
||||
This method exists on every `Model` frame, for code from later clients. It
|
||||
|
||||
@@ -4047,6 +4047,8 @@ enum Offsets {
|
||||
// write it; the show hook (0x00505D49) reads it to rebuild the model.
|
||||
// Those four are the only accesses in 0x00500000..0x00507000.
|
||||
OFF_PLAYERMODEL_DISPLAY_RECORD = 0x3F0,
|
||||
// The pane's unit GUID (u64), stored by the SetUnit helper (0x00505B50).
|
||||
OFF_PLAYERMODEL_UNIT_GUID = 0x3E0,
|
||||
// SetDisplay — `__thiscall(pane, record)`: clears the unit GUID, stores
|
||||
// the record and, if the frame is visible, builds the model.
|
||||
FUN_PLAYERMODEL_SET_DISPLAY = 0x00505CB0,
|
||||
@@ -4069,11 +4071,63 @@ enum Offsets {
|
||||
OFF_DRESSUPMODEL_COMPONENT = 0x3F8,
|
||||
OFF_DRESSUPMODEL_MAIN_HAND_LANE = 0x3FC,
|
||||
OFF_DRESSUPMODEL_OFF_HAND_LANE = 0x404,
|
||||
// Byte: the unit showed its native display when the pane was built (not
|
||||
// shapeshifted). TryOnItem's armor arms need it and the component.
|
||||
OFF_DRESSUPMODEL_READY = 0x40C,
|
||||
// SetModelInstance — `__thiscall(pane, model)`, DressUpModel vtable slot
|
||||
// 36 (0x008083A8 + 0x90; its only reference): sets the model, clones the
|
||||
// unit's component and copies the unit's hand lanes. Null on hide.
|
||||
FUN_DRESSUPMODEL_SET_MODEL_INSTANCE = 0x00504350,
|
||||
SLOT_DRESSUPMODEL_SET_MODEL_INSTANCE = 0x00808438,
|
||||
// ClearModel — `__thiscall(pane)`: Undress's worker.
|
||||
FUN_DRESSUPMODEL_CLEAR_MODEL = 0x00504490,
|
||||
// The engine's Lua DressUpModel:Dress() — `int __fastcall(L)`: re-runs
|
||||
// the SetUnit helper (0x00505B50) with the pane's GUID.
|
||||
FUN_SCRIPT_DRESSUPMODEL_DRESS = 0x00504CD0,
|
||||
// AttachItemModel — `int __fastcall(model, displayRecord, bodySlot,
|
||||
// sheatheType, bool sheathed, bool shield, bool ranged)`, ret 0x14: hangs
|
||||
// a held item at the hand, or at its sheath point when `sheathed`. The
|
||||
// unit's own AttachHandItem (0x0060B8C0) passes its sheath state there.
|
||||
FUN_ATTACH_ITEM_MODEL = 0x0047A0C0,
|
||||
// TryOnItem's held arm: the install (`push 0` as sheathed) and the two
|
||||
// evictions of the other lane, each a `call` to the function above or
|
||||
// FUN_DETACH_HELD_ITEM_SLOT. The third detach (0x00504A85) is the
|
||||
// target lane's own occupant.
|
||||
CALL_TRY_ON_ATTACH_ITEM = 0x00504AE6,
|
||||
CALL_TRY_ON_EVICT_OFF_HAND = 0x00504A0C,
|
||||
CALL_TRY_ON_EVICT_MAIN_HAND = 0x00504A55,
|
||||
// TryOnItem's hand choice, reached only from the held arm (jump-table
|
||||
// entry 0x0A: inventory types 13-15, 17, 21-23, 25, 26). The site is
|
||||
// `mov edx, [edi+0x2C]; cmp edx, 0x0D` (8B 57 2C 83 FA 0D) before the
|
||||
// `jnz`. Both paths start with ECX = the player (or null), ESI = the
|
||||
// pane, EDI = the item record, and set [ebp-4] to body slot 0xF / 0x10
|
||||
// before the CanHoldBoth (0x00504BC0) eviction of the other lane.
|
||||
SITE_TRY_ON_HAND_CHOICE = 0x0050492D,
|
||||
TRY_ON_MAIN_HAND_PATH = 0x0050495F,
|
||||
TRY_ON_OFF_HAND_PATH = 0x005049B1,
|
||||
// AttachItemVisuals — `__fastcall(model, itemVisualID)`: detaches the
|
||||
// model's children, then hangs each ItemVisualEffects model of the
|
||||
// ItemVisuals row (attachments 0-4). Ignores an id with no row.
|
||||
FUN_ATTACH_ITEM_VISUALS = 0x00479700,
|
||||
// CM2Model::FindChild — `__thiscall(model, attachId)`, ret 4: the child
|
||||
// model hung at that attachment, or null.
|
||||
FUN_M2MODEL_FIND_CHILD = 0x00712F00,
|
||||
// GetItemDescVisual — `int __thiscall(visibleItem)`: -1 when the item's
|
||||
// display has its own ItemVisuals row (AttachItemModel already hung it),
|
||||
// else the visual of the first of its 7 enchants (+0x0C) that has one,
|
||||
// else 0. CGPlayer_C::AttachEnchantVisual (0x005EED50) gives the result
|
||||
// to FindChild + AttachItemVisuals on the attachment AttachItemModel
|
||||
// used; AttachHandItem (0x0060B8C0) runs that for players only.
|
||||
FUN_VISIBLE_ITEM_GET_DESC_VISUAL = 0x0062EC70,
|
||||
// UnequipCharacterSlot — `__thiscall(component, slot 0..11)`.
|
||||
FUN_CHARCOMP_UNEQUIP_SLOT = 0x00478960,
|
||||
// DetachHeldItemSlot — `__fastcall(model, bodySlot 0xF/0x10,
|
||||
// sheatheType, bool shield)`; Undress passes `invType == 14` as shield.
|
||||
FUN_DETACH_HELD_ITEM_SLOT = 0x0047A310,
|
||||
// Unit vtable slot 39 — `const void *__thiscall(unit, hand 0/1/2)`: the
|
||||
// ItemDisplayInfo row of the hand's item (CGUnit_C 0x00605EE0, CGPlayer_C
|
||||
// 0x005EC350); AttachHandItem's record source.
|
||||
VTBL_UNIT_GET_HELD_ITEM_DISPLAY = 0x9C,
|
||||
// TryOnItem — `__thiscall(pane, itemID)`: the worker behind TryOn.
|
||||
FUN_DRESSUPMODEL_TRY_ON_ITEM = 0x00504540,
|
||||
// The worker's item-cache callback (0x00504BA0) re-runs it once the
|
||||
@@ -5861,6 +5915,14 @@ enum Offsets {
|
||||
VAR_ITEMDISPLAYINFO_RECORDS = 0x00C0DC10,
|
||||
VAR_ITEMDISPLAYINFO_COUNT = 0x00C0DC14,
|
||||
OFF_ITEMDISPLAYINFO_ICON = 0x14,
|
||||
// ItemVisuals ID (column 22). Read by AttachItemModel (0x0047A200) and
|
||||
// GetItemDescVisual (0x0062EC93); 365 stock rows set it.
|
||||
OFF_ITEMDISPLAYINFO_ITEM_VISUAL = 0x58,
|
||||
|
||||
// ItemVisuals.dbc — an ID plus five ItemVisualEffects IDs (6 columns),
|
||||
// the glow AttachItemVisuals hangs on a weapon. Instance 0x00C0DB68.
|
||||
VAR_ITEMVISUALS_RECORDS = 0x00C0DB70,
|
||||
VAR_ITEMVISUALS_COUNT = 0x00C0DB74,
|
||||
|
||||
// ItemSet.dbc — armor/equipment set definitions referenced by
|
||||
// `ItemStats_C::m_itemSet`. Standard 5-DWORD class shape at
|
||||
@@ -8075,6 +8137,9 @@ enum Offsets {
|
||||
OFF_SPELLITEMENCHANT_AMOUNT = 0x10, // int Amount[3] (EffectPointsMin)
|
||||
OFF_SPELLITEMENCHANT_ARG = 0x28, // int EffectArg[3]
|
||||
OFF_SPELLITEMENCHANT_NAME = 0x34, // char *name[8], locale-indexed
|
||||
// ItemVisuals ID (column 22), the weapon glow; read by GetItemDescVisual
|
||||
// (0x0062ECD7). Crusader 1900 = 103, Fiery 803 = 25, Icy 1894 = 126.
|
||||
OFF_SPELLITEMENCHANT_ITEM_VISUAL = 0x58,
|
||||
|
||||
// ItemRandomProperties.dbc — the table a random-suffix ID (the third
|
||||
// field of an `item:id:enchant:suffix:unique` link) indexes to get
|
||||
|
||||
@@ -143,6 +143,7 @@ FrameXML\ColorSwatch.lua
|
||||
FrameXML\ColorSwatch.xml
|
||||
FrameXML\ScriptErrorsFrame.lua
|
||||
FrameXML\ScriptErrorsFrame.xml
|
||||
FrameXML\DressUpFrame.lua
|
||||
|
||||
Menu\MenuConstants.lua
|
||||
Menu\Classic\MenuConstants.lua
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 4.3.4's DressUpItemLink (FrameXML/ModelFrames.lua): the whole link goes to
|
||||
-- the model, so DressUpModel:TryOn shows the link's enchant glow, and an item
|
||||
-- the model cannot wear does not open the window. 1.12.1's version passes
|
||||
-- only the item ID.
|
||||
--
|
||||
-- Differences from the 4.3.4 source:
|
||||
-- - The link goes through DressUpItem, which 4.3.4 folded in. Here
|
||||
-- Blizzard_AuctionUI replaces DressUpItem to show its own model, and
|
||||
-- some clients add steps to it (Turtle dresses the tabard).
|
||||
-- - An uncached item skips the IsDressableItem test, which reads only the
|
||||
-- cache. 1.12.1 opens no tooltip before a chat link's Control-click, so
|
||||
-- the test would drop most chat links; TryOn loads the item instead.
|
||||
|
||||
function DressUpItemLink(link)
|
||||
if ( not link or (C_Item.IsItemDataCachedByID(link) and not IsDressableItem(link)) ) then
|
||||
return;
|
||||
end
|
||||
DressUpItem(link);
|
||||
end
|
||||
+8
-4
@@ -40,12 +40,16 @@ Resolved ResolveString(const char *s) {
|
||||
if (const char *m = std::strstr(s, "item:")) {
|
||||
m += 5;
|
||||
out.itemID = std::atoi(m);
|
||||
// Random suffix is the 3rd colon-separated field: itemID:enchant:suffix.
|
||||
// Walk to the 2nd ':' (stopping at the link boundary), read the number
|
||||
// after it. Bare "item:N" (no colons) leaves suffix 0.
|
||||
// The enchant and random suffix are the 2nd and 3rd colon-separated
|
||||
// fields: itemID:enchant:suffix. Walk the colons (stopping at the link
|
||||
// boundary) and read the number after each. Bare "item:N" leaves both 0.
|
||||
int colons = 0;
|
||||
for (const char *p = m; *p != '\0' && *p != '|'; ++p) {
|
||||
if (*p == ':' && ++colons == 2) {
|
||||
if (*p != ':')
|
||||
continue;
|
||||
if (++colons == 1) {
|
||||
out.enchant = std::atoi(p + 1);
|
||||
} else {
|
||||
out.suffix = std::atoi(p + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ struct Resolved {
|
||||
int itemID;
|
||||
int suffix; // random-property / suffix ID (3rd `item:` field), 0 if none
|
||||
const char *name;
|
||||
int enchant; // permanent enchant ID (2nd `item:` field), 0 if none
|
||||
};
|
||||
|
||||
// Resolves a Lua arg at 1-based stack `idx` to an item reference.
|
||||
|
||||
+434
-16
@@ -11,8 +11,9 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `DressUpModel:UndressSlot(slot)` and the 4.3.4 `DressUpModel:TryOn(item)`
|
||||
// (DressUpModelFrame_Scripts.cpp).
|
||||
// `DressUpModel:UndressSlot(slot)`, the 4.3.4 `DressUpModel:TryOn(item)`
|
||||
// (DressUpModelFrame_Scripts.cpp), and retail's TryOn result, IsGeoReady,
|
||||
// SetSheathed / GetSheathed and SetAutoDress / GetAutoDress.
|
||||
//
|
||||
// UndressSlot is one step of vanilla's own Undress (DressUpModel::ClearModel
|
||||
// 0x00504490), which 4.3.4 split per slot: a component slot is cleared with
|
||||
@@ -23,15 +24,45 @@
|
||||
// takes 4.3.4's argument forms in front of it: an item ID, an item link or
|
||||
// `item:` string, a trade-skill `enchant:` link (the item its recipe makes),
|
||||
// or the name of an item the player carries. Anything else shows
|
||||
// ERR_NOT_EQUIPPABLE, as 4.3.4 does. 4.3.4's optional hand argument
|
||||
// ("mainhand" / "offhand") is not supported: vanilla's worker picks the hand
|
||||
// itself.
|
||||
// ERR_NOT_EQUIPPABLE, as 4.3.4 does.
|
||||
//
|
||||
// Retail's hand argument is an inventory slot name, matched the way
|
||||
// GetInventorySlotInfo matches it (SStrCmpI over the PaperDollItemFrame
|
||||
// rows): MainHandSlot or SecondaryHandSlot. The worker picks a hand at one
|
||||
// site (SITE_TRY_ON_HAND_CHOICE); a named hand sends a held item down that
|
||||
// hand's path, so the worker's own eviction of an incompatible other lane
|
||||
// runs, as 4.3.4's does for a forced hand.
|
||||
//
|
||||
// The enchant glow is the player's AttachEnchantVisual step on the item the
|
||||
// worker hangs: FindChild at the attachment AttachItemModel used, then
|
||||
// AttachItemVisuals with the enchant's ItemVisuals row, unless the item's
|
||||
// display hangs its own (GetItemDescVisual's rule, and 4.3.4's
|
||||
// `enchants && !visuals`). The enchant is the spellEnchantID argument, else
|
||||
// the link's enchant field, as 4.3.4 passes it. The worker's cache callback
|
||||
// re-runs with neither hand nor enchant, as 4.3.4's does.
|
||||
//
|
||||
// 4.3.4's worker (`TryOnWithVisual`, 0x00A05010) tries on what a recipe makes:
|
||||
// an item with no inventory type whose spell teaches a CREATE_ITEM spell is
|
||||
// swapped for the made item. Vanilla's worker shows ERR_NOT_EQUIPPABLE
|
||||
// instead. `TryOnItem` adds the swap in front of the worker, for our TryOn
|
||||
// and for the worker's cache-load callback, so an uncached pattern works too.
|
||||
//
|
||||
// TryOn's result (retail's ItemTryOnReason) is read from the same gates the
|
||||
// worker passes: an uncached record is DataPending (the worker has queued the
|
||||
// cache callback), a held type is installed, and an armor type needs the
|
||||
// pane's component and ready byte (a shapeshifted unit has neither path).
|
||||
//
|
||||
// Sheathing is the unit's own AttachHandItem step on the pane's lanes: detach
|
||||
// the lane, then AttachItemModel with the sheathed flag. The lanes keep no
|
||||
// display row, so the pane keeps one per lane: the unit's slot-39 row when the
|
||||
// pane is built, then whatever TryOnItem installs. Until SetSheathed is called
|
||||
// the pane keeps vanilla's placement (the unit's own, then the hand for every
|
||||
// try-on). Each lane keeps its glow too, so a re-hung weapon gets it back: a
|
||||
// player's from GetItemDescVisual on its visible item (only players glow in
|
||||
// vanilla), a try-on's from its enchant.
|
||||
//
|
||||
// Auto-dress off undresses the pane with ClearModel right after
|
||||
// SetModelInstance clones the unit, unless Dress() is the one rebuilding it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -42,11 +73,13 @@
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Model::DressUp {
|
||||
namespace {
|
||||
@@ -54,18 +87,212 @@ namespace {
|
||||
using UnequipSlot_t = void(__thiscall *)(void *component, uint32_t slot);
|
||||
using DetachHeldItem_t = void(__fastcall *)(void *model, uint32_t bodySlot, uint32_t sheatheType,
|
||||
bool shield);
|
||||
using AttachItemModel_t = int(__fastcall *)(void *model, const void *record, uint32_t bodySlot,
|
||||
uint32_t sheatheType, bool sheathed, bool shield,
|
||||
bool ranged);
|
||||
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, ...);
|
||||
|
||||
// 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;
|
||||
constexpr int kTryOnWrongRace = 1;
|
||||
constexpr int kTryOnNotEquippable = 2;
|
||||
constexpr int kTryOnDataPending = 3;
|
||||
|
||||
struct Lane {
|
||||
uintptr_t offset;
|
||||
uint32_t bodySlot;
|
||||
};
|
||||
constexpr Lane kLanes[2] = {
|
||||
{Offsets::OFF_DRESSUPMODEL_MAIN_HAND_LANE, kBodySlotMainHand},
|
||||
{Offsets::OFF_DRESSUPMODEL_OFF_HAND_LANE, kBodySlotOffHand},
|
||||
};
|
||||
|
||||
struct PaneState {
|
||||
bool autoDress = true;
|
||||
bool sheathSet = false; // SetSheathed was called; weapons follow the two below
|
||||
bool sheathed = false;
|
||||
bool hideWeapons = false;
|
||||
const void *laneDisplay[2] = {}; // ItemDisplayInfo row on each lane, or null
|
||||
uint32_t laneVisual[2] = {}; // enchant ItemVisuals row on each lane, or 0
|
||||
};
|
||||
std::unordered_map<void *, PaneState> g_panes;
|
||||
|
||||
void *g_tryOnPane = nullptr; // the pane whose TryOnItem worker is running
|
||||
uint32_t g_tryOnBodySlot = 0; // the hand TryOn named (kBodySlot*), or 0
|
||||
uint32_t g_tryOnEnchant = 0; // the SpellItemEnchantment TryOn gave, or 0
|
||||
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);
|
||||
}
|
||||
|
||||
int LaneIndex(uint32_t bodySlot) {
|
||||
return bodySlot == kBodySlotOffHand ? 1 : 0;
|
||||
}
|
||||
|
||||
// The glow `enchantID` gives an item shown with `display`: none when the
|
||||
// display has its own ItemVisuals row (GetItemDescVisual 0x0062EC70).
|
||||
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)
|
||||
return 0;
|
||||
const uint8_t *enchant = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
return enchant != nullptr
|
||||
? Game::Read<uint32_t>(enchant, Offsets::OFF_SPELLITEMENCHANT_ITEM_VISUAL)
|
||||
: 0;
|
||||
}
|
||||
|
||||
// AttachEnchantVisual's step (0x005EED50): the glow on the item hung at
|
||||
// `attachId`.
|
||||
void HangVisual(void *model, int attachId, uint32_t visual) {
|
||||
if (visual == 0 || attachId < 0)
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
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)
|
||||
continue;
|
||||
const bool shield = lane[3] == kInvTypeShield;
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
|
||||
model, kLanes[i].bodySlot, lane[4], shield);
|
||||
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);
|
||||
HangVisual(model, attachId, s.laneVisual[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Slot 36, after the original has cloned the unit and copied its lanes.
|
||||
void __fastcall SetModelInstance(void *pane, void * /*edx*/, void *model) {
|
||||
reinterpret_cast<SetModelInstance_t>(Offsets::FUN_DRESSUPMODEL_SET_MODEL_INSTANCE)(pane,
|
||||
model);
|
||||
if (model == nullptr)
|
||||
return;
|
||||
PaneState &s = g_panes[pane];
|
||||
s.laneDisplay[0] = s.laneDisplay[1] = nullptr;
|
||||
s.laneVisual[0] = s.laneVisual[1] = 0;
|
||||
// The original filled the lanes from this unit, resolved the same way.
|
||||
const uint64_t guid = Game::Read<uint64_t>(pane, Offsets::OFF_PLAYERMODEL_UNIT_GUID);
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid)) {
|
||||
const auto get = Game::Read<GetHeldItemDisplay_t>(
|
||||
Game::Read<const void *>(unit, 0), Offsets::VTBL_UNIT_GET_HELD_ITEM_DISPLAY);
|
||||
s.laneDisplay[0] = get(unit, 0);
|
||||
s.laneDisplay[1] = get(unit, 1);
|
||||
}
|
||||
// AttachHandEnchant's glow (0x005EED00), for players only: the visible
|
||||
// item in the lane's slot (0xF / 0x10, the same numbers as the body
|
||||
// slots), and its visual unless the display hangs its own (-1).
|
||||
if (void *player = Object::ByGuid(Offsets::TYPEMASK_PLAYER, guid)) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const void *item = reinterpret_cast<GetVisibleItem_t>(
|
||||
Offsets::FUN_UNIT_GET_VISIBLE_ITEM)(player, kLanes[i].bodySlot);
|
||||
if (item == nullptr || Game::Read<uint32_t>(item, Offsets::OFF_VISIBLE_ITEM_ITEM_ID) == 0)
|
||||
continue;
|
||||
const int visual =
|
||||
reinterpret_cast<GetDescVisual_t>(Offsets::FUN_VISIBLE_ITEM_GET_DESC_VISUAL)(item);
|
||||
s.laneVisual[i] = visual > 0 ? static_cast<uint32_t>(visual) : 0;
|
||||
}
|
||||
}
|
||||
if (!s.autoDress && !g_dressing) {
|
||||
reinterpret_cast<ClearModel_t>(Offsets::FUN_DRESSUPMODEL_CLEAR_MODEL)(pane);
|
||||
return;
|
||||
}
|
||||
if (s.sheathSet)
|
||||
ApplyWeapons(pane, s);
|
||||
}
|
||||
|
||||
// TryOnItem's install: record the lane's row and glow, honor SetSheathed,
|
||||
// then hang the glow.
|
||||
int __fastcall TryOnAttach(void *model, const void *record, uint32_t bodySlot,
|
||||
uint32_t sheatheType, bool sheathed, bool shield, bool ranged) {
|
||||
const auto attach = reinterpret_cast<AttachItemModel_t>(Offsets::FUN_ATTACH_ITEM_MODEL);
|
||||
if (g_tryOnPane == nullptr)
|
||||
return attach(model, record, bodySlot, sheatheType, sheathed, shield, ranged);
|
||||
PaneState &s = g_panes[g_tryOnPane];
|
||||
const int lane = LaneIndex(bodySlot);
|
||||
s.laneDisplay[lane] = record;
|
||||
s.laneVisual[lane] = EnchantVisual(record, g_tryOnEnchant);
|
||||
if (s.sheathSet) {
|
||||
if (s.hideWeapons)
|
||||
return -1;
|
||||
sheathed = s.sheathed;
|
||||
}
|
||||
const int attachId = attach(model, record, bodySlot, sheatheType, sheathed, shield, ranged);
|
||||
HangVisual(model, attachId, s.laneVisual[lane]);
|
||||
return attachId;
|
||||
}
|
||||
|
||||
// TryOnItem's eviction of the other lane. The worker leaves that lane's
|
||||
// record in place, so the pane drops its row to keep SetSheathed from
|
||||
// hanging the evicted item again.
|
||||
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;
|
||||
}
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(model, bodySlot,
|
||||
sheatheType, shield);
|
||||
}
|
||||
|
||||
// TryOnItem's hand choice. With a hand named, the worker takes that hand's
|
||||
// path; otherwise the thunk does the instructions it replaced and the
|
||||
// worker's `jnz` reads their flags.
|
||||
__declspec(naked) void HandChoiceThunk() {
|
||||
__asm {
|
||||
cmp dword ptr [g_tryOnBodySlot], 0Fh
|
||||
je mainHand
|
||||
cmp dword ptr [g_tryOnBodySlot], 10h
|
||||
je offHand
|
||||
mov edx, dword ptr [edi + 2Ch]
|
||||
cmp edx, 0Dh
|
||||
ret
|
||||
mainHand:
|
||||
add esp, 4
|
||||
jmp dword ptr [g_mainHandPath]
|
||||
offHand:
|
||||
add esp, 4
|
||||
jmp dword ptr [g_offHandPath]
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -139,23 +366,80 @@ uint32_t RecipeItem(const char *link) {
|
||||
return Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_EFFECT_ITEM_TYPE);
|
||||
}
|
||||
|
||||
// `__thiscall(pane, itemID)` as `__fastcall` with the EDX slot unused, so it
|
||||
// can stand at the worker's call sites.
|
||||
void __fastcall TryOnItem(void *pane, void *, uint32_t itemID) {
|
||||
// The item the worker is given: a cached recipe item becomes what it makes.
|
||||
uint32_t WornItem(uint32_t itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record != nullptr &&
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE) == 0) {
|
||||
const uint32_t made = Item::Dressable::RecipeItem(record);
|
||||
if (made != 0)
|
||||
itemID = made;
|
||||
return made;
|
||||
}
|
||||
return itemID;
|
||||
}
|
||||
|
||||
// `__thiscall(pane, itemID)` as `__fastcall` with the EDX slot unused, so it
|
||||
// can stand at the worker's call sites.
|
||||
void __fastcall TryOnItem(void *pane, void *, uint32_t itemID) {
|
||||
g_tryOnPane = pane;
|
||||
reinterpret_cast<TryOnItem_t>(Offsets::FUN_DRESSUPMODEL_TRY_ON_ITEM)(pane, WornItem(itemID));
|
||||
g_tryOnPane = nullptr;
|
||||
}
|
||||
|
||||
// The lane a hand name picks: the PaperDollItemFrame row it names
|
||||
// (GetInventorySlotInfo's match), if that row is a held slot; else 0.
|
||||
uint32_t NamedHand(const char *name) {
|
||||
const auto *records = Game::Read<const uint8_t *>(Offsets::VAR_PAPERDOLLITEMFRAME_RECORDS);
|
||||
const int count = Game::Read<int>(Offsets::VAR_PAPERDOLLITEMFRAME_COUNT);
|
||||
if (records == nullptr)
|
||||
return 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *row = records + i * Offsets::OFF_PAPERDOLLITEMFRAME_RECORD_STRIDE;
|
||||
if (Game::StrCmpI(Game::Read<const char *>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NAME),
|
||||
name) != 0)
|
||||
continue;
|
||||
switch (Game::Read<int>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER)) {
|
||||
case 16: return kBodySlotMainHand;
|
||||
case 17: return kBodySlotOffHand;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// What the worker did with `itemID`, from the gates it passes (0x00504540).
|
||||
int TryOnResult(void *pane, uint32_t itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return kTryOnDataPending;
|
||||
if (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID) == 0 ||
|
||||
PaneModel(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:
|
||||
// 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:
|
||||
return kTryOnSuccess; // the held arm
|
||||
default:
|
||||
return kTryOnNotEquippable;
|
||||
}
|
||||
reinterpret_cast<TryOnItem_t>(Offsets::FUN_DRESSUPMODEL_TRY_ON_ITEM)(pane, itemID);
|
||||
}
|
||||
|
||||
int __fastcall Script_TryOn(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
// Read before the name search below, which stomps the Lua stack.
|
||||
const uint32_t bodySlot = Game::Lua::Type(L, 3) == Game::Lua::TYPE_STRING
|
||||
? NamedHand(Game::Lua::ToString(L, 3))
|
||||
: 0;
|
||||
const bool enchantGiven = Game::Lua::IsNumber(L, 4) != 0;
|
||||
uint32_t enchant = enchantGiven ? static_cast<uint32_t>(Game::Lua::ToNumber(L, 4)) : 0;
|
||||
uint32_t itemID = 0;
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_NUMBER) {
|
||||
itemID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
@@ -170,24 +454,91 @@ int __fastcall Script_TryOn(void *L) {
|
||||
// any other string is the name of an item the player carries.
|
||||
const Item::Arg::Resolved arg = Item::Arg::ResolveString(s);
|
||||
Item::Location::ByGUIDResult found{};
|
||||
if (arg.itemID != 0)
|
||||
if (arg.itemID != 0) {
|
||||
itemID = static_cast<uint32_t>(arg.itemID);
|
||||
else if (arg.name != nullptr && Item::Location::FindByArg(L, arg, &found))
|
||||
if (!enchantGiven)
|
||||
enchant = static_cast<uint32_t>(arg.enchant);
|
||||
} else if (arg.name != nullptr && Item::Location::FindByArg(L, arg, &found))
|
||||
itemID = static_cast<uint32_t>(Item::ID::FromCGItem(found.item));
|
||||
}
|
||||
}
|
||||
if (itemID == 0) {
|
||||
reinterpret_cast<DisplayError_t>(Offsets::FUN_GAMEUI_DISPLAY_ERROR)(
|
||||
Offsets::GAME_ERROR_NOT_EQUIPPABLE);
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, kTryOnNotEquippable);
|
||||
return 1;
|
||||
}
|
||||
g_tryOnBodySlot = bodySlot;
|
||||
g_tryOnEnchant = enchant;
|
||||
TryOnItem(pane, nullptr, itemID);
|
||||
g_tryOnBodySlot = g_tryOnEnchant = 0;
|
||||
Game::Lua::PushNumber(L, TryOnResult(pane, WornItem(itemID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_Dress(void *L) {
|
||||
g_dressing = true;
|
||||
const int n = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_DRESSUPMODEL_DRESS)(L);
|
||||
g_dressing = false;
|
||||
return n;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsGeoReady(void *L) {
|
||||
void *pane = ResolvePane(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);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetSheathed(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
PaneState &s = g_panes[pane];
|
||||
s.sheathSet = true;
|
||||
s.sheathed = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
s.hideWeapons = Game::Lua::ToBoolean(L, 3) != 0;
|
||||
ApplyWeapons(pane, s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSheathed(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const auto it = g_panes.find(pane);
|
||||
Game::Lua::PushBool(L, it != g_panes.end() && it->second.sheathed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetAutoDress(void *L) {
|
||||
if (void *pane = ResolvePane(L))
|
||||
g_panes[pane].autoDress = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAutoDress(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const auto it = g_panes.find(pane);
|
||||
Game::Lua::PushBool(L, it == g_panes.end() || it->second.autoDress);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"UndressSlot", &Script_UndressSlot},
|
||||
{"TryOn", &Script_TryOn},
|
||||
{"Dress", &Script_Dress},
|
||||
{"IsGeoReady", &Script_IsGeoReady},
|
||||
{"SetSheathed", &Script_SetSheathed},
|
||||
{"GetSheathed", &Script_GetSheathed},
|
||||
{"SetAutoDress", &Script_SetAutoDress},
|
||||
{"GetAutoDress", &Script_GetAutoDress},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kUndressArgs[] = {
|
||||
@@ -198,23 +549,90 @@ const Game::Doc::Function kUndressSlot{
|
||||
const Game::Doc::Field kTryOnArgs[] = {
|
||||
Game::Doc::Req("item", "ItemInfo",
|
||||
"An item ID, item link, trade-skill recipe link, or the name of an item you carry."),
|
||||
Game::Doc::Opt("handSlotName", "cstring", nullptr,
|
||||
"MAINHANDSLOT or SECONDARYHANDSLOT: the hand that holds the item."),
|
||||
Game::Doc::Opt("spellEnchantID", "number", nullptr,
|
||||
"The enchant whose glow a weapon shows. Defaults to the item link's enchant."),
|
||||
};
|
||||
const Game::Doc::Function kTryOn{"Puts an item on the model.", kTryOnArgs, {}};
|
||||
const Game::Doc::Field kTryOnRets[] = {
|
||||
Game::Doc::Req("result", "ItemTryOnReason"),
|
||||
};
|
||||
const Game::Doc::Function kTryOn{"Puts an item on the model.", kTryOnArgs, kTryOnRets};
|
||||
const Game::Doc::Function kDress{"Dresses the model in its unit's equipment.", {}, {}};
|
||||
const Game::Doc::Field kGeoReadyRets[] = {
|
||||
Game::Doc::Req("ready", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsGeoReady{
|
||||
"Returns whether the model and the item models on it have loaded.", {}, kGeoReadyRets};
|
||||
const Game::Doc::Field kSheathedArgs[] = {
|
||||
Game::Doc::Opt("sheathed", "bool", "false"),
|
||||
Game::Doc::Opt("hideWeapons", "bool", "false"),
|
||||
};
|
||||
const Game::Doc::Function kSetSheathed{
|
||||
"Puts the model's weapons in its hands, in their sheaths, or hides them.", kSheathedArgs,
|
||||
{}};
|
||||
const Game::Doc::Field kSheathedRets[] = {
|
||||
Game::Doc::Req("sheathed", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kGetSheathed{"Returns whether the model's weapons are sheathed.", {},
|
||||
kSheathedRets};
|
||||
const Game::Doc::Field kEnabledArgs[] = {
|
||||
Game::Doc::Opt("enabled", "bool", "false"),
|
||||
};
|
||||
const Game::Doc::Function kSetAutoDress{
|
||||
"Sets whether the model wears its unit's equipment when the unit is set.", kEnabledArgs, {}};
|
||||
const Game::Doc::Field kEnabledRets[] = {
|
||||
Game::Doc::Req("enabled", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kGetAutoDress{
|
||||
"Returns whether the model wears its unit's equipment when the unit is set.", {},
|
||||
kEnabledRets};
|
||||
const Game::Doc::Method g_methodDocs[] = {
|
||||
{"UndressSlot", &kUndressSlot},
|
||||
{"TryOn", &kTryOn},
|
||||
{"UndressSlot", &kUndressSlot}, {"TryOn", &kTryOn},
|
||||
{"Dress", &kDress}, {"IsGeoReady", &kIsGeoReady},
|
||||
{"SetSheathed", &kSetSheathed}, {"GetSheathed", &kGetSheathed},
|
||||
{"SetAutoDress", &kSetAutoDress}, {"GetAutoDress", &kGetAutoDress},
|
||||
};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kItemTryOnReason[] = {
|
||||
{"Success", kTryOnSuccess},
|
||||
{"WrongRace", kTryOnWrongRace},
|
||||
{"NotEquippable", kTryOnNotEquippable},
|
||||
{"DataPending", kTryOnDataPending},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Common::RedirectCall(Offsets::CALL_TRY_ON_ITEM_CACHED, Offsets::FUN_DRESSUPMODEL_TRY_ON_ITEM,
|
||||
reinterpret_cast<const void *>(&TryOnItem));
|
||||
Common::RedirectCall(Offsets::CALL_TRY_ON_ATTACH_ITEM, Offsets::FUN_ATTACH_ITEM_MODEL,
|
||||
reinterpret_cast<const void *>(&TryOnAttach));
|
||||
Common::RedirectCall(Offsets::CALL_TRY_ON_EVICT_OFF_HAND, Offsets::FUN_DETACH_HELD_ITEM_SLOT,
|
||||
reinterpret_cast<const void *>(&TryOnEvict));
|
||||
Common::RedirectCall(Offsets::CALL_TRY_ON_EVICT_MAIN_HAND, Offsets::FUN_DETACH_HELD_ITEM_SLOT,
|
||||
reinterpret_cast<const void *>(&TryOnEvict));
|
||||
static const uint8_t kHandChoice[6] = {0x8B, 0x57, 0x2C, 0x83, 0xFA, 0x0D};
|
||||
Common::ReplaceWithCall(Offsets::SITE_TRY_ON_HAND_CHOICE, kHandChoice, sizeof kHandChoice,
|
||||
reinterpret_cast<const void *>(&HandChoiceThunk));
|
||||
Common::RedirectSlot(Offsets::SLOT_DRESSUPMODEL_SET_MODEL_INSTANCE,
|
||||
Offsets::FUN_DRESSUPMODEL_SET_MODEL_INSTANCE,
|
||||
reinterpret_cast<const void *>(&SetModelInstance));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_DRESSUPMODEL_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])));
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "ItemTryOnReason", kItemTryOnReason,
|
||||
static_cast<int>(sizeof(kItemTryOnReason) / sizeof(kItemTryOnReason[0])), "UIModelInfo");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Frames are rebuilt on /reload and a new frame can reuse an old address.
|
||||
void PrepareForReload() {
|
||||
g_panes.clear();
|
||||
}
|
||||
|
||||
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
} // namespace Model::DressUp
|
||||
|
||||
Reference in New Issue
Block a user