mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b36bb10e6 | ||
|
|
6628dcca19 | ||
|
|
55d94c80d6 | ||
|
|
c0b403ba05 | ||
|
|
f6b98bc3ec | ||
|
|
8f4f574174 | ||
|
|
43a989dd56 | ||
|
|
0caddb8fe6 | ||
|
|
8e603a7a0b | ||
|
|
4726fd7c85 | ||
|
|
3e0c40184b | ||
|
|
b3e8d92b03 | ||
|
|
0a3f8324ea | ||
|
|
6f3a8ffc1d | ||
|
|
3862408e1a | ||
|
|
12a81f7f89 | ||
|
|
05fa5845b7 |
@@ -10,3 +10,4 @@
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.blp binary
|
||||
|
||||
+9
-7
@@ -76,13 +76,14 @@ endif()
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
|
||||
|
||||
# Embed the addons under `src/embed/AddOns/` and `src/bindings/Bindings.xml` as RCDATA
|
||||
# resources. The file-read hook in `src/addons/Embedded.cpp` serves the
|
||||
# addon files when the engine asks for paths under
|
||||
# `Interface\AddOns\<addon>\`, so the addons load even when the user
|
||||
# doesn't have them on disk. `src/bindings/Inject.cpp` splices the bindings
|
||||
# XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time.
|
||||
# Embed the addons under `src/embed/AddOns/`, `src/bindings/Bindings.xml` and
|
||||
# the textures under `src/embed/Textures/` as RCDATA resources. The file-read
|
||||
# hook in `src/addons/Embedded.cpp` serves the addon files when the engine
|
||||
# asks for paths under `Interface\AddOns\<addon>\`, so the addons load even
|
||||
# when the user doesn't have them on disk. `src/bindings/Inject.cpp` splices
|
||||
# the bindings XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time. `src/texture/Builtin.cpp`
|
||||
# serves each texture at its game path when no file has it.
|
||||
include(cmake/embed_resources.cmake)
|
||||
embed_resources(
|
||||
TARGET ${PROJECT_NAME}
|
||||
@@ -90,6 +91,7 @@ embed_resources(
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools" "ClassicAPI_EventTrace"
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
TEXTURES_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/Textures"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
|
||||
target_compile_definitions(
|
||||
|
||||
@@ -106,13 +106,13 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `SetErrorCallstackHeight`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `debuglocals`, `debugstack` (coroutine argument), `difftime`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `gmatch`, `math.cosh`, `math.fmod`, `math.huge`, `math.modf`, `math.sinh`, `math.tanh`, `Mixin`, `newproxy`, `scrub`, `select`, `SetErrorCallstackHeight`, `strbyte` (range args), `strcmputf8i`, `strconcat`, `string.byte` (range args), `string.concat`, `string.format` (two-digit `%n$`), `string.gmatch`, `string.gsub` (table replacement, `%0`), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.removemulti`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [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` (`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`, `PlayerModel:ApplySpellVisualKit` |
|
||||
| [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` |
|
||||
@@ -131,7 +131,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer`, `InteractUnit`, `C_PlayerInteractionManager.InteractUnit`, `IsTargetLoose` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas`, `SetPortraitTextureFromCreatureDisplayID` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
@@ -219,12 +219,13 @@ open the game menu and click AddOn Options, then ClassicAPI.
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Stand, dismount and form](docs/API.md#stand-dismount-and-form-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast, loot or open the flight map while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast, use an object, loot, attack or open the flight map while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, talk to an NPC, use an object or an item, or open the flight map. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
| [Expanded cursors](docs/API.md#expanded-cursors-expandedcursors) | `expandedCursors` | `0` | `1` makes the cursor show what you can do with an NPC or an object: an anvil for repairs, a hearthstone for an inn, a horseshoe for a stable, `!` or `?` for quests, and a fish hook for your fishing bobber. |
|
||||
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
|
||||
| [Name completion](docs/API.md#name-completion-settings) | `autoCompleteResortNamesOnRecency` | `1` | `1` puts the players that you whispered first in `C_AutoComplete` results, the most recent first. `0` uses alphabetical order. |
|
||||
| | `autoCompleteWhenEditingFromCenter` | `1` | `1` also matches the text after the cursor when you type inside a name. |
|
||||
@@ -504,10 +505,10 @@ the embedded addon.
|
||||
## Lua errors
|
||||
|
||||
ClassicAPI replaces the game's Lua error box with a Lua error window. The
|
||||
window opens when an error occurs. It shows the message, the time, the count
|
||||
and the stack of each error. You can select and copy the text. The arrow
|
||||
buttons show the previous and the next error. The **Reload UI** button
|
||||
reloads the interface.
|
||||
window opens when an error occurs. It shows the message, the time, the count,
|
||||
the stack and the local variables of each error. You can select and copy the
|
||||
text. The arrow buttons show the previous and the next error. The
|
||||
**Reload UI** button reloads the interface.
|
||||
|
||||
If you use `!BugGrabber` (with BugSack), BugGrabber gets the errors and the
|
||||
window does not open. If an addon sets its own error handler, the window also
|
||||
|
||||
+52
-12
@@ -1,5 +1,5 @@
|
||||
# embed_resources.cmake — embed the addons and the bindings XML as Win32
|
||||
# RCDATA resources.
|
||||
# embed_resources.cmake — embed the addons, the bindings XML and the
|
||||
# built-in textures as Win32 RCDATA resources.
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
@@ -16,20 +16,27 @@
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
# namespace Texture::Builtin::Manifest {
|
||||
# struct File { const char *path; int resourceId; int index; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
#
|
||||
# Each resource is a pack: one gzip stream of several files, which
|
||||
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
|
||||
# Resource 1 is the bindings XML, a pack of one; then each addon is one
|
||||
# pack, in ADDONS order. A pack compresses each file against the files
|
||||
# before it, so the addon text packs to about 22% of its size, against 27%
|
||||
# for one gzip stream per file. The DLL inflates a whole pack with the
|
||||
# engine's zlib on first use (src/embed/Resource.cpp), so the
|
||||
# load-on-demand addon costs nothing until it loads.
|
||||
# pack, in ADDONS order; then the textures, one pack. A pack compresses each
|
||||
# file against the files before it, so the addon text packs to about 22% of
|
||||
# its size, against 27% for one gzip stream per file. The DLL inflates a
|
||||
# whole pack with the engine's zlib on first use (src/embed/Resource.cpp),
|
||||
# so the load-on-demand addon costs nothing until it loads.
|
||||
#
|
||||
# File paths are relative to ADDONS_ROOT, in backslash form
|
||||
# Addon file paths are relative to ADDONS_ROOT, in backslash form
|
||||
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case. A file's index
|
||||
# is its place in its addon's pack, which is in path order.
|
||||
# `Interface\AddOns\`. Texture paths are relative to TEXTURES_ROOT, where
|
||||
# each file sits at its game path (`Interface\Cursor\Quest.blp`), for
|
||||
# src/texture/Builtin.cpp. The DLL compares both ignoring case. A file's
|
||||
# index is its place in its pack, which is in path order.
|
||||
#
|
||||
# Each addon's `.toc` is the one file that is not embedded verbatim: its
|
||||
# `## Version:` line is stamped with ADDON_VERSION into a copy under
|
||||
@@ -46,7 +53,8 @@
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file>
|
||||
# TEXTURES_ROOT <dir> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
|
||||
|
||||
@@ -81,7 +89,8 @@ function(_embed_write_if_changed path content)
|
||||
endfunction()
|
||||
|
||||
function(embed_resources)
|
||||
cmake_parse_arguments(E "" "TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
cmake_parse_arguments(E ""
|
||||
"TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;TEXTURES_ROOT;OUT_DIR"
|
||||
"ADDONS" ${ARGN})
|
||||
|
||||
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
|
||||
@@ -125,6 +134,22 @@ function(embed_resources)
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
file(GLOB_RECURSE textures CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_TEXTURES_ROOT}" "${E_TEXTURES_ROOT}/*")
|
||||
list(SORT textures) # deterministic indexes
|
||||
set(texture_paths "")
|
||||
set(texture_manifest "")
|
||||
set(index 0)
|
||||
foreach(rel ${textures})
|
||||
list(APPEND texture_paths "${E_TEXTURES_ROOT}/${rel}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND texture_manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "Textures" ${texture_paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
@@ -159,6 +184,21 @@ ${manifest}};
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Addons::Embedded::Manifest
|
||||
|
||||
namespace Texture::Builtin::Manifest {
|
||||
|
||||
struct File {
|
||||
const char *path; // the game path
|
||||
int resourceId; // the textures' pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
${texture_manifest}};
|
||||
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Texture::Builtin::Manifest
|
||||
")
|
||||
|
||||
add_custom_target(${E_TARGET}_embed DEPENDS ${inputs})
|
||||
|
||||
+427
-11
@@ -202,6 +202,7 @@ build instructions.
|
||||
- [Cursor](#cursor)
|
||||
- [`GetCursorInfo()`](#getcursorinfo)
|
||||
- [`CursorHasMacro()`](#cursorhasmacro)
|
||||
- [Expanded cursors (`expandedCursors`)](#expanded-cursors-expandedcursors)
|
||||
|
||||
- [DeathInfo](#deathinfo)
|
||||
- [`C_DeathInfo.GetSelfResurrectOptions()`](#c_deathinfogetselfresurrectoptions)
|
||||
@@ -678,6 +679,8 @@ build instructions.
|
||||
- [`difftime(time2 [, time1])`](#difftimetime2--time1)
|
||||
- [`xpcall(f, msgh [, arg1, ...])`](#xpcallf-msgh--arg1-)
|
||||
- [`GetCallstackHeight()` / `GetErrorCallstackHeight()` / `SetErrorCallstackHeight([height])`](#getcallstackheight--geterrorcallstackheight--seterrorcallstackheightheight)
|
||||
- [`debugstack(thread [, start [, count1 [, count2]]])`](#debugstackthread--start--count1--count2)
|
||||
- [`debuglocals([level [, skipFunctionsAndUserdata]])`](#debuglocalslevel--skipfunctionsanduserdata)
|
||||
- [`collectgarbage(opt [, arg])`](#collectgarbageopt--arg)
|
||||
- [`table.wipe(t)` / `wipe(t)`](#tablewipet--wipet)
|
||||
- [`table.count(tbl)`](#tablecounttbl)
|
||||
@@ -777,13 +780,30 @@ build instructions.
|
||||
|
||||
- [Model](#model)
|
||||
- [`model:SetDisplayInfo(displayID)`](#modelsetdisplayinfodisplayid)
|
||||
- [`model:SetCreature(creatureID)`](#modelsetcreaturecreatureid)
|
||||
- [`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)
|
||||
- [`model:SetRotation(rotation [, animate])`](#modelsetrotationrotation--animate)
|
||||
- [`model:SetUnit(unit)`](#modelsetunitunit)
|
||||
- [`model:CanSetUnit(unit)`](#modelcansetunitunit)
|
||||
- [`model:GetDisplayInfo()`](#modelgetdisplayinfo)
|
||||
- [`model:SetAnimation(animation [, variation])`](#modelsetanimationanimation--variation)
|
||||
- [`model:FreezeAnimation(animation, variation, frame)`](#modelfreezeanimationanimation-variation-frame)
|
||||
- [`model:HasAnimation(animation)`](#modelhasanimationanimation)
|
||||
- [`model:SetDoBlend([doBlend])`](#modelsetdoblenddoblend)
|
||||
- [`model:GetDoBlend()`](#modelgetdoblend)
|
||||
- [`model:SetKeepModelOnHide(keepModelOnHide)`](#modelsetkeepmodelonhidekeepmodelonhide)
|
||||
- [`model:GetKeepModelOnHide()`](#modelgetkeepmodelonhide)
|
||||
- [`model:ApplySpellVisualKit(spellVisualKitID [, oneShot])`](#modelapplyspellvisualkitspellvisualkitid--oneshot)
|
||||
- [`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)
|
||||
@@ -943,7 +963,7 @@ build instructions.
|
||||
- [`UnitCastingDuration(unit)` / `UnitChannelDuration(unit)`](#unitcastingdurationunit--unitchanneldurationunit)
|
||||
- [`C_Spell.GetSpellLevelInfo(spellID)`](#c_spellgetspelllevelinfospellid)
|
||||
- [`GetSpellRequiredTargetLevel(spellID)`](#getspellrequiredtargetlevelspellid)
|
||||
- [Cast settings (`autoStand`, `autoDismount`, `autoUnshift`)](#cast-settings-autostand-autodismount-autounshift)
|
||||
- [Stand, dismount and form settings (`autoStand`, `autoDismount`, `autoUnshift`)](#stand-dismount-and-form-settings-autostand-autodismount-autounshift)
|
||||
|
||||
- [SpellActivationOverlay](#spellactivationoverlay)
|
||||
- [`C_SpellActivationOverlay.IsSpellOverlayed(spellID)` / `IsSpellOverlayed(spellID)`](#c_spellactivationoverlayisspelloverlayedspellid--isspelloverlayedspellid)
|
||||
@@ -1031,6 +1051,7 @@ build instructions.
|
||||
- [`C_Texture.GetAtlasElements()`](#c_texturegetatlaselements)
|
||||
- [`C_Texture.RegisterAtlas(name, file, width, height, left, right, top, bottom [, tilesHorizontally, tilesVertically])`](#c_textureregisteratlasname-file-width-height-left-right-top-bottom--tileshorizontally-tilesvertically)
|
||||
- [Atlas markup](#atlas-markup)
|
||||
- [`SetPortraitTextureFromCreatureDisplayID(textureObject, creatureDisplayID)`](#setportraittexturefromcreaturedisplayidtextureobject-creaturedisplayid)
|
||||
|
||||
- [Time](#time)
|
||||
- [`GetServerTime()`](#getservertime)
|
||||
@@ -4852,6 +4873,57 @@ Changes to what the cursor holds are reported by the
|
||||
|
||||
Returns `true` if the cursor holds a macro, and `false` if it does not.
|
||||
|
||||
### Expanded cursors (`expandedCursors`)
|
||||
|
||||
When the `expandedCursors` setting is `1` and you point at an NPC or an
|
||||
object, the cursor shows what you can do with it. The default is `0`. You can
|
||||
also change the setting in the ClassicAPI options, on the Interface page.
|
||||
|
||||
These NPC services get new images:
|
||||
|
||||
| The NPC has | The cursor |
|
||||
|---|---|
|
||||
| A quest that you can accept, or a quest that you did not complete | A yellow `!` |
|
||||
| A quest that you can turn in | A yellow `?` |
|
||||
| A repeatable quest that you can turn in | A blue `?` |
|
||||
| Repairs | An anvil |
|
||||
| An inn | A hearthstone |
|
||||
| A stable | A horseshoe |
|
||||
|
||||
When an NPC has more than one service, the cursor shows the first service
|
||||
in this list:
|
||||
|
||||
1. A quest
|
||||
2. Repairs
|
||||
3. An inn
|
||||
4. Flights
|
||||
5. Training
|
||||
6. Resurrection at a spirit healer
|
||||
7. A bank
|
||||
8. Guild charters, tabards and battlegrounds
|
||||
9. An auction house
|
||||
10. A stable
|
||||
11. Goods for sale
|
||||
12. Gossip
|
||||
|
||||
The other services keep their usual images. For example, a vendor that also
|
||||
has gossip shows the vendor image, not the speech bubble.
|
||||
|
||||
Your fishing bobber shows a fish hook, not the gear.
|
||||
|
||||
When you cannot use the NPC or the object, the cursor shows the gray form of
|
||||
the image. For example, this occurs when you are too far away.
|
||||
|
||||
The soft-interact icon and
|
||||
[`SetUnitCursorTexture`](#setunitcursortexturetexture-unit--style--includelowpriority)
|
||||
show the same images as the cursor. For a quest, they show no image.
|
||||
|
||||
```
|
||||
/console expandedCursors 1
|
||||
```
|
||||
|
||||
*ClassicAPI extension.*
|
||||
|
||||
## DeathInfo
|
||||
|
||||
### `C_DeathInfo.GetSelfResurrectOptions()`
|
||||
@@ -16136,6 +16208,44 @@ end
|
||||
|
||||
Works in the world and on the login screen.
|
||||
|
||||
### `debugstack(thread [, start [, count1 [, count2]]])`
|
||||
|
||||
With a coroutine as the first argument, `debugstack` returns the call stack
|
||||
of that coroutine. `start` is 0 by default, which is the innermost level of
|
||||
the coroutine. Without a coroutine, `debugstack` works as before.
|
||||
|
||||
```lua
|
||||
local co = coroutine.create(function() coroutine.yield() end)
|
||||
coroutine.resume(co)
|
||||
print(debugstack(co))
|
||||
```
|
||||
|
||||
### `debuglocals([level [, skipFunctionsAndUserdata]])`
|
||||
|
||||
Returns the local variables and the upvalues of a call level as one string.
|
||||
An error handler uses it to show the values in the code that caused the
|
||||
error. `level` counts the same as in `debugstack`, and is 1 by default.
|
||||
|
||||
Each variable is one `name=value` line. A string value is in quotes. A
|
||||
table shows the name of its frame, or `<table>`, then its fields one space in,
|
||||
then `}`. Fields of tables inside a table do not show. A function shows the
|
||||
file and line where it is defined.
|
||||
|
||||
If `skipFunctionsAndUserdata` is `true`, function and userdata values do not
|
||||
show. The text stops at 4095 bytes.
|
||||
|
||||
`debuglocals` works only while an error handler runs. At other times it
|
||||
returns nothing.
|
||||
|
||||
```lua
|
||||
seterrorhandler(function(message)
|
||||
local level = GetCallstackHeight() - GetErrorCallstackHeight() + 1
|
||||
print(debuglocals(level, true))
|
||||
end)
|
||||
```
|
||||
|
||||
The error window shows these values under **Locals**.
|
||||
|
||||
### `collectgarbage(opt [, arg])`
|
||||
|
||||
Accepts Lua 5.1's string options. The stock 5.0 collector takes only an
|
||||
@@ -18420,11 +18530,12 @@ m:SetPoint("CENTER", 0, 0)
|
||||
m:SetDisplayInfo(330) -- a human NPC
|
||||
```
|
||||
|
||||
### `model:SetCreature(creatureID)`
|
||||
### `model:SetCreature(creatureID [, displayID])`
|
||||
|
||||
Shows a creature on the model by its creature ID (the creature's database
|
||||
ID, not a display ID). The model is the same as `SetDisplayInfo` with the
|
||||
creature's display ID.
|
||||
creature's display ID. If you give `displayID`, the model shows that
|
||||
display in place of the creature's own display.
|
||||
|
||||
The creature must be in the client creature cache. The client adds a
|
||||
creature to the cache when you see it, target it, or put the mouse on it.
|
||||
@@ -18446,6 +18557,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.
|
||||
@@ -18490,6 +18626,140 @@ with no animation.
|
||||
PreviewModel:SetRotation(math.pi / 2, false)
|
||||
```
|
||||
|
||||
### `model:SetUnit(unit)`
|
||||
|
||||
Shows a unit on the model. Returns `true` when the unit exists, and `false`
|
||||
when it does not. An unknown unit token is an error.
|
||||
|
||||
```lua
|
||||
if not PreviewModel:SetUnit("target") then
|
||||
PreviewModel:SetUnit("player")
|
||||
end
|
||||
```
|
||||
|
||||
### `model:CanSetUnit(unit)`
|
||||
|
||||
Returns `true` when the unit exists, so `SetUnit` can show it. The call
|
||||
does not change the model. An unknown unit token is an error, the same as
|
||||
in `SetUnit`.
|
||||
|
||||
### `model:GetDisplayInfo()`
|
||||
|
||||
Returns the display ID (a `CreatureDisplayInfo` ID) that the model shows:
|
||||
|
||||
- After `SetDisplayInfo` or `SetCreature`, the display that the model got.
|
||||
- After `SetUnit`, the display that the unit has now. For a shapeshifted
|
||||
druid, this is the display of the form.
|
||||
|
||||
Returns 0 when the model shows nothing or an item, or when its unit no
|
||||
longer exists.
|
||||
|
||||
### `model:SetAnimation(animation [, variation])`
|
||||
|
||||
Sets the animation that the model plays. `animation` is an `AnimationData`
|
||||
ID, for example 0 Stand, 4 Walk, 5 Run, 60 EmoteTalk, 67 EmoteWave, 69
|
||||
EmoteDance. `variation` selects one of the model's versions of the
|
||||
animation. The default is 0. The value -1 selects a version at random.
|
||||
|
||||
The model plays the animation again each time it ends. The model goes back
|
||||
to Stand when the frame loads a new model: after `SetUnit`, `RefreshUnit`,
|
||||
`SetDisplayInfo`, `SetCreature`, `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)`.
|
||||
|
||||
If the model does not have the animation, it plays the fallback animation
|
||||
that `AnimationData` gives, or Stand. Use `HasAnimation` to find out first.
|
||||
While the animation is not Stand, `SetRotation` turns the model without the
|
||||
step animation.
|
||||
|
||||
Call `SetAnimation` after `SetUnit`, `SetDisplayInfo`, or `SetCreature`. The
|
||||
animation starts when the model finishes loading.
|
||||
|
||||
```lua
|
||||
PreviewModel:SetUnit("player")
|
||||
PreviewModel:SetAnimation(69) -- dance
|
||||
```
|
||||
|
||||
### `model:FreezeAnimation(animation, variation, frame)`
|
||||
|
||||
Holds the model still in one pose of an animation. `animation` and
|
||||
`variation` are the same as in `SetAnimation`. `frame` is the time into the
|
||||
animation, in milliseconds. A time past the end of the animation wraps to
|
||||
the start.
|
||||
|
||||
The model holds the pose until the frame loads a new model, the same as
|
||||
with `SetAnimation`. To make the model move again, call `SetAnimation`.
|
||||
|
||||
```lua
|
||||
PreviewModel:FreezeAnimation(69, 0, 500) -- dance pose, 0.5 seconds in
|
||||
```
|
||||
|
||||
### `model:HasAnimation(animation)`
|
||||
|
||||
Returns `true` when the model has the animation (an `AnimationData` ID).
|
||||
Returns `false` until the model finishes loading. A model file that the
|
||||
client already loaded in this session is ready at once.
|
||||
|
||||
```lua
|
||||
local anim = PreviewModel:HasAnimation(69) and 69 or 0
|
||||
PreviewModel:SetAnimation(anim)
|
||||
```
|
||||
|
||||
### `model:SetDoBlend([doBlend])`
|
||||
|
||||
Sets how the model starts the animation that `SetAnimation` set. If
|
||||
`doBlend` is `true`, the model blends from the animation that plays now into
|
||||
that animation. If `doBlend` is `false` or not given, the animation starts
|
||||
at once. The default is `false`.
|
||||
|
||||
### `model:GetDoBlend()`
|
||||
|
||||
Returns the value that `SetDoBlend` set.
|
||||
|
||||
### `model:SetKeepModelOnHide(keepModelOnHide)`
|
||||
|
||||
Sets whether the frame keeps its model while the frame is hidden. The
|
||||
default is `false`: the frame removes the model when it hides, and loads
|
||||
the model again from its unit, 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`,
|
||||
`SetItem`, or `DressUpModel:Dress()` while the frame is hidden, the frame loads the
|
||||
new model when it shows, the same as with `false`.
|
||||
|
||||
```lua
|
||||
DressUpModel:SetKeepModelOnHide(true)
|
||||
```
|
||||
|
||||
### `model:GetKeepModelOnHide()`
|
||||
|
||||
Returns the value that `SetKeepModelOnHide` set.
|
||||
|
||||
### `model:ApplySpellVisualKit(spellVisualKitID [, oneShot])`
|
||||
|
||||
Shows the effects of a spell visual kit (a `SpellVisualKit` ID) on the
|
||||
model, for example the glow on the hands while a spell is cast. Each effect
|
||||
attaches to its point on the model: head, chest, base, hands, breath, or
|
||||
weapons. An effect whose point the model does not have does not show.
|
||||
|
||||
If `oneShot` is `true`, each effect plays one time and then fades out. If
|
||||
`oneShot` is `false` or not given, the effects play again and again until
|
||||
the model changes. The model changes after `SetUnit`, `SetDisplayInfo`,
|
||||
`SetCreature`, `SetItem`, and when the frame shows again after it was
|
||||
hidden.
|
||||
|
||||
A kit can also change the color or the transparency of the model. This
|
||||
change stays until the model changes. The kit does not play its animation
|
||||
or its sound. A kit ID that is not in `SpellVisualKit` does nothing.
|
||||
|
||||
```lua
|
||||
PreviewModel:SetUnit("player")
|
||||
PreviewModel:ApplySpellVisualKit(99) -- holy glow on both hands
|
||||
PreviewModel:ApplySpellVisualKit(154, true) -- Holy Light flash, one time
|
||||
```
|
||||
|
||||
### `DressUpModel:UndressSlot(slot)`
|
||||
|
||||
Removes the item in one inventory slot from a `DressUpModel`. `slot` is an
|
||||
@@ -18501,7 +18771,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:
|
||||
|
||||
@@ -18516,12 +18786,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
|
||||
@@ -22118,10 +22474,11 @@ with `SPELL_FAILED_LOWLEVEL`; higher ranks auto-downrank.
|
||||
> 1.12 client's `Spell.dbc`, so the `spellLevel` rule is the only
|
||||
> client-readable target-level mechanism.
|
||||
|
||||
### Cast settings (`autoStand`, `autoDismount`, `autoUnshift`)
|
||||
### Stand, dismount and form settings (`autoStand`, `autoDismount`, `autoUnshift`)
|
||||
|
||||
Three settings change what occurs when your state blocks a cast. Each one is
|
||||
`0` (the default) or `1`. At `0`, the cast fails with the usual error.
|
||||
Three settings change what occurs when your state blocks a cast or another
|
||||
action. Each one is `0` (the default) or `1`. At `0`, the action fails with
|
||||
the usual error.
|
||||
|
||||
| Setting | At `1`, before the cast |
|
||||
|---|---|
|
||||
@@ -22137,6 +22494,32 @@ cast can leave. For example, Healing Touch in Bear Form and Renew in
|
||||
Shadowform. A spell that needs a different form or stance ("Must be in ...")
|
||||
still fails, and you stay in your form.
|
||||
|
||||
The settings also apply to these actions:
|
||||
|
||||
| Action | `autoStand` | `autoDismount` | `autoUnshift` |
|
||||
|---|---|---|---|
|
||||
| Talk to an NPC | — | — | You leave the form. |
|
||||
| Use an object | — | You dismount. You stay mounted at a mailbox. | You leave the form. |
|
||||
| Loot a creature | You stand up. | You dismount. | — |
|
||||
| Loot a corpse | You stand up. | — | — |
|
||||
| Use an item | — | — | You leave the form. |
|
||||
| Attack | — | You dismount. | — |
|
||||
| Open the flight map | You stand up. | You dismount. | You leave the form. |
|
||||
|
||||
A form that can talk and use objects, for example Shadowform, stays on when
|
||||
you talk to an NPC or use an object. While `autoUnshift` is `1`, the cursor
|
||||
over an NPC or an object shows its usual icon in such a form. While
|
||||
`autoDismount` is `1`, the cursor shows the attack icon over an enemy while
|
||||
you are mounted.
|
||||
|
||||
You dismount to attack only when the target is in melee range. Farther away,
|
||||
the attack does not start and "Out of range." shows. With click-to-move on,
|
||||
no error shows and you walk to the target; click the target again when you
|
||||
arrive.
|
||||
|
||||
To open a locked object, such as a chest, you cast a spell. Thus the cast
|
||||
rules above apply.
|
||||
|
||||
```
|
||||
/console autoDismount 1
|
||||
```
|
||||
@@ -23858,6 +24241,36 @@ in text.
|
||||
Text typed by other players never draws atlas art. This matches how a typed
|
||||
texture path behaves.
|
||||
|
||||
### `SetPortraitTextureFromCreatureDisplayID(textureObject, creatureDisplayID)`
|
||||
|
||||
Shows the round portrait of a creature on a texture, by its display ID (a
|
||||
`CreatureDisplayInfo` ID). A unit is not necessary. The function returns
|
||||
nothing.
|
||||
|
||||
The texture changes at once. The portrait is transparent until the creature
|
||||
model loads, and then it shows, a short time after the call.
|
||||
|
||||
A character-based display gets the same appearance as a spawned NPC of that
|
||||
display. Like the portrait of a unit, the portrait does not show helms,
|
||||
shoulders, or weapons.
|
||||
|
||||
A display ID that is not in `CreatureDisplayInfo` does not change the
|
||||
texture. Some models have no portrait view. For these models, the portrait
|
||||
stays transparent.
|
||||
|
||||
Each display ID is drawn one time. Later calls for the same display ID show
|
||||
the finished portrait at once. The portraits stay until you log out.
|
||||
|
||||
```lua
|
||||
local frame = CreateFrame("Frame", nil, UIParent)
|
||||
frame:SetWidth(64)
|
||||
frame:SetHeight(64)
|
||||
frame:SetPoint("CENTER", 0, 0)
|
||||
local portrait = frame:CreateTexture(nil, "ARTWORK")
|
||||
portrait:SetAllPoints(frame)
|
||||
SetPortraitTextureFromCreatureDisplayID(portrait, 3254)
|
||||
```
|
||||
|
||||
## Time
|
||||
|
||||
### `GetServerTime()`
|
||||
@@ -24589,6 +25002,9 @@ Returns `true` when it sets the texture. When the unit has no image, it
|
||||
returns `false` and clears the texture.
|
||||
Otherwise it returns `false` and does not change the texture.
|
||||
|
||||
A quest cursor (see [Expanded cursors](#expanded-cursors-expandedcursors))
|
||||
has no image, also when `includeLowPriority` is `true`.
|
||||
|
||||
```lua
|
||||
local icon = UIParent:CreateTexture(nil, "OVERLAY")
|
||||
icon:SetWidth(24)
|
||||
|
||||
+3
-3
@@ -178,9 +178,9 @@ behaviour the comment describes (`push 0; mov edx, 1; mov ecx, L`) is
|
||||
| `0x006F4450` | `lua_next` | fastcall | calls `luaH_next` at `0x006FA2A0` | ★ |
|
||||
| `0x006F44E0` | `lua_concat` | fastcall | calls `luaV_concat` at `0x006F84D0` | ★ |
|
||||
| `0x006F4560` | `lua_newuserdata` | fastcall | tag 7; calls `luaS_newudata` at `0x006F9E30` | ★ |
|
||||
| `0x006F45B0` | `lua_getupvalue` | fastcall | reads upvalue from C closure | ◦ |
|
||||
| `0x006F4660` | `lua_setupvalue` | fastcall | | ◦ |
|
||||
| `0x006F46E0` | *internal upvalue helper*| | shared by get/set above | ◦ |
|
||||
| `0x006F45B0` | `lua_pushupvalues` | fastcall | pushes the running C closure's upvalues, returns how many | ★ |
|
||||
| `0x006F4660` | `lua_getupvalue` | fastcall | `(L, funcindex, n)`: pushes upvalue n, returns its name or null | ★ |
|
||||
| `0x006F46E0` | `aux_upvalue` | | finds upvalue n of a function, returns its name | ★ |
|
||||
|
||||
## Auxiliary library (`luaL_*`)
|
||||
|
||||
|
||||
@@ -64,6 +64,17 @@ bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 2, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target) {
|
||||
uintptr_t current = 0;
|
||||
std::memcpy(¤t, reinterpret_cast<const void *>(slot), sizeof current);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (current == to)
|
||||
return true;
|
||||
if (current != expected)
|
||||
return false;
|
||||
return PatchBytes(reinterpret_cast<void *>(slot), &to, sizeof to);
|
||||
}
|
||||
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target) {
|
||||
uint8_t code[64];
|
||||
if (len < 5 || len > sizeof code)
|
||||
@@ -102,4 +113,20 @@ void EnsureParentDirs(const char *relPath) {
|
||||
}
|
||||
}
|
||||
|
||||
char FoldPathChar(char c) {
|
||||
if (c == '/')
|
||||
return '\\';
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return static_cast<char>(c + ('a' - 'A'));
|
||||
return c;
|
||||
}
|
||||
|
||||
bool SamePath(const char *a, const char *b) {
|
||||
for (; FoldPathChar(*a) == FoldPathChar(*b); ++a, ++b) {
|
||||
if (*a == '\0')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -28,6 +28,10 @@ bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target);
|
||||
// at `site` to `target` when it still reaches `expected`. Same contract.
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// RedirectCall for a function-pointer slot (a vtable entry): points the slot
|
||||
// at `slot` to `target` when it still holds `expected`. Same contract.
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target);
|
||||
|
||||
// Writes `call target` plus NOP padding over the `len` bytes (5 to 64) of
|
||||
// code at `site` when they still hold `original`; true when the site
|
||||
// now calls `target`. A site holding anything else (another DLL's patch, a
|
||||
@@ -45,4 +49,11 @@ void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
// directory. Directories that already exist are a no-op.
|
||||
void EnsureParentDirs(const char *relPath);
|
||||
|
||||
// `c` as game paths compare it: '/' as '\', and A-Z as a-z.
|
||||
char FoldPathChar(char c);
|
||||
|
||||
// Whether `a` and `b` are the same game path: equal after FoldPathChar, so
|
||||
// any case and either slash.
|
||||
bool SamePath(const char *a, const char *b);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
+538
-24
@@ -671,6 +671,39 @@ enum Offsets {
|
||||
OFF_ANCHOR_NODE_Y = 0x08,
|
||||
OFF_ANCHOR_NODE_RELATIVE_POINT = 0x10,
|
||||
VTBL_ANCHOR_NODE_GET_RELATIVE = 0x0C,
|
||||
// The relative node's other three slots (Ghidra: each function's only
|
||||
// reference is its slot): +0x00 the deleting destructor 0x00767D80, `void
|
||||
// *__thiscall(node, int flags)`, `ret 4` — every free of a point (SetPoint,
|
||||
// ClearAllPoints, FUN_REGION_CLEAR_POINT_BY_RELATIVE, the layout destructor)
|
||||
// is `push 1; call [vtbl]`; +0x04 / +0x08 the x / y getters 0x007A2F90 /
|
||||
// 0x007A3070, `float __thiscall(node, float scale)`, `ret 4` (benilla-re
|
||||
// ui/anchors "Relative point coordinates"). A getter ends a deferred
|
||||
// relative's deferral with FUN_REGION_LAYOUT_REALIZE(0) (0x007A2FC0), then
|
||||
// reads the node again (0x007A2FC5).
|
||||
VTBL_ANCHOR_NODE_DELETE = 0x00,
|
||||
VTBL_ANCHOR_NODE_X = 0x04,
|
||||
VTBL_ANCHOR_NODE_Y = 0x08,
|
||||
FUN_ANCHOR_NODE_DELETE = 0x00767D80,
|
||||
FUN_ANCHOR_NODE_X = 0x007A2F90,
|
||||
FUN_ANCHOR_NODE_Y = 0x007A3070,
|
||||
// A layout's point walks, 4.3.4's GetFramePointX / Y: `float
|
||||
// __thiscall(layout, const int *points, int count)`, `ret 8` — calls
|
||||
// VTBL_ANCHOR_NODE_X (Y) of each set slot in `points` with the layout's
|
||||
// scale (+0x58) and returns the first result that is not
|
||||
// VAR_LAYOUT_UNDEFINED. The six edge resolvers are the only callers
|
||||
// (Ghidra xrefs): left / centre x / right call the x walk, top / centre y /
|
||||
// bottom the y walk.
|
||||
FUN_LAYOUT_POINTS_X = 0x007671A0,
|
||||
FUN_LAYOUT_POINTS_Y = 0x007671F0,
|
||||
CALL_LAYOUT_LEFT_POINTS = 0x007673F1,
|
||||
CALL_LAYOUT_CENTER_X_POINTS = 0x00767281,
|
||||
CALL_LAYOUT_RIGHT_POINTS = 0x00767561,
|
||||
CALL_LAYOUT_TOP_POINTS = 0x007674F1,
|
||||
CALL_LAYOUT_CENTER_Y_POINTS = 0x00767381,
|
||||
CALL_LAYOUT_BOTTOM_POINTS = 0x007675D1,
|
||||
// float: the "no value" an edge, a point or a walk returns (benilla-re
|
||||
// s_layoutUndefined); the walk compares with `fcomp [0x00CF550C]`.
|
||||
VAR_LAYOUT_UNDEFINED = 0x00CF550C,
|
||||
|
||||
// The point names the XML loader and SetPoint share (benilla-re
|
||||
// ui/anchors "Point slots and point names"), 4.3.4's StringToFramePoint /
|
||||
@@ -1944,10 +1977,36 @@ enum Offsets {
|
||||
VAR_CURSOR_BASE_MODE = 0x00BE2C4C,
|
||||
CURSOR_MODE_POINT = 1,
|
||||
CURSOR_MODE_CAST = 2,
|
||||
CURSOR_MODE_BUY = 3,
|
||||
CURSOR_MODE_ATTACK = 4,
|
||||
CURSOR_MODE_INTERACT = 5,
|
||||
CURSOR_MODE_SPEAK = 6,
|
||||
CURSOR_MODE_PICKUP = 8,
|
||||
CURSOR_MODE_TAXI = 9,
|
||||
CURSOR_MODE_TRAINER = 10,
|
||||
CURSOR_MODE_LAST_NAMED = 20,
|
||||
CURSOR_MODE_UNABLE_OFFSET = 20,
|
||||
CURSOR_MODE_CUSTOM = 0x29,
|
||||
// The shown mode, the one SetCursorMode compares and stores.
|
||||
VAR_CURSOR_MODE = 0x00BE2C2C,
|
||||
// SendCursorImage — `void __cdecl(void)`: copies the image slot of
|
||||
// VAR_CURSOR_MODE (slot n at 0x00BE2D80 + n * 0x1000, CURSOR_IMAGE_SIZE
|
||||
// squared BGRA8888 texels) to the device cursor, or the dragged payload's
|
||||
// picture while one is held (disassembled 0x523790-0x52380D).
|
||||
FUN_SEND_CURSOR_IMAGE = 0x00523790,
|
||||
// SetCustomCursor — `uint32_t __fastcall(const char *path)`, plain ret:
|
||||
// shows a named BLP as mode 41. Returns 1 at once when mode 41 already
|
||||
// shows `path` (SStrCmpI against VAR_CUSTOM_CURSOR_NAME). Otherwise it
|
||||
// loads the file, and for a 32x32 image stores mode 41, copies the texels
|
||||
// into slot 41 (VAR_CURSOR_IMAGE_CUSTOM), sends the image, copies `path`
|
||||
// into VAR_CUSTOM_CURSOR_NAME (CURSOR_FILE_SIZE bytes) and returns 1; a
|
||||
// file that does not load returns 0 and changes nothing (disassembled
|
||||
// 0x523C40-0x523D0E). ClassifyHoveredGameObject uses it for a
|
||||
// record-named cursor.
|
||||
FUN_SET_CUSTOM_CURSOR = 0x00523C40,
|
||||
VAR_CURSOR_IMAGE_CUSTOM = 0x00C0BD80,
|
||||
VAR_CUSTOM_CURSOR_NAME = 0x00BE2C70,
|
||||
CURSOR_IMAGE_SIZE = 32,
|
||||
// s_cursorNames: `const char *[42]`, mode -> image name (0 and 41 null;
|
||||
// 21..40 carry their own "Unable…" names). The loader formats
|
||||
// `Interface\Cursor\%s.blp` from it (0x5236C0); dumped from the exe.
|
||||
@@ -1967,6 +2026,16 @@ enum Offsets {
|
||||
// 9 / 0x19 and 0x81 after the object's IsMouseoverable answers nonzero.
|
||||
FUN_WORLD_FRAME_CLASSIFY_UNIT = 0x00482200,
|
||||
FUN_WORLD_FRAME_CLASSIFY_CORPSE = 0x00482740,
|
||||
// ClassifyHoveredUnit's NPC flag chain (0x482336-0x4826D6), reached for a
|
||||
// unit the player can interact with now (0x606880) with the unit in ESI,
|
||||
// the player in EDI, and EBX 1 when the unit is beyond the interact reach
|
||||
// (squared distance above 0xB4B32C), else 0. Its head is `mov ecx,
|
||||
// [esi + 0x110]` (8B 8E 10 01 00 00), and no jump in the function lands
|
||||
// inside it. CLASSIFY_UNIT_RETURN is the epilogue after the chain's first
|
||||
// SetCursorMode call (`pop edi; pop esi; pop ebx; mov esp, ebp; pop ebp;
|
||||
// ret 4`), with ESP as it is at the head (disassembled 0x482200-0x4826D6).
|
||||
SITE_CLASSIFY_UNIT_NPC_FLAGS = 0x00482336,
|
||||
CLASSIFY_UNIT_RETURN = 0x00482356,
|
||||
// CGGameObject_C::GetCursor — `uint32_t __thiscall(go)`: 0 when not
|
||||
// interactable, CURSOR_MODE_CUSTOM for a record-named cursor, else the
|
||||
// handler's mode (+20 when not usable). IsUsable — `bool __thiscall(go)`,
|
||||
@@ -1975,6 +2044,17 @@ enum Offsets {
|
||||
// a custom-cursor texture load for 41.
|
||||
FUN_GAMEOBJECT_GET_CURSOR = 0x005F8760,
|
||||
FUN_GAMEOBJECT_IS_USABLE = 0x005F8810,
|
||||
// The handler's mode is its vtable slot 4 (+0x10), which GetCursor
|
||||
// tail-jumps to with the handler in ECX (`jmp [eax+0x10]` at 0x5F878B);
|
||||
// GetCursor is the slot's one reader. BaseHandlerGetCursor — `uint32_t
|
||||
// __thiscall(handler)`, plain ret — is the slot in most types: 0 when slot
|
||||
// 5 says not interactable; a lock's LockType cursor by name; else
|
||||
// CURSOR_MODE_INTERACT when usable (slot 6), its Unable form when not
|
||||
// (disassembled 0x5F3070-0x5F3123). The fishing node (type 17) handler's
|
||||
// vtable is 0x80BC80, whose slot 4 holds it (benilla-re
|
||||
// s_fishingNodeHandlerVtable; xref 0x80BC90).
|
||||
FUN_GO_HANDLER_BASE_GET_CURSOR = 0x005F3070,
|
||||
SLOT_FISHING_NODE_HANDLER_GET_CURSOR = 0x0080BC90,
|
||||
// Object type words (descriptor OBJECT_FIELD_TYPE) the hover dispatch
|
||||
// switches on, exactly (0x4828D0): unit, player, game object, corpse.
|
||||
OBJECT_TYPE_WORD_UNIT = 0x09,
|
||||
@@ -2814,23 +2894,14 @@ enum Offsets {
|
||||
UNIT_POWER_MIN_TYPE = 0,
|
||||
UNIT_POWER_MAX_TYPE = 4, // happiness; types 5/6 only valid post-WotLK
|
||||
|
||||
// Per-power-type display divisor. Raw descriptor values get
|
||||
// divided by this before they're surfaced through Lua. Vanilla
|
||||
// 1.12 stores rage as 0..1000 (internally) and divides by 10 to
|
||||
// show 0..100; happiness is stored at 1000x scale and divides
|
||||
// by 1000. Engine reads from `Script_UnitMana` at
|
||||
// `0x006E7130 + type * 4` (= `0x0086F978`). Same trick the
|
||||
// 3.3.5/4.x clients use; the table is per-client, the design is
|
||||
// stable.
|
||||
//
|
||||
// [0] = 1 MANA
|
||||
// [1] = 10 RAGE
|
||||
// [2] = 1 FOCUS
|
||||
// [3] = 1 ENERGY
|
||||
// [4] = 1000 HAPPINESS
|
||||
VAR_UNIT_POWER_DIVISOR_TABLE = 0x0086F978,
|
||||
// GetPowerDisplayDivisor — `int __fastcall(int powerType)`, the reader of
|
||||
// the table above (1 for a negative type). benilla-re spell symbols.
|
||||
// GetPowerDisplayDivisor — `uint32_t __fastcall(int powerType)`: the
|
||||
// divisor that turns a stored power amount (descriptor POWER/MAXPOWER,
|
||||
// Spell.dbc ManaCost, the cost helper's answer) into displayed units.
|
||||
// Returns `[0x0086F978 + 4·type]`, or 1 for a negative type (verified by
|
||||
// disassembly). Table {1, 10, 1, 1, 1000}: rage is stored in tenths
|
||||
// (Shield Slam's 20 rage is 200), happiness at 1000x. Same table as
|
||||
// 3.3.5's `s_powerDisplayMod`. Read it only through
|
||||
// `Unit::Power::DisplayDivisor`.
|
||||
FUN_POWER_DISPLAY_DIVISOR = 0x006E7130,
|
||||
OFF_UNIT_FIELD_LEVEL = 0x70,
|
||||
// UNIT_FIELD_BYTES_0 dword at +0x78; byte 3 (= +0x7B) is the unit's
|
||||
@@ -3179,13 +3250,6 @@ enum Offsets {
|
||||
// 4.3.4 `GetSpellInfo` reports this instead of the cost (with
|
||||
// isFunnel = true) when it is positive.
|
||||
FUN_GET_SPELL_POWER_PER_SECOND = 0x006E3300,
|
||||
// Divisor that turns a stored power amount into its displayed units:
|
||||
// `__fastcall uint32_t(int powerType)` → `[0x0086F978 + 4·type]`, 1 for a
|
||||
// negative type. Table {1, 10, 1, 1, 1000} — rage is stored in tenths
|
||||
// (Heroic Strike's 15 rage is 150 in Spell.dbc). Verified by disassembly;
|
||||
// same table as 3.3.5's `s_powerDisplayMod`.
|
||||
FUN_GET_POWER_DISPLAY_DIVISOR = 0x006E7130,
|
||||
|
||||
// The engine's spell-castability verdict for the LOCAL player — the
|
||||
// function behind `IsUsableAction` for a player spell slot (the spell
|
||||
// branch of the per-slot recompute `FUN_004E5050`) and behind the
|
||||
@@ -3310,6 +3374,115 @@ enum Offsets {
|
||||
// state locally; a no-op while mounted, on a taxi, dead, channeling or
|
||||
// moving (benilla-re objectlayer/standstate#the-sit-gate).
|
||||
FUN_PLAYER_SEND_STAND_STATE = 0x005ED430,
|
||||
// CGUnit_C::IsShapeShifted — `char __thiscall(const void *unit)`, no
|
||||
// args: the form byte names a SpellShapeshiftForm row without flag bit
|
||||
// 0 (a stance is not a shapeshift). Disassembled.
|
||||
FUN_UNIT_IS_SHAPESHIFTED = 0x0060E9F0,
|
||||
// The two interaction form gates, each `call FUN_UNIT_IS_SHAPESHIFTED`
|
||||
// (ECX = the player) followed by a test for SHAPESHIFT_FORM_FLAG_CAN_-
|
||||
// INTERACT; shifted without it refuses (disassembled):
|
||||
// the NPC right-click (CGPlayer_C::HandleOnRightClick, 0x005F0130):
|
||||
// ERR_CANT_INTERACT_SHAPESHIFTED, before the range check and walk;
|
||||
// the game-object use check (the handler's vtable slot 6, 0x005F3130,
|
||||
// 4.3.4's CGGameObject_C_TypeBase::CanUseNow; every type but door and
|
||||
// chair): error 0x1A0, only for an object with no Lock row.
|
||||
CALL_NPC_INTERACT_IS_SHAPESHIFTED = 0x005F0150,
|
||||
CALL_GAMEOBJECT_CAN_USE_IS_SHAPESHIFTED = 0x005F31E1,
|
||||
// CGUnit_C::CanInteractNow (0x00606880, `this` = the player, ECX = EDI)
|
||||
// has the same form test at this call. Its only caller is the hover
|
||||
// cursor (0x00482310, a .text call scan), which shows the NPC's service
|
||||
// cursor only when it passes; 4.3.4's CursorTrackUnit asks the same.
|
||||
CALL_UNIT_CAN_INTERACT_NOW_IS_SHAPESHIFTED = 0x006068B4,
|
||||
// The use check's mount rung, the same no-Lock-row branch: `mov ecx,
|
||||
// [eax+0x1FC]` (8B 88 FC 01 00 00, the player's mount display), then
|
||||
// `test ecx,ecx; jle pass`, then the mailbox test; refuses 0x19F
|
||||
// (ERR_NOT_WHILE_MOUNTED, no 1.12 text). EDI = the player; EAX is dead.
|
||||
SITE_GAMEOBJECT_CAN_USE_MOUNT_READ = 0x005F31A8,
|
||||
// 4.3.4's IsUsableMounted — `bool __thiscall(const void *const *fields)`,
|
||||
// ECX = &object->descriptor: GAMEOBJECT_TYPE_ID is 19, the mailbox.
|
||||
FUN_GAMEOBJECT_USABLE_MOUNTED = 0x0047CFF0,
|
||||
OFF_GAMEOBJECT_DESCRIPTOR = 0x110,
|
||||
// A type handler's object pointer (benilla-re objectlayer/gameobjects).
|
||||
OFF_GAMEOBJECT_HANDLER_OBJECT = 0x4,
|
||||
// The default use (slot 7, 0x005F33E0, 4.3.4's TypeBase::Use): its
|
||||
// unlocked, no-spell branch sends CMSG_GAMEOBJ_USE, opening with `mov
|
||||
// edi,0x7FF9E4` (BF E4 F9 7F 00, the CDataStore vtable). [EBP-4] = the
|
||||
// handler; EAX, ECX and EDX are dead there.
|
||||
SITE_GAMEOBJECT_USE_SEND = 0x005F35DF,
|
||||
// The two types whose slot 7 replaces the default use (vtable dump):
|
||||
// the mailbox (opens the mail frame) and text (opens the page). Both
|
||||
// `char __thiscall(void *handler)`, no args, return 1.
|
||||
SLOT_GAMEOBJECT_MAILBOX_USE = 0x0080BDBC,
|
||||
FUN_GAMEOBJECT_MAILBOX_USE = 0x005F6820,
|
||||
SLOT_GAMEOBJECT_TEXT_USE = 0x0080B954,
|
||||
FUN_GAMEOBJECT_TEXT_USE = 0x005F58C0,
|
||||
|
||||
// The rest of 4.3.4's CanAutoStand / CanAutoDismount / CanAutoUnshift
|
||||
// sites, each found as the matching 1.12 gate (disassembled):
|
||||
//
|
||||
// Stand-state reads before a loot, `call [reg+0xA4]` (GetStandState, 6
|
||||
// bytes) with ECX = the player, then `test eax,eax; jnz` to
|
||||
// ERR_LOOT_NOTSTANDING (4.3.4 CGUnit_C::OnRightClick / CGCorpse_C::
|
||||
// OnRightClick): the unit right-click's loot step (FF 90 A4 00 00 00)
|
||||
// and the corpse right-click (0x005D6BF0, FF 92 A4 00 00 00).
|
||||
SITE_UNIT_LOOT_STAND_CALL = 0x0060BFB7,
|
||||
SITE_CORPSE_LOOT_STAND_CALL = 0x005D6C3B,
|
||||
// The unit right-click's loot step skips a mounted player: `mov eax,
|
||||
// [ecx+0x1FC]` (8B 81 FC 01 00 00), `test eax,eax; jg skin`. ESI = the
|
||||
// player; ECX and EDX are dead. 4.3.4 has no such test there and its
|
||||
// LootUnit dismounts instead.
|
||||
SITE_UNIT_LOOT_MOUNT_READ = 0x0060BF98,
|
||||
// CGPlayer_C::LootUnit (0x005DF2A0) past its reach / walk-up, before the
|
||||
// loot window close and CMSG_LOOT: `push 0; xor dl,dl; mov cl,1` (6A 00
|
||||
// 32 D2 B1 01) for FUN_0048F200(1, 0, 0). ESI = the player; EAX is
|
||||
// dead. 4.3.4's LootUnit dismounts at this point.
|
||||
SITE_LOOT_UNIT_SEND = 0x005DF3B3,
|
||||
// The item use's form refusal (0x005D8D00, 4.3.4 CGItem_C::Use):
|
||||
// `push 0x15E` (68 5E 01 00 00, ERR_NO_ITEMS_WHILE_SHAPESHIFTED), then
|
||||
// `jmp` to the error tail. ESI = the player; EAX, ECX, EDX are dead.
|
||||
// ITEM_USE_FORM_PASSED is where the form test's pass jumps. Turtle's exe
|
||||
// makes the refusal unreachable (`jmp` over the form test at
|
||||
// 0x005D9139); the stock 1.12.1 exe reaches it.
|
||||
SITE_ITEM_USE_FORM_REFUSAL = 0x005D914D,
|
||||
ITEM_USE_FORM_PASSED = 0x005D9157,
|
||||
// The mount reads in 4.3.4's CGUnit_C::CanAttackNow, inlined in 1.12
|
||||
// (each followed by `test; jg refuse`):
|
||||
// the unit right-click's attack step (8B 82 FC 01 00 00), player ESI,
|
||||
// EAX out, ECX and EDX dead;
|
||||
// the hover cursor's attack cursor (8B 82 FC 01 00 00), player EDI,
|
||||
// EAX out, ECX and EDX dead;
|
||||
// StartAttack (0x005ECB70, 8B 81 FC 01 00 00), player ESI, EAX out,
|
||||
// ECX and EDX dead;
|
||||
// the attack resolver (0x00612DF0, 4.3.4 VerifyAttackIconPressed's
|
||||
// ERR_ATTACK_MOUNTED, 8B 81 FC 01 00 00), player EDI, EAX out;
|
||||
// the resolver's can-attack step (8B 90 FC 01 00 00), player ECX
|
||||
// (live after), EDX out, EAX dead.
|
||||
SITE_RIGHT_CLICK_ATTACK_MOUNT_READ = 0x0060C1B4,
|
||||
SITE_CURSOR_ATTACK_MOUNT_READ = 0x00482659,
|
||||
SITE_START_ATTACK_MOUNT_READ = 0x005ECC06,
|
||||
SITE_ATTACK_RESOLVER_MOUNT_READ = 0x00613039,
|
||||
SITE_ATTACK_RESOLVER_CAN_ATTACK_MOUNT_READ = 0x00613142,
|
||||
// StartAttack once every test passed, before the swing: `mov eax,[esi+
|
||||
// 0xC8C]` (8B 86 8C 0C 00 00), ESI = the player, EDI = the target; ECX
|
||||
// and EDX are dead. START_ATTACK_RETURN is its plain epilogue. 4.3.4's
|
||||
// CombatModeEnter dismounts here.
|
||||
SITE_START_ATTACK_PASSED = 0x005ECC56,
|
||||
START_ATTACK_RETURN = 0x005ECD9A,
|
||||
// ERR_OUT_OF_RANGE's message id (benilla-re tables/messages.tsv).
|
||||
GAME_ERROR_OUT_OF_RANGE = 125,
|
||||
// CGPlayer_C::CanAutoInteract — `bool __thiscall(const void *player)`,
|
||||
// no args: a live active mover with the click-to-move cvar on
|
||||
// (disassembled). 4.3.4's right-click passes it as CombatModeEnter's
|
||||
// `silent`, so the out-of-reach error stays quiet while the player walks.
|
||||
FUN_PLAYER_CAN_AUTO_INTERACT = 0x0060F900,
|
||||
// The taxi map opener (0x004DB710) ends like 4.3.4's: TAXIMAP_OPENED,
|
||||
// then tutorial 34 through FUN_TRIGGER_TUTORIAL (`void __fastcall(int
|
||||
// id, int)`) at this call; 4.3.4 then stands, dismounts and leaves the
|
||||
// form when the map has more than one node (VAR_TAXI_MAP_NODE_COUNT,
|
||||
// the count the opener's TaxiNodeSetCurrent loop runs to).
|
||||
CALL_TAXI_MAP_OPENED_TUTORIAL = 0x004DBAA2,
|
||||
FUN_TRIGGER_TUTORIAL = 0x004B5390,
|
||||
VAR_TAXI_MAP_NODE_COUNT = 0x00BB4AA8,
|
||||
|
||||
// Effective spell/channel duration (ms):
|
||||
// `__fastcall int(const void *spellRecord, int pet /*0 = player,
|
||||
@@ -3949,6 +4122,13 @@ enum Offsets {
|
||||
VTBL_SIMPLEMODEL_UPDATE_MODEL = 0x98,
|
||||
// `CM2Scene *__thiscall(frame)` — the frame's scene, made on first use.
|
||||
FUN_SIMPLEMODEL_GET_SCENE = 0x0076CFC0,
|
||||
// CSimpleModelFFX::SetModelInstance — `__thiscall(frame, model)`, ret 4:
|
||||
// AddRefs the new model, releases the old one, stores it at +0x318.
|
||||
// Slot 36 of the Model and pane vtables; DressUpModel's own slot 36
|
||||
// (0x00504350) calls it first.
|
||||
FUN_SIMPLEMODEL_SET_MODEL_INSTANCE = 0x0076CD30,
|
||||
// The Model frame's per-paint update — `__thiscall(frame)`, slot 38.
|
||||
FUN_SIMPLEMODEL_UPDATE_MODEL = 0x0076D1A0,
|
||||
|
||||
// ---- PlayerModel / DressUpModel / TabardModel (benilla ui/playermodel) --
|
||||
// CGCharacterModelBase: a Model frame (0x3F8 bytes) that shows a copy of a
|
||||
@@ -3967,6 +4147,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,
|
||||
@@ -3975,6 +4157,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
|
||||
@@ -3982,6 +4182,37 @@ enum Offsets {
|
||||
FUN_PLAYERMODEL_CONFIGURE_CAMERA = 0x00505890,
|
||||
// The engine's Lua PlayerModel:SetRotation(angle) — `int __fastcall(L)`.
|
||||
FUN_SCRIPT_PLAYERMODEL_SET_ROTATION = 0x00505F00,
|
||||
// The engine's Lua PlayerModel:SetUnit(unit) — `int __fastcall(L)`,
|
||||
// returns nothing: FUN_TOKEN_TO_GUID, then the SetUnit helper below.
|
||||
// Turtle's client patches its tail (0x00505E19) to also take a number.
|
||||
FUN_SCRIPT_PLAYERMODEL_SET_UNIT = 0x00505D70,
|
||||
// SetUnit helper — `__thiscall(pane, uint64 guid)`, ret 8: stores the
|
||||
// GUID, clears the display record and, if the frame is visible
|
||||
// (OFF_FRAME_VISIBLE), builds the model from the unit (type mask 8).
|
||||
// Its callers: SetUnit, RefreshUnit (0x00505E40), DressUpModel:Dress.
|
||||
FUN_PLAYERMODEL_SET_UNIT_GUID = 0x00505B50,
|
||||
// Hide and Show — `__thiscall(pane)`, slots 33 and 34 of all three pane
|
||||
// vtables. Hide drops the model (slot 36 with null), then jumps to the
|
||||
// base hide. Show, while the frame is not yet visible, builds the model
|
||||
// from the GUID or the display record, then jumps to the base show.
|
||||
FUN_PLAYERMODEL_HIDE = 0x00505CE0,
|
||||
FUN_PLAYERMODEL_SHOW = 0x00505D00,
|
||||
FUN_SIMPLEFRAME_HIDE = 0x0076AD50,
|
||||
FUN_SIMPLEFRAME_SHOW = 0x0076AE10,
|
||||
// The turn latch (SetRotation sets it, the next paint clears it) and the
|
||||
// turn deadline in OS ms (now + 100 on a turn, 0 once Stand is armed).
|
||||
OFF_PLAYERMODEL_TURNING = 0x3E8,
|
||||
OFF_PLAYERMODEL_TURN_DEADLINE = 0x3EC,
|
||||
// UpdateModel — `__thiscall(pane)`, slot 38 (DressUpModel's 0x00504470
|
||||
// calls it first): the base update, then, with no turn latched and the
|
||||
// deadline passed (0 counts as passed), arms Stand (-1, 0, -1, 0, 1.0, 1,
|
||||
// 1) when key bone -1 plays anything else, and zeroes the deadline.
|
||||
// 4.3.4's UpdateModel is the same with the pane's m_sequence and
|
||||
// m_sequenceVariation in place of 0 and -1, and a loaded-model gate.
|
||||
FUN_PLAYERMODEL_UPDATE_MODEL = 0x00505C50,
|
||||
// vtable slot 36: SetModelInstance `__thiscall(frame, model)`; the pane
|
||||
// Hide passes null to drop the model (0x00505CE7).
|
||||
VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE = 0x90,
|
||||
|
||||
// DressUpModel fields (benilla ui/dressup): the character component, and
|
||||
// the main- and off-hand lanes, 8 bytes each: byte 0 item class (0 =
|
||||
@@ -3989,11 +4220,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
|
||||
@@ -4039,6 +4322,11 @@ enum Offsets {
|
||||
OFF_M2MODEL_SHARED = 0x30,
|
||||
OFF_M2SHARED_DATA = 0x130,
|
||||
OFF_M2DATA_CAMERA_ID_LOOKUP = 0x12C,
|
||||
// The sequence count, and the {count, uint16 *} lookup from animation id
|
||||
// to sequence. PlayAnimation (0x007121A0) maps a resolved id through the
|
||||
// lookup and drops it when the index is not below the count.
|
||||
OFF_M2DATA_SEQUENCE_COUNT = 0x1C,
|
||||
OFF_M2DATA_ANIMATION_LOOKUP = 0x24,
|
||||
// SetAnimated — `__thiscall(model, bool)`; M2SceneAnimateAndCollect —
|
||||
// `__thiscall(scene, const float *cameraPos)`. ConfigureCamera runs both
|
||||
// (with the frame's facing zeroed) before it copies a model camera.
|
||||
@@ -5781,6 +6069,37 @@ 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,
|
||||
// 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.
|
||||
VAR_ITEMVISUALS_RECORDS = 0x00C0DB70,
|
||||
VAR_ITEMVISUALS_COUNT = 0x00C0DB74,
|
||||
|
||||
// SpellVisualKit.dbc (35 columns, 0x8C-byte rows). The char-select ghost
|
||||
// kit (0x004727F0) reads the store at 0x004727F6/0x0047280A: nine effect
|
||||
// IDs from +0x0C (attachments 20, 34, 19, 21, 22, 17, 23, 24, 25, pushed
|
||||
// at 0x00472821..0x0047287F), then four procedure kinds from +0x3C with
|
||||
// their first values from +0x4C (the loop at 0x00472888). Kind 1 is a
|
||||
// tint, the value an 0xRRGGBB colour stored as a float; kind 14 an alpha.
|
||||
VAR_SPELLVISUALKIT_RECORDS = 0x00C0D74C,
|
||||
VAR_SPELLVISUALKIT_COUNT = 0x00C0D750,
|
||||
OFF_SPELLVISUALKIT_EFFECTS = 0x0C,
|
||||
OFF_SPELLVISUALKIT_PROC_KIND = 0x3C,
|
||||
OFF_SPELLVISUALKIT_PROC_VALUE = 0x4C,
|
||||
// SpellVisualEffectName.dbc: the model file at +0x08, which the ghost
|
||||
// kit's effect step (0x00472780) creates (0x004727AF).
|
||||
VAR_SPELLVISUALEFFECTNAME_RECORDS = 0x00C0D760,
|
||||
VAR_SPELLVISUALEFFECTNAME_COUNT = 0x00C0D764,
|
||||
OFF_SPELLVISUALEFFECTNAME_FILE = 0x08,
|
||||
|
||||
// ItemSet.dbc — armor/equipment set definitions referenced by
|
||||
// `ItemStats_C::m_itemSet`. Standard 5-DWORD class shape at
|
||||
@@ -5906,6 +6225,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
|
||||
@@ -6275,6 +6601,17 @@ enum Offsets {
|
||||
OFF_UNIT_DESCRIPTOR_RACE_BYTE = 0x78,
|
||||
OFF_UNIT_DESCRIPTOR_CLASS_BYTE = 0x79,
|
||||
OFF_UNIT_DESCRIPTOR_SEX_BYTE = 0x7A,
|
||||
// UNIT_NPC_FLAGS, read at descriptor +0x234 by ClassifyHoveredUnit's flag
|
||||
// chain (0x48233C) and IsQuestGiverToTalk (0x5DF49C).
|
||||
OFF_UNIT_DESCRIPTOR_NPC_FLAGS = 0x234,
|
||||
// CGUnit_C::m_questStatus: the quest giver status the server last sent
|
||||
// (SMSG_QUESTGIVER_STATUS), stored by SetQuestGiverStatus (0x607440) for
|
||||
// a unit with NPC flag 0x2 (benilla-re objectlayer/unitnames).
|
||||
OFF_CGUNIT_QUEST_STATUS = 0xCB8,
|
||||
// IsQuestGiverToTalk — `uint32_t __stdcall(CGUnit_C *unit)`, ret 4: 1 for
|
||||
// NPC flag 0x2 with a quest status other than 0 and 1. ClassifyHoveredUnit
|
||||
// shows Speak for it (disassembled 0x5DF490-0x5DF4C4).
|
||||
FUN_IS_QUEST_GIVER_TO_TALK = 0x005DF490,
|
||||
|
||||
// The local player's UNIT_FIELD_BYTES_0 race/class/sex bytes, mirrored
|
||||
// into a login-time global that is populated at character-enter —
|
||||
@@ -7984,6 +8321,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
|
||||
@@ -8795,8 +9135,32 @@ enum Offsets {
|
||||
LUA_GET_STACK = 0x006FBAA0,
|
||||
LUA_GET_INFO = 0x006FBC70,
|
||||
SIZEOF_LUA_DEBUG = 0x60,
|
||||
OFF_LUA_DEBUG_NAME = 0x04,
|
||||
OFF_LUA_DEBUG_SOURCE = 0x10,
|
||||
OFF_LUA_DEBUG_CURRENTLINE = 0x14,
|
||||
OFF_LUA_DEBUG_LINEDEFINED = 0x1C,
|
||||
// `lua_getlocal(L, ar, n)` — `const char *__fastcall` (n on the stack):
|
||||
// pushes local n of the call level ar names (ar+0x5C, i_ci) and returns
|
||||
// its name, or returns null and pushes nothing (no such local, or a C
|
||||
// level). No engine callers.
|
||||
LUA_GET_LOCAL = 0x006FBB20,
|
||||
// `lua_getupvalue(L, funcindex, n)` — `const char *__fastcall` (n on the
|
||||
// stack): pushes upvalue n of the function at funcindex and returns its
|
||||
// name ("" for a C closure), or null for no such upvalue or a value that
|
||||
// is not a function. (0x006F45B0 is lua_pushupvalues.)
|
||||
LUA_GET_UPVALUE = 0x006F4660,
|
||||
// `debugstack([start [, count1 [, count2]]])` — the engine's C function
|
||||
// (base-function table 0x811E28); no engine callers. It reads the stack
|
||||
// of the state it is called on through five lua_getstack calls and one
|
||||
// lua_getinfo("Snl"), and pushes its text onto that same state.
|
||||
// BaseLib::DebugStack points those six calls at a thread's state.
|
||||
FUN_SCRIPT_DEBUGSTACK = 0x00703760,
|
||||
CALL_DEBUGSTACK_GETSTACK_FIRST = 0x0070383F,
|
||||
CALL_DEBUGSTACK_GETSTACK_TAIL_PROBE = 0x0070386E,
|
||||
CALL_DEBUGSTACK_GETSTACK_TAIL_START = 0x00703896,
|
||||
CALL_DEBUGSTACK_GETSTACK_TAIL_SKIP = 0x007038AD,
|
||||
CALL_DEBUGSTACK_GETSTACK_NEXT = 0x00703998,
|
||||
CALL_DEBUGSTACK_GETINFO = 0x007038CD,
|
||||
// `luaO_chunkid(out, source, bufflen)` — `void __fastcall(char *out /*ecx*/,
|
||||
// const char *source /*edx*/, int bufflen /*stack*/)`, RET 4: the name a
|
||||
// chunk has in error messages. `=name` is copied as is, `@file` is the
|
||||
@@ -9264,9 +9628,21 @@ enum Offsets {
|
||||
FUN_BLP_DECODE_LEVEL = 0x005A7F00,
|
||||
FUN_BLP_FREE_SCRATCH = 0x005A8100,
|
||||
FUN_BLP_RESET = 0x005A7AA0,
|
||||
// Parse(this, const void *file) → nonzero for a 'BLP2' type-1 header
|
||||
// (ret 4): Open's second half. It copies BLP_HEADER_SIZE bytes of `file`
|
||||
// into the record and keeps `file` as the pixel source (+0x498), which
|
||||
// Reset only drops (it frees the decoded chain at +0x00), so a buffer the
|
||||
// caller owns decodes like the one Open reads (disassembled
|
||||
// 0x5A83B0-0x5A842A).
|
||||
FUN_BLP_PARSE = 0x005A83B0,
|
||||
BLP_HEADER_SIZE = 0x494,
|
||||
BLP_RECORD_SIZE = 0x4B4,
|
||||
OFF_BLP_MAGIC = 0x04,
|
||||
OFF_BLP_VERSION = 0x08,
|
||||
// byte: 1 palette, 2 DXT, 3 raw BGRA (DecodeLevel's switch, 0x5A7F6B)
|
||||
OFF_BLP_COMPRESSION = 0x0C,
|
||||
// byte: for DXT, 0 DXT1, 1 DXT3, 7 DXT5 (DecodeLevel takes its class
|
||||
// at 0x5A7FC3)
|
||||
OFF_BLP_ALPHA_TYPE = 0x0E,
|
||||
OFF_BLP_WIDTH = 0x10,
|
||||
OFF_BLP_HEIGHT = 0x14,
|
||||
@@ -9276,6 +9652,15 @@ enum Offsets {
|
||||
// benilla image/convert#palette-to-bgra). GetPortraitAlphaMask decodes
|
||||
// to this format.
|
||||
GX_TEX_FORMAT_BGRA8888 = 2,
|
||||
// DecodeLevel converts a DXT level to another format through the matrix
|
||||
// InitConvertMatrix (0x005A4FC0) fills, by the class of the alpha type.
|
||||
// It fills DXT1 and DXT3 to BGRA8888 (cells 0x00C0F840, 0x00C0F8D0) and
|
||||
// leaves DXT5's (0x00C0F960) empty, so a DXT5 level gets an unwritten
|
||||
// SMemAlloc buffer and DecodeLevel still returns 1 (disassembled
|
||||
// 0x5A4FC0-0x5A5062 and 0x5A7FC3-0x5A8045; benilla
|
||||
// image/convert#conversion-matrix).
|
||||
BLP_COMPRESSION_DXT = 2,
|
||||
BLP_ALPHA_TYPE_DXT5 = 7,
|
||||
|
||||
// MPQ file enumerator — walks every mounted archive's `(listfile)`
|
||||
// and invokes a callback for each entry whose path begins with a
|
||||
@@ -10429,6 +10814,10 @@ enum Offsets {
|
||||
// descriptor — though we only expose `IsMounted()` for the
|
||||
// player to match modern API semantics.
|
||||
OFF_UNIT_FIELD_MOUNTDISPLAYID = 0x1FC,
|
||||
// UNIT_FIELD_DISPLAYID: the display the unit shows now (a shapeshift
|
||||
// form's while shapeshifted). Field 0x7D in the update-field name table
|
||||
// (0x0083A73C).
|
||||
OFF_UNIT_FIELD_DISPLAYID = 0x1F4,
|
||||
|
||||
// Local player movement-flags u32. Lives directly on the
|
||||
// CGPlayer object (not the broadcast descriptor) — this is
|
||||
@@ -10989,6 +11378,13 @@ enum Offsets {
|
||||
// event-driven (state changes only), not per-frame, so per-call
|
||||
// overhead from a hooked predicate is negligible.
|
||||
FUN_FIRE_EVENT_NO_ARGS = 0x00703E50,
|
||||
// The enter cascade's (0x004908C0) `mov ecx, 0x10E; call
|
||||
// FUN_FIRE_EVENT_NO_ARGS`: PLAYER_LOGIN, the only place it fires. It runs
|
||||
// only while the arm byte 0x00B4E260 is set, which the interface
|
||||
// initializer (0x0048FBF0) sets after the addons load and this path
|
||||
// clears, so it fires once per UI instance: at login and after a reload
|
||||
// (benilla ui/worldenter). E8 call, disassembled.
|
||||
CALL_ENTER_WORLD_FIRE_PLAYER_LOGIN = 0x00490959,
|
||||
|
||||
// `__fastcall(int sendRelease, char earlyReturnOnGameObject,
|
||||
// char showError)` — engine's "close the current loot window"
|
||||
@@ -11629,6 +12025,10 @@ enum Offsets {
|
||||
VAR_SPELL_SHAPESHIFT_FORM_COUNT = 0x00C0D778,
|
||||
OFF_SPELL_SHAPESHIFT_FORM_FLAGS = 0x2C,
|
||||
SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT = 0x100,
|
||||
// Flag 0x8: the form may talk to NPCs and use objects (Shadowform's 0x9).
|
||||
// Tested by both interaction gates (`test byte [eax+0x2C],0x8` at
|
||||
// 0x005F017F / 0x005F3210) and 4.3.4's HandleOnRightClick / CanUseNow.
|
||||
SHAPESHIFT_FORM_FLAG_CAN_INTERACT = 0x8,
|
||||
|
||||
// Control-loss aura effect types (EffectApplyAuraName values), used by
|
||||
// C_LossOfControl to classify a debuff. Values are authoritative from the
|
||||
@@ -12611,6 +13011,120 @@ enum Offsets {
|
||||
// the per-stage applies clamp against (FUN_005a29d0/FUN_005a2b40).
|
||||
VAR_GX_DEVICE = 0x00C0ED38,
|
||||
OFF_GXDEV_STAGE_CAP = 0x23C,
|
||||
// ---- Unit portrait bake (BakePortrait 0x00524F60, benilla ui/portraits)
|
||||
// Every entry below is a call the bake makes, read off its disassembly;
|
||||
// calling convention from the call site. `Model::Portrait` replays the
|
||||
// bake on a display model of its own.
|
||||
// M2CreateScene — `void *()`: a fresh scene on the model cache.
|
||||
FUN_M2_CREATE_SCENE = 0x00706E10,
|
||||
// `__thiscall(scene)`: drops one scene reference; at zero the destructor
|
||||
// releases every instance on the scene's model list.
|
||||
FUN_M2SCENE_RELEASE = 0x00707320,
|
||||
// CreateModelFromPath — `void *__thiscall(scene, const char *path,
|
||||
// uint32 flags)`, `ret 8`: a new instance of the file's shared model
|
||||
// (Spells\ErrorCube.mdx when the file is missing). AttachDisplayModel
|
||||
// passes the CreatureModelData path and 0. The caller owns one reference.
|
||||
FUN_M2SCENE_CREATE_MODEL = 0x00707350,
|
||||
// CreateModelFromModel — `void *__thiscall(scene, source, uint32 flags)`:
|
||||
// a new instance of the source's shared model, with its attachments.
|
||||
FUN_M2SCENE_COPY_MODEL = 0x00707400,
|
||||
// M2SceneDraw — `__thiscall(scene, pass)`; the bake draws 0 then 1.
|
||||
FUN_M2SCENE_DRAW = 0x00708900,
|
||||
// `__fastcall(model)`: drops one instance reference (HandleClose2).
|
||||
FUN_MODEL_INSTANCE_RELEASE = 0x007103A0,
|
||||
// CM2Model::IsReady — `__thiscall(model, wait, recurseChildren)`:
|
||||
// loaded AND every texture has its device texture. `wait` blocks on the
|
||||
// reads and creates the device textures (GetTextureDeviceObject(.., 1));
|
||||
// the bake's gate passes (0, 1), the bake itself (1, 1).
|
||||
FUN_MODEL_INSTANCE_READY = 0x00710450,
|
||||
// CM2Model::SetVisible — `__thiscall(model, bool)`.
|
||||
FUN_M2MODEL_SET_VISIBLE = 0x00710C50,
|
||||
// CM2Model::SetAlpha — `__thiscall(model, float)`; SetDiffuseColor —
|
||||
// `__thiscall(model, const float rgb[3])`. Plain stores, which the
|
||||
// ghost kit applies (0x004728C1, 0x00472939). Attached children take
|
||||
// them through the per-frame combine.
|
||||
FUN_M2MODEL_SET_ALPHA = 0x00710CB0,
|
||||
FUN_M2MODEL_SET_DIFFUSE_COLOR = 0x00710CF0,
|
||||
// CM2Model::HasAttachment — `__thiscall(model, id)`, ret 4. Waits for
|
||||
// the model file (0x00710520) when it has not loaded.
|
||||
FUN_M2MODEL_HAS_ATTACHMENT = 0x00712CB0,
|
||||
// CM2Model::Attach — `__thiscall(child, parent, id)`, ret 8. The
|
||||
// parent takes a reference; the attach index resolves once the parent
|
||||
// loads.
|
||||
FUN_M2MODEL_ATTACH = 0x00712F70,
|
||||
// CM2Model::Detach — `__thiscall(child)`: unlinks the child from its
|
||||
// parent, clears its parent (OFF_M2MODEL_PARENT) and drops the parent's
|
||||
// reference. The destructor (0x0070E3F5) detaches every child this way.
|
||||
FUN_M2MODEL_DETACH = 0x00713020,
|
||||
OFF_M2MODEL_PARENT = 0x1CC,
|
||||
// CM2Model::HasLiveParticles — `__thiscall(model)`: +0x3D8, which
|
||||
// Animate sets while an emitter emits or holds a live particle. The
|
||||
// dying-effect drain (0x0061F6CC) releases a stopped effect once false.
|
||||
FUN_M2MODEL_HAS_LIVE_PARTICLES = 0x007135C0,
|
||||
// CM2Model::PlayAnimation — `__thiscall(model, 7 args)`, `ret 0x1C`. The
|
||||
// bake passes (-1, 0, 0, 0, 1.0f, 0, 1).
|
||||
FUN_M2MODEL_PLAY_ANIMATION = 0x007121A0,
|
||||
// CM2Model::GetKeyBoneAnimId — `__thiscall(model, keyBone)`, ret 4: the id
|
||||
// last requested on the key bone (bone state +0xF8, the id as given, not
|
||||
// its fallback). Blocks on the shared data when the model is not loaded.
|
||||
FUN_M2MODEL_GET_KEY_BONE_ANIM = 0x00712090,
|
||||
// The bone states, 0x118 bytes each, at [model+0x90]; key bone -1 is bone
|
||||
// 0 (GetKeyBoneAnimId above). PlayAnimation fills a bone's primary slot
|
||||
// (+0x98): the offset at +0xB8, where the sequence time is start +
|
||||
// (ftol((now - startStamp) * speed) + offset) mod length, and the done
|
||||
// latch at +0xC0, which it clears. Update (0x00719370) queues the done
|
||||
// callback once when the bone's window reaches its end stamp (+0xAC) and
|
||||
// the latch is 0, and sets the latch. A speed of 0 makes the end stamp
|
||||
// the start stamp, so done would fire on the next update.
|
||||
OFF_M2MODEL_BONE_STATES = 0x90,
|
||||
OFF_M2BONE_SEQUENCE_OFFSET = 0xB8,
|
||||
OFF_M2BONE_DONE_REPORTED = 0xC0,
|
||||
// ApplyCreatureDisplay — `__fastcall(model, displayRec, modelDataRec)`,
|
||||
// `ret 4`: the CreatureDisplayInfo row's three skins as the model's
|
||||
// replaceable textures 11..13 (AttachDisplayModel 0x00505AE6).
|
||||
FUN_APPLY_CREATURE_DISPLAY = 0x004797B0,
|
||||
// DetachAllItemModels — `__fastcall(model)`: the bake strips the copy's
|
||||
// attachments (helm, shoulders, held items) before it draws.
|
||||
FUN_DETACH_ALL_ITEM_MODELS = 0x0047A230,
|
||||
// CameraSetDeviceTransforms — `__fastcall(camera, const float rect[4],
|
||||
// int)`: sets the device projection and view from the camera over the
|
||||
// rect; the bake passes {0, 0, 1, aspect} and 0.
|
||||
FUN_CAMERA_SET_DEVICE_TRANSFORMS = 0x007ADA40,
|
||||
// PortraitLighting — the bake's lighting callback (ambient 0.45 grey and a
|
||||
// white light along (-1, 0, -1)), set with FUN_MODEL_INSTANCE_SET_RENDER_CB
|
||||
// and a null argument.
|
||||
FUN_PORTRAIT_LIGHTING = 0x00525B10,
|
||||
// GetPortraitAlphaMask — `void *__fastcall(uint32 size)`: an array whose
|
||||
// byte data (pointer at +8) is the round mask's alpha, one per texel.
|
||||
FUN_PORTRAIT_ALPHA_MASK = 0x005258A0,
|
||||
// GxCapsScreenSize — `__fastcall(float rect[4])`, {top, left, bottom,
|
||||
// right}; see FUN_GX_CAPS_WINDOW_SIZE.
|
||||
FUN_GX_CAPS_SCREEN_SIZE = 0x0058A250,
|
||||
// The bake's device gate: `int ()` each. WINDOW_ACTIVE reads the device's
|
||||
// active flag (+0xF30, cleared while minimised), DEVICE_ALIVE reads
|
||||
// OFF_GXDEVD3D_ALIVE (+0xF2C).
|
||||
FUN_GX_WINDOW_ACTIVE = 0x0058A260,
|
||||
FUN_GX_DEVICE_ALIVE = 0x0058A280,
|
||||
// GxMasterEnable get / set — `bool __fastcall(cap)` / `__fastcall(cap,
|
||||
// value)`. The bake saves cap 1 (fog), turns it off, and restores it.
|
||||
FUN_GX_MASTER_ENABLE_GET = 0x0058A210,
|
||||
FUN_GX_MASTER_ENABLE_SET = 0x0058A1E0,
|
||||
GX_MASTER_FOG = 1,
|
||||
// SetClearColorDevice — `__stdcall(uint32 argb)`; the bake sets 0.
|
||||
FUN_GX_SET_CLEAR_COLOR = 0x0058A920,
|
||||
// ClearDevice — `__fastcall(uint32 mask)`; the bake clears 3 before the
|
||||
// draw and again after the readback.
|
||||
FUN_GX_CLEAR = 0x0058A970,
|
||||
// `__fastcall(const int rect[4], GrowableArray<uint32> *out)`: reads a
|
||||
// back-buffer rectangle (device slot +0x48); the bake reads {0,0,64,64}.
|
||||
FUN_GX_READ_PIXELS = 0x00589CF0,
|
||||
// GxTexMarkRectDirty — `__fastcall(CGxTex *, const int rect[4], uint32
|
||||
// immediate)`, `ret 4`: re-latches a callback texture's texels; the bake's
|
||||
// re-draw of a cached portrait uploads its new pixels with immediate 1.
|
||||
FUN_GX_TEX_MARK_RECT_DIRTY = 0x0058ACD0,
|
||||
// CSimpleTexture::SetImage — `__thiscall(texture, HTEXTURE)`, `ret 4`:
|
||||
// binds a texture handle (taking a reference) and notifies the layer.
|
||||
FUN_SIMPLETEXTURE_SET_IMAGE = 0x00770300,
|
||||
// The vertex-stream primitive every textured region draws through
|
||||
// (FUN_0076fb00 calls it per region). Decompiler signature (13 params):
|
||||
// (count, positions, posStride, s3, s3Stride, colors, colorStride, drop8,
|
||||
|
||||
+8
-23
@@ -65,6 +65,7 @@
|
||||
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
@@ -103,26 +104,12 @@ constexpr const char *kDevMarkerFile = ".classicapi-dev";
|
||||
// paths are case-insensitive.
|
||||
constexpr const char *kAddOnsPrefix = "Interface\\AddOns\\";
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// `path` with the case-insensitive `prefix` stripped, NULL when it does
|
||||
// not start with it.
|
||||
const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
while (*prefix) {
|
||||
if (*path == '\0') return nullptr;
|
||||
if (NormalizeChar(*path) != NormalizeChar(*prefix)) return nullptr;
|
||||
if (Common::FoldPathChar(*path) != Common::FoldPathChar(*prefix)) return nullptr;
|
||||
++path; ++prefix;
|
||||
}
|
||||
return path;
|
||||
@@ -130,7 +117,7 @@ const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
|
||||
int IndexOfAddon(const char *name) {
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
if (PathEqualsCI(name, kAddons[i]))
|
||||
if (Common::SamePath(name, kAddons[i]))
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
@@ -144,7 +131,7 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
if (rel == nullptr) return -1;
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
const char *rest = StripPrefixCI(rel, kAddons[i]);
|
||||
if (rest != nullptr && NormalizeChar(*rest) == '\\') {
|
||||
if (rest != nullptr && Common::FoldPathChar(*rest) == '\\') {
|
||||
*relPath = rel;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
@@ -155,12 +142,10 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
// The embedded bytes for manifest path `relPath`; `data` is NULL when it
|
||||
// is not embedded.
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::File(Manifest::kFiles[i].resourceId,
|
||||
Manifest::kFiles[i].index);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
const Manifest::File *file = Embed::FindFile(Manifest::kFiles, relPath);
|
||||
if (file == nullptr)
|
||||
return {nullptr, 0};
|
||||
return Embed::File(file->resourceId, file->index);
|
||||
}
|
||||
|
||||
// Engine file I/O + Storm allocator — see addons/EngineIO.h for the shapes and
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
// 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/>.
|
||||
|
||||
// `debuglocals([level [, skipFunctionsAndUserdata]])` — the locals and then
|
||||
// the upvalues of a call level as text, one `name=value` line each, for an
|
||||
// error report. 4.3.4's Script_debuglocals and the FrameScript_PackVariable
|
||||
// it formats with, in retail's spacing (probed: `n=7`, `s="hi"`,
|
||||
// `t=<table>{`, ` k=1`, `}`; 4.3.4 writes `n = 7` and `t = <table> {`):
|
||||
// - Only while an error handler runs; otherwise it returns nothing. 4.3.4
|
||||
// tests a flag its error function sets around the handler call. 1.12's
|
||||
// error function sets none, but while a handler runs that function's
|
||||
// level is on the stack (ErrorCallstack::ErrorFunctionLevel).
|
||||
// - `level` counts as lua_getstack and debugstack do: 1 is the caller.
|
||||
// Less than 1 is 1.
|
||||
// - A table prints as `name=<table>{`, or with the frame's name for a
|
||||
// frame (t[0], as FrameScript_GetObject reads it), then its fields one
|
||||
// space in at the top level only, then `}`. A function prints where it
|
||||
// is defined. The text stops at 4095 bytes, each line at 1023.
|
||||
// Retail's second argument, which its Blizzard_ScriptErrors passes as true,
|
||||
// leaves out the function and userdata values, at the top level and in
|
||||
// tables.
|
||||
//
|
||||
// A number key in int range prints truncated with %d, as 4.3.4's
|
||||
// IsIntegerNumber tests only the range: 2.5 prints as 2. Two departures from
|
||||
// 4.3.4, both where it would fail inside the handler: a number key outside
|
||||
// that range is formatted from a copy, not converted in place (that breaks
|
||||
// lua_next), and a key that is not a number or string prints as its type in
|
||||
// angle brackets (4.3.4 formats a null string).
|
||||
//
|
||||
// Retail also lists `(*temporary)` lines: 5.1's lua_getlocal names each stack
|
||||
// slot past a level's locals so, a C level's arguments included (an
|
||||
// `error("x")` report shows `(*temporary)="x"`). 1.12's lua_getlocal
|
||||
// (0x6FBB20) is 5.0's, which names declared locals only. Those slots are
|
||||
// the compiler's registers, which 5.0 and 5.1 allocate differently, so they
|
||||
// are left out rather than imitated.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace BaseLib::DebugLocals {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
using lua_getlocal_t = const char *(__fastcall *)(void *L, const void *ar, int n);
|
||||
using lua_getupvalue_t = const char *(__fastcall *)(void *L, int funcindex, int n);
|
||||
|
||||
const auto GetLocal = reinterpret_cast<lua_getlocal_t>(Offsets::LUA_GET_LOCAL);
|
||||
const auto GetUpvalue = reinterpret_cast<lua_getupvalue_t>(Offsets::LUA_GET_UPVALUE);
|
||||
|
||||
constexpr size_t kTextMax = 4095; // 4.3.4's 4096-byte buffer, less its NUL
|
||||
constexpr size_t kLineSize = 1024;
|
||||
|
||||
struct Packer {
|
||||
void *L;
|
||||
bool skipFunctionsAndUserdata;
|
||||
std::string text;
|
||||
|
||||
void Line(const char *fmt, const char *indent, const char *name, const char *value = "",
|
||||
int number = 0) {
|
||||
char line[kLineSize];
|
||||
std::snprintf(line, sizeof line, fmt, indent, name, value, number);
|
||||
const size_t room = kTextMax - text.size();
|
||||
text.append(line, std::min(room, std::strlen(line)));
|
||||
}
|
||||
|
||||
// The value on the stack top, as 4.3.4's FrameScript_PackVariable.
|
||||
void Pack(const char *name, int depth) {
|
||||
const char *indent = depth > 0 ? " " : "";
|
||||
switch (Lua::Type(L, -1)) {
|
||||
case Lua::TYPE_NIL:
|
||||
Line("%s%s=nil\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_BOOLEAN:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToBoolean(L, -1) ? "true" : "false");
|
||||
break;
|
||||
case Lua::TYPE_LIGHTUSERDATA:
|
||||
case Lua::TYPE_USERDATA:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
Line("%s%s=<userdata>\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_NUMBER:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToString(L, -1)); // a copy: safe to convert
|
||||
break;
|
||||
case Lua::TYPE_STRING:
|
||||
Line("%s%s=\"%s\"\n", indent, name, Lua::ToString(L, -1));
|
||||
break;
|
||||
case Lua::TYPE_TABLE:
|
||||
PackTable(name, depth, indent);
|
||||
break;
|
||||
case Lua::TYPE_FUNCTION:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
PackFunction(name, indent);
|
||||
break;
|
||||
default:
|
||||
Line("%s%s=<%s>\n", indent, name, Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PackTable(const char *name, int depth, const char *indent) {
|
||||
void *object = Lua::ResolveObject(L, -1);
|
||||
Line("%s%s=%s{\n", indent, name, object ? Lua::ObjectDisplayName(object) : "<table>");
|
||||
if (depth == 0) {
|
||||
Lua::PushNil(L);
|
||||
while (Lua::Next(L, -2) != 0) {
|
||||
char key[32];
|
||||
const char *keyName = key;
|
||||
const int keyType = Lua::Type(L, -2);
|
||||
const double number = keyType == Lua::TYPE_NUMBER ? Lua::ToNumber(L, -2) : 0;
|
||||
if (keyType == Lua::TYPE_NUMBER && std::isfinite(number) && number >= -2147483648.0 &&
|
||||
number <= 2147483647.0) {
|
||||
std::snprintf(key, sizeof key, "%d", static_cast<int>(number));
|
||||
} else if (keyType == Lua::TYPE_STRING) {
|
||||
keyName = Lua::ToString(L, -2);
|
||||
} else if (keyType == Lua::TYPE_NUMBER) {
|
||||
Lua::PushValue(L, -2);
|
||||
std::snprintf(key, sizeof key, "%s", Lua::ToString(L, -1));
|
||||
Lua::SetTop(L, -2);
|
||||
} else {
|
||||
std::snprintf(key, sizeof key, "<%s>", Lua::TypeName(L, keyType));
|
||||
}
|
||||
Pack(keyName, 1);
|
||||
Lua::SetTop(L, -2); // the value; the key stays for lua_next
|
||||
}
|
||||
}
|
||||
Line("%s}\n", indent, "");
|
||||
}
|
||||
|
||||
void PackFunction(const char *name, const char *indent) {
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::GetInfo(L, ">nS", ar); // pops the copy
|
||||
const auto field = [&ar](uintptr_t offset) { return Game::Read<const char *>(ar, offset); };
|
||||
const char *source = field(Offsets::OFF_LUA_DEBUG_SOURCE);
|
||||
const int line = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_LINEDEFINED);
|
||||
if (const char *fn = field(Offsets::OFF_LUA_DEBUG_NAME)) {
|
||||
char named[kLineSize];
|
||||
std::snprintf(named, sizeof named, "%s() defined %s", fn, source ? source : "?");
|
||||
Line("%s%s=%s:%d\n", indent, name, named, line);
|
||||
} else {
|
||||
Line("%s%s=<function> defined %s:%d\n", indent, name, source ? source : "?", line);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int __fastcall Script_debuglocals(void *L) {
|
||||
if (ErrorCallstack::ErrorFunctionLevel(L) == 0)
|
||||
return 0;
|
||||
int level = static_cast<int>(Lua::ToNumber(L, 1));
|
||||
if (level <= 0)
|
||||
level = 1;
|
||||
Packer packer{L, Lua::ToBoolean(L, 2) != 0, {}};
|
||||
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
if (Lua::GetStack(L, level, ar)) {
|
||||
const char *name;
|
||||
for (int i = 1; (name = GetLocal(L, ar, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::GetInfo(L, "f", ar); // the level's function
|
||||
for (int i = 1; (name = GetUpvalue(L, -1, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::PushLString(L, packer.text.data(), static_cast<unsigned int>(packer.text.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDebuglocalsArgs[] = {
|
||||
Game::Doc::Opt("level", "number", "1", "The call level, as debugstack counts it."),
|
||||
Game::Doc::Opt("skipFunctionsAndUserdata", "boolean", "false",
|
||||
"Leaves out function and userdata values."),
|
||||
};
|
||||
const Game::Doc::Field kDebuglocalsRets[] = {
|
||||
Game::Doc::Opt("locals", "string", nullptr,
|
||||
"One `name=value` line for each local and upvalue. Nothing outside an "
|
||||
"error handler."),
|
||||
};
|
||||
const Game::Doc::Function kDebuglocals{
|
||||
"The local variables and upvalues of a call level as text, for an error report.",
|
||||
kDebuglocalsArgs, kDebuglocalsRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugLocals
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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/>.
|
||||
|
||||
// `debugstack([thread,] [start [, count1 [, count2]]])` — the stack of a
|
||||
// coroutine, as 4.3.4's Script_debugstack takes it: with a thread first,
|
||||
// the levels are that thread's and `start` defaults to 0 (the thread's
|
||||
// innermost level) instead of 1.
|
||||
//
|
||||
// 1.12's debugstack (FUN_SCRIPT_DEBUGSTACK) has no thread argument, and a
|
||||
// thread falls through its argument checks so that it returns the thread
|
||||
// itself: `msg .. debugstack(co)` raises "attempt to concatenate a thread
|
||||
// value" in the coroutine error paths of WeakAuras, Questie's ThreadLib,
|
||||
// MRT and Zygor.
|
||||
//
|
||||
// The engine's function still does the work, so its text and its head and
|
||||
// tail windows stay exactly as they are. It reads levels through six calls
|
||||
// (five lua_getstack, one lua_getinfo), and those calls are redirected to
|
||||
// read from the state Script_debugstack names for the call: the thread, or
|
||||
// the calling state when there is none. Script_debugstack replaces the
|
||||
// global and is the engine function's only way in, so the state is set for
|
||||
// every call; an error part way cannot leave a thread behind for the next.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace BaseLib::DebugStack {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// The state whose levels the engine's debugstack reads, or null for its own.
|
||||
void *g_levelsOf = nullptr;
|
||||
|
||||
int __fastcall GetStack(void *L, int level, void *ar) {
|
||||
return Lua::GetStack(g_levelsOf ? g_levelsOf : L, level, ar);
|
||||
}
|
||||
|
||||
int __fastcall GetInfo(void *L, const char *what, void *ar) {
|
||||
return Lua::GetInfo(g_levelsOf ? g_levelsOf : L, what, ar);
|
||||
}
|
||||
|
||||
int __fastcall Script_debugstack(void *L) {
|
||||
const auto engineDebugStack =
|
||||
reinterpret_cast<Lua::CFunction>(Offsets::FUN_SCRIPT_DEBUGSTACK);
|
||||
g_levelsOf = nullptr;
|
||||
if (Lua::Type(L, 1) == Lua::TYPE_THREAD) {
|
||||
g_levelsOf = Lua::ToThread(L, 1);
|
||||
Lua::Remove(L, 1);
|
||||
if (!Lua::IsNumber(L, 1)) { // start defaults to 0 for a thread
|
||||
Lua::PushNumber(L, 0);
|
||||
Lua::Insert(L, 1);
|
||||
}
|
||||
}
|
||||
const int results = engineDebugStack(L);
|
||||
g_levelsOf = nullptr;
|
||||
return results;
|
||||
}
|
||||
|
||||
void Apply() {
|
||||
const auto getStack = reinterpret_cast<const void *>(&GetStack);
|
||||
for (const uintptr_t site :
|
||||
{Offsets::CALL_DEBUGSTACK_GETSTACK_FIRST, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_PROBE,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_START, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_SKIP,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_NEXT})
|
||||
Common::RedirectCall(site, Offsets::LUA_GET_STACK, getStack);
|
||||
Common::RedirectCall(Offsets::CALL_DEBUGSTACK_GETINFO, Offsets::LUA_GET_INFO,
|
||||
reinterpret_cast<const void *>(&GetInfo));
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Apply();
|
||||
Lua::RegisterGlobalFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Apply();
|
||||
Lua::RegisterGlueFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugStack
|
||||
@@ -40,12 +40,26 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
int ErrorFunctionLevel(void *L) {
|
||||
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
|
||||
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
|
||||
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (found)
|
||||
return level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> g_setHeight;
|
||||
@@ -59,21 +73,6 @@ int Height(void *L) {
|
||||
return height;
|
||||
}
|
||||
|
||||
// The innermost level whose function is the engine's error function, or 0
|
||||
// when no error is being reported.
|
||||
int ErrorFunctionLevel(void *L) {
|
||||
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
|
||||
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
|
||||
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (found)
|
||||
return level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCallstackHeight(void *L) {
|
||||
Game::Lua::PushNumber(L, Height(L));
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
// The innermost call level whose function is the engine's error function
|
||||
// (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER), or 0 when no error is being
|
||||
// reported. Nonzero exactly while an error handler runs.
|
||||
int ErrorFunctionLevel(void *L);
|
||||
|
||||
} // namespace BaseLib::ErrorCallstack
|
||||
+2
-15
@@ -47,6 +47,7 @@
|
||||
// section's header follows.
|
||||
|
||||
#include "Inject.h"
|
||||
#include "Common.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include "embed/Resource.h"
|
||||
@@ -212,20 +213,6 @@ const char *FindSplicePosition(const char *content, size_t size,
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a != '\0' && *b != '\0') {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
|
||||
@@ -233,7 +220,7 @@ using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original) {
|
||||
if (path == nullptr || !PathEqualsCI(path, kFrameXMLBindingsPath))
|
||||
if (path == nullptr || !Common::SamePath(path, kFrameXMLBindingsPath))
|
||||
return false;
|
||||
|
||||
// Pull the real file through the original reader with the caller's
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
@@ -684,9 +685,7 @@ void SetItem(Entry &e, int itemId) {
|
||||
}
|
||||
|
||||
int DisplayAmount(int amount, int powerType) {
|
||||
using Divisor_t = int(__fastcall *)(int powerType);
|
||||
const int divisor = reinterpret_cast<Divisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(powerType);
|
||||
const int divisor = static_cast<int>(Unit::Power::DisplayDivisor(powerType));
|
||||
return divisor != 0 ? amount / divisor : amount;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The cursor modes later clients added, shown on 1.12's cursor, and the
|
||||
// `expandedCursors` cvar that turns them on (0 by default: they change what
|
||||
// 1.12 shows).
|
||||
//
|
||||
// The engine's cursor is a table of 42 images of 32x32, one per mode;
|
||||
// SendCursorImage copies slot VAR_CURSOR_MODE to the device. SetCursorMode
|
||||
// stores the mode it is given and sends, so a mode past 41 must never reach
|
||||
// it. Slot 41 is how the engine shows an image outside the table:
|
||||
// SetCustomCursor loads a named BLP into it, stores mode 41, sends it and
|
||||
// keeps the name, and returns at once while that name is already showing. The
|
||||
// game object classifier uses it for a record-named cursor.
|
||||
//
|
||||
// Show() calls SetCustomCursor with the mode's path, so a file of that name (a
|
||||
// patch MPQ, a loose file) wins, and a repeat each frame costs one name
|
||||
// compare. 1.12 has none of these files, so on a miss Show() does what
|
||||
// SetCustomCursor does after its load, with the image embedded in the DLL
|
||||
// (Texture::Builtin), which a texture load of the same path also gets.
|
||||
//
|
||||
// The files are 4.3.4's Interface\Cursor images, under 4.3.4's names. 4.3.4
|
||||
// already ships StableMaster.blp and Fishing.blp, but only as art a record
|
||||
// could name. StableMaster is a mode in retail's table (Enum.Cursormode
|
||||
// StablemasterCursor, listed after Ping and Enchant), not in 4.3.4's; no
|
||||
// client's table has Fishing.
|
||||
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "texture/Builtin.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Cursor::Extended {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetCustomCursor_t = uint32_t(__fastcall *)(const char *path);
|
||||
using SendCursorImage_t = void(__cdecl *)();
|
||||
|
||||
// In mode order, the Unable forms after the others.
|
||||
const char *const kPaths[] = {
|
||||
"Interface\\Cursor\\RepairNPC.blp",
|
||||
"Interface\\Cursor\\Innkeeper.blp",
|
||||
"Interface\\Cursor\\Quest.blp",
|
||||
"Interface\\Cursor\\QuestRepeatable.blp",
|
||||
"Interface\\Cursor\\QuestTurnIn.blp",
|
||||
"Interface\\Cursor\\StableMaster.blp",
|
||||
"Interface\\Cursor\\Fishing.blp",
|
||||
"Interface\\Cursor\\UnableRepairNPC.blp",
|
||||
"Interface\\Cursor\\UnableInnkeeper.blp",
|
||||
"Interface\\Cursor\\UnableQuest.blp",
|
||||
"Interface\\Cursor\\UnableQuestRepeatable.blp",
|
||||
"Interface\\Cursor\\UnableQuestTurnIn.blp",
|
||||
"Interface\\Cursor\\UnableStableMaster.blp",
|
||||
"Interface\\Cursor\\UnableFishing.blp",
|
||||
};
|
||||
static_assert(sizeof kPaths / sizeof kPaths[0] == 2 * kUnableOffset, "one path per mode");
|
||||
|
||||
CVar::Factory::Handle g_enabled = nullptr;
|
||||
|
||||
void Register() {
|
||||
g_enabled = CVar::Factory::Register(
|
||||
"expandedCursors",
|
||||
"Show what an NPC or object offers on the cursor: anvil for repairs, ! and ? for quests, "
|
||||
"hook for your bobber",
|
||||
Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Enabled() { return CVar::Factory::GetInt(g_enabled) != 0; }
|
||||
|
||||
bool Is(uint32_t mode) { return mode >= kRepairNpc && mode < kRepairNpc + 2 * kUnableOffset; }
|
||||
|
||||
bool IsUnable(uint32_t mode) { return Is(mode) && mode >= kRepairNpc + kUnableOffset; }
|
||||
|
||||
bool IsQuest(uint32_t mode) {
|
||||
if (!Is(mode))
|
||||
return false;
|
||||
const uint32_t kind = IsUnable(mode) ? mode - kUnableOffset : mode;
|
||||
return kind >= kQuest && kind <= kQuestTurnIn;
|
||||
}
|
||||
|
||||
const char *Path(uint32_t mode) { return kPaths[mode - kRepairNpc]; }
|
||||
|
||||
void Show(uint32_t mode) {
|
||||
const char *path = Path(mode);
|
||||
if (reinterpret_cast<SetCustomCursor_t>(Offsets::FUN_SET_CUSTOM_CURSOR)(path) != 0)
|
||||
return;
|
||||
const Texture::Builtin::Image *image = Texture::Builtin::Find(path);
|
||||
if (image == nullptr || image->width != Offsets::CURSOR_IMAGE_SIZE ||
|
||||
image->height != Offsets::CURSOR_IMAGE_SIZE)
|
||||
return;
|
||||
Game::Ref<uint32_t>(Offsets::VAR_CURSOR_MODE) = Offsets::CURSOR_MODE_CUSTOM;
|
||||
std::memcpy(reinterpret_cast<void *>(Offsets::VAR_CURSOR_IMAGE_CUSTOM), image->texels,
|
||||
Offsets::CURSOR_IMAGE_SIZE * Offsets::CURSOR_IMAGE_SIZE * sizeof(uint32_t));
|
||||
reinterpret_cast<SendCursorImage_t>(Offsets::FUN_SEND_CURSOR_IMAGE)();
|
||||
Common::BoundedCopy(reinterpret_cast<char *>(Offsets::VAR_CUSTOM_CURSOR_NAME), path,
|
||||
Offsets::CURSOR_FILE_SIZE);
|
||||
}
|
||||
|
||||
} // namespace Cursor::Extended
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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 "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Extended {
|
||||
|
||||
// The cursor modes later clients added that 1.12 has no image for, numbered
|
||||
// on from the engine's last mode (CURSOR_MODE_CUSTOM): 4.3.4's RepairNPC,
|
||||
// Innkeeper and quest cursors, retail's StableMaster, and the fish hook for
|
||||
// the fishing bobber. As with the engine's modes, each has an Unable form,
|
||||
// kUnableOffset further on.
|
||||
constexpr uint32_t kRepairNpc = Offsets::CURSOR_MODE_CUSTOM + 1;
|
||||
constexpr uint32_t kInnkeeper = kRepairNpc + 1;
|
||||
constexpr uint32_t kQuest = kRepairNpc + 2;
|
||||
constexpr uint32_t kQuestRepeatable = kRepairNpc + 3;
|
||||
constexpr uint32_t kQuestTurnIn = kRepairNpc + 4;
|
||||
constexpr uint32_t kStableMaster = kRepairNpc + 5;
|
||||
constexpr uint32_t kFishing = kRepairNpc + 6;
|
||||
constexpr uint32_t kUnableOffset = 7;
|
||||
|
||||
// Whether the `expandedCursors` cvar asks for these modes. The pickers that
|
||||
// give them (cursor/Npc, cursor/Fishing) keep 1.12's cursor while it is 0.
|
||||
bool Enabled();
|
||||
|
||||
// Whether `mode` is one of these, in either form.
|
||||
bool Is(uint32_t mode);
|
||||
|
||||
// Whether `mode` is one of these in its Unable form.
|
||||
bool IsUnable(uint32_t mode);
|
||||
|
||||
// Whether `mode` is a quest giver's cursor (Quest, QuestRepeatable,
|
||||
// QuestTurnIn), in either form.
|
||||
bool IsQuest(uint32_t mode);
|
||||
|
||||
// The image file of one of these modes, `Interface\Cursor\<its name>.blp`.
|
||||
const char *Path(uint32_t mode);
|
||||
|
||||
// Shows one of these modes as the cursor. The engine's SetCursorMode cannot,
|
||||
// so its hook (cursor/Hover) sends them here.
|
||||
void Show(uint32_t mode);
|
||||
|
||||
} // namespace Cursor::Extended
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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/>.
|
||||
|
||||
// With `expandedCursors` at 1 (Cursor::Extended::Enabled), the cursor over
|
||||
// the player's fishing bobber is a fish hook in place of the Interact gear.
|
||||
//
|
||||
// A game object's cursor is data. GetCursor (FUN_GAMEOBJECT_GET_CURSOR) gives
|
||||
// the cursor its record names, else its type handler's: BaseHandlerGetCursor
|
||||
// for most types, which gives a lock's LockType cursor, else Interact. The
|
||||
// record's name comes from the server, and tortoise-wow sends it empty; even
|
||||
// 3.3.5's data names none for the bobber (AzerothCore's template 35591), and
|
||||
// a bobber has no lock, so every client shows the gear. Later clients ship
|
||||
// the hook (Interface\Cursor\Fishing.blp) only for data to name.
|
||||
//
|
||||
// The fishing node handler (type 17) gets its own cursor slot, the way
|
||||
// 4.3.4's guild bank type overrides its cursor. The slot still runs
|
||||
// BaseHandlerGetCursor, which decides everything (0 for a bobber the player
|
||||
// is not channelling, Unable when not usable), and swaps only the image. The
|
||||
// classifier passes the mode to SetCursorMode, whose hook (cursor/Hover)
|
||||
// shows it.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Fishing {
|
||||
|
||||
namespace {
|
||||
|
||||
using HandlerGetCursor_t = uint32_t(__thiscall *)(void *handler);
|
||||
|
||||
uint32_t __fastcall FishingNodeCursor(void *handler, void * /*edx*/) {
|
||||
const uint32_t mode =
|
||||
reinterpret_cast<HandlerGetCursor_t>(Offsets::FUN_GO_HANDLER_BASE_GET_CURSOR)(handler);
|
||||
if (!Cursor::Extended::Enabled())
|
||||
return mode;
|
||||
if (mode == Offsets::CURSOR_MODE_INTERACT)
|
||||
return Cursor::Extended::kFishing;
|
||||
if (mode == Offsets::CURSOR_MODE_INTERACT + Offsets::CURSOR_MODE_UNABLE_OFFSET)
|
||||
return Cursor::Extended::kFishing + Cursor::Extended::kUnableOffset;
|
||||
return mode;
|
||||
}
|
||||
|
||||
void Patch() {
|
||||
Common::RedirectSlot(Offsets::SLOT_FISHING_NODE_HANDLER_GET_CURSOR,
|
||||
Offsets::FUN_GO_HANDLER_BASE_GET_CURSOR,
|
||||
reinterpret_cast<const void *>(&FishingNodeCursor));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Patch};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Cursor::Fishing
|
||||
+23
-1
@@ -27,14 +27,21 @@
|
||||
// The spell-targeting and mouselook legs of 0x4828D0 are left out: they
|
||||
// answer for the mouse, not for the object.
|
||||
//
|
||||
// The unit classifier and the fishing node's handler can also pick a mode of
|
||||
// Cursor::Extended (cursor/Npc, cursor/Fishing). The engine's SetCursorMode
|
||||
// cannot show one, so the same hook hands those to Cursor::Extended::Show,
|
||||
// and while capturing records them like any other.
|
||||
//
|
||||
// The images are the engine's own: `Interface\Cursor\<name>.blp` from its
|
||||
// cursor name table, and a game object's named cursor from its record
|
||||
// (FormatGameObjectCursorFile, `Unable` when not usable).
|
||||
// (FormatGameObjectCursorFile, `Unable` when not usable). An extended mode's
|
||||
// image is its embedded file (Cursor::Extended::Path).
|
||||
|
||||
#include "cursor/Hover.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
@@ -58,6 +65,10 @@ void __fastcall SetCursorMode_h(uint32_t mode) {
|
||||
g_captured = mode;
|
||||
return;
|
||||
}
|
||||
if (Cursor::Extended::Is(mode)) {
|
||||
Cursor::Extended::Show(mode);
|
||||
return;
|
||||
}
|
||||
SetCursorMode_o(mode);
|
||||
}
|
||||
|
||||
@@ -122,6 +133,17 @@ bool ImagePath(void *object, uint32_t mode, bool lowPriority, char *buf, std::si
|
||||
std::snprintf(buf, size, "%s", file);
|
||||
return true;
|
||||
}
|
||||
if (Cursor::Extended::Is(mode)) {
|
||||
// Retail shows no icon for a quest giver's cursor, not even the
|
||||
// low-priority pointer (checked in game, 2026-11-03, with the soft
|
||||
// interact icon and SoftTargetLowPriorityIcons 1). Blizzard_NamePlates
|
||||
// shows that icon exactly when SetUnitCursorTexture returns true, so
|
||||
// the refusal is the function's own.
|
||||
if (Cursor::Extended::IsQuest(mode))
|
||||
return false;
|
||||
std::snprintf(buf, size, "%s", Cursor::Extended::Path(mode));
|
||||
return true;
|
||||
}
|
||||
if (mode >= Offsets::CURSOR_MODE_CUSTOM)
|
||||
return false;
|
||||
const char *name = Game::Read<const char *>(Offsets::VAR_CURSOR_NAMES + 4 * mode);
|
||||
|
||||
+7
-6
@@ -18,9 +18,9 @@
|
||||
|
||||
namespace Cursor::Hover {
|
||||
|
||||
// The cursor mode (Offsets::CURSOR_MODE_*) the world cursor would show with
|
||||
// the mouse over `object` — a unit, player, game object or corpse — or 0
|
||||
// when it shows none. Computes it without showing it.
|
||||
// The cursor mode (Offsets::CURSOR_MODE_* or Cursor::Extended) the world
|
||||
// cursor would show with the mouse over `object` — a unit, player, game
|
||||
// object or corpse — or 0 when it shows none. Computes it without showing it.
|
||||
uint32_t Mode(void *object);
|
||||
|
||||
// For a game object's named cursor (CURSOR_MODE_CUSTOM): whether the player
|
||||
@@ -28,9 +28,10 @@ uint32_t Mode(void *object);
|
||||
bool IsUsable(void *object);
|
||||
|
||||
// Writes the image file of `mode` over `object` into `buf`. False, with
|
||||
// `buf` untouched, for no mode, and for the base (resting) mode unless
|
||||
// `lowPriority` — retail's low-priority image, the plain pointer over a unit
|
||||
// with nothing to do (SetUnitCursorTexture's includeLowPriority).
|
||||
// `buf` untouched, for no mode, for a quest giver's cursor (retail shows no
|
||||
// icon for one), and for the base (resting) mode unless `lowPriority` —
|
||||
// retail's low-priority image, the plain pointer over a unit with nothing to
|
||||
// do (SetUnitCursorTexture's includeLowPriority).
|
||||
bool ImagePath(void *object, uint32_t mode, bool lowPriority, char *buf, std::size_t size);
|
||||
|
||||
} // namespace Cursor::Hover
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// 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/>.
|
||||
|
||||
// With `expandedCursors` at 1 (Cursor::Extended::Enabled), the cursor over an
|
||||
// NPC shows what it offers, as in later clients: an anvil for a repairer, a
|
||||
// hearthstone for an innkeeper, a horseshoe for a stable master, and a quest
|
||||
// giver's ! or ? (Cursor::Extended). At 0, the default, it stays 1.12's.
|
||||
//
|
||||
// 1.12 picks the cursor for a hovered unit in ClassifyHoveredUnit
|
||||
// (FUN_WORLD_FRAME_CLASSIFY_UNIT). For a unit the player can interact with,
|
||||
// its NPC flag chain takes the lowest flag bit first: gossip, quest giver,
|
||||
// vendor, flight master and so on. It never tests the repair flag, and shows
|
||||
// Interact for an innkeeper. 4.3.4's CursorTrackUnit first asks the quest
|
||||
// giver's status (GetQuestGiverCursor), then tests the services from repair
|
||||
// down, with vendor and gossip near the end.
|
||||
//
|
||||
// The thunk stands in for the head of 1.12's chain
|
||||
// (SITE_CLASSIFY_UNIT_NPC_FLAGS). Everything the engine decides before it
|
||||
// stays the engine's: mouselook, spell targeting, charm, whether the unit can
|
||||
// be interacted with now, and whether it is beyond the interact reach (EBX,
|
||||
// the Unable form). With the cvar at 0 the thunk does the instruction it
|
||||
// replaced and the stock chain runs. The pick goes to the engine's
|
||||
// SetCursorMode, whose hook (cursor/Hover) shows an extended mode and records
|
||||
// every mode while the soft-interact code asks for the hover verdict.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Npc {
|
||||
|
||||
namespace {
|
||||
|
||||
// UNIT_NPC_FLAGS bits, as 1.12's chain tests them (tortoise-wow
|
||||
// UnitDefines.h names them).
|
||||
constexpr uint32_t kGossip = 0x1;
|
||||
constexpr uint32_t kVendor = 0x4;
|
||||
constexpr uint32_t kFlightMaster = 0x8;
|
||||
constexpr uint32_t kTrainer = 0x10;
|
||||
constexpr uint32_t kSpiritHealer = 0x20;
|
||||
constexpr uint32_t kSpiritGuide = 0x40;
|
||||
constexpr uint32_t kInnkeeper = 0x80;
|
||||
constexpr uint32_t kBanker = 0x100;
|
||||
constexpr uint32_t kPetitioner = 0x200;
|
||||
constexpr uint32_t kTabardDesigner = 0x400;
|
||||
constexpr uint32_t kBattleMaster = 0x800;
|
||||
constexpr uint32_t kAuctioneer = 0x1000;
|
||||
constexpr uint32_t kStableMaster = 0x2000;
|
||||
constexpr uint32_t kRepair = 0x4000;
|
||||
|
||||
// Quest giver statuses (OFF_CGUNIT_QUEST_STATUS). 1.12 numbers them its own
|
||||
// way (tortoise-wow QuestDef.h); its status-to-marker table (0x80C454) shows
|
||||
// each one's meaning: 3 a grey ?, 4 a blue ?, 5 a yellow !, 6 and 7 a yellow
|
||||
// ?. 0 none, 1 unavailable (grey !) and 2 chat (no marker) have no cursor,
|
||||
// as 4.3.4's none and unavailable have none.
|
||||
constexpr uint32_t kStatusIncomplete = 3;
|
||||
constexpr uint32_t kStatusRewardRepeatable = 4;
|
||||
constexpr uint32_t kStatusAvailable = 5;
|
||||
constexpr uint32_t kStatusRewardOld = 6;
|
||||
constexpr uint32_t kStatusReward = 7;
|
||||
|
||||
using IsQuestGiverToTalk_t = uint32_t(__stdcall *)(void *unit);
|
||||
using SetCursorMode_t = void(__fastcall *)(uint32_t mode);
|
||||
|
||||
uintptr_t g_classifyReturn = Offsets::CLASSIFY_UNIT_RETURN;
|
||||
|
||||
// 4.3.4's quest cursor for a status (GetQuestGiverCursor), matched by the
|
||||
// status's meaning: 4.3.4 gives Quest to an available and to an incomplete
|
||||
// quest, QuestRepeatable to a repeatable reward (its blue ?), and
|
||||
// QuestTurnIn to a reward (its yellow ?).
|
||||
uint32_t QuestMode(uint32_t status) {
|
||||
switch (status) {
|
||||
case kStatusIncomplete:
|
||||
case kStatusAvailable:
|
||||
return Cursor::Extended::kQuest;
|
||||
case kStatusRewardRepeatable:
|
||||
return Cursor::Extended::kQuestRepeatable;
|
||||
case kStatusRewardOld:
|
||||
case kStatusReward:
|
||||
return Cursor::Extended::kQuestTurnIn;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 4.3.4's order (CursorTrackUnit) in 1.12's flag bits, or 0 for none.
|
||||
// 4.3.4's guild master, guild banker and spell click tests have no 1.12
|
||||
// flag, and its flight master test also reads an icon type; for that flag
|
||||
// 1.12 shows Taxi, as here. 4.3.4 also takes a cursor that the creature's
|
||||
// cache record names (IconName), which 1.12's creature query does not carry.
|
||||
// A stable master gets retail's StableMaster in 4.3.4's place for it, where
|
||||
// 4.3.4 shows Speak. Where 4.3.4 tests gossip, this also takes the quest
|
||||
// giver 1.12 would speak to (IsQuestGiverToTalk), so a status without a
|
||||
// quest cursor keeps 1.12's Speak.
|
||||
uint32_t ServiceMode(void *unit) {
|
||||
if (const uint32_t quest = QuestMode(Game::Read<uint32_t>(unit, Offsets::OFF_CGUNIT_QUEST_STATUS)))
|
||||
return quest;
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(Game::Read<const void *>(unit, Offsets::OFF_UNIT_DESCRIPTOR),
|
||||
Offsets::OFF_UNIT_DESCRIPTOR_NPC_FLAGS);
|
||||
if (flags & kRepair)
|
||||
return Cursor::Extended::kRepairNpc;
|
||||
if (flags & kInnkeeper)
|
||||
return Cursor::Extended::kInnkeeper;
|
||||
if (flags & kFlightMaster)
|
||||
return Offsets::CURSOR_MODE_TAXI;
|
||||
if (flags & kTrainer)
|
||||
return Offsets::CURSOR_MODE_TRAINER;
|
||||
if (flags & (kSpiritHealer | kSpiritGuide))
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
if (flags & kBanker)
|
||||
return Offsets::CURSOR_MODE_BUY;
|
||||
if (flags & (kPetitioner | kTabardDesigner | kBattleMaster))
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
if (flags & kAuctioneer)
|
||||
return Offsets::CURSOR_MODE_BUY;
|
||||
if (flags & kStableMaster)
|
||||
return Cursor::Extended::kStableMaster;
|
||||
if (flags & kVendor)
|
||||
return Offsets::CURSOR_MODE_PICKUP;
|
||||
if ((flags & kGossip) ||
|
||||
reinterpret_cast<IsQuestGiverToTalk_t>(Offsets::FUN_IS_QUEST_GIVER_TO_TALK)(unit) != 0)
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True when it set the mode. `unable` is the engine's EBX: 1 beyond the
|
||||
// interact reach, else 0. A unit with no service gets the base mode, where
|
||||
// the stock chain ends too (ShowBaseCursor).
|
||||
bool __stdcall Pick(void *unit, uint32_t unable) {
|
||||
if (!Cursor::Extended::Enabled())
|
||||
return false;
|
||||
uint32_t mode = ServiceMode(unit);
|
||||
if (mode == 0)
|
||||
mode = Game::Read<uint32_t>(Offsets::VAR_CURSOR_BASE_MODE);
|
||||
else if (unable != 0)
|
||||
mode += Cursor::Extended::Is(mode) ? Cursor::Extended::kUnableOffset
|
||||
: Offsets::CURSOR_MODE_UNABLE_OFFSET;
|
||||
reinterpret_cast<SetCursorMode_t>(Offsets::FUN_SET_CURSOR_MODE)(mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(Offsets::OFF_UNIT_DESCRIPTOR == 0x110, "NpcFlagsThunk replays [esi + 0x110]");
|
||||
|
||||
// Stands in for `mov ecx, [esi + 0x110]`, the head of the chain: the unit in
|
||||
// ESI, the Unable flag in EBX. When Pick set the mode, it leaves the
|
||||
// classifier through the chain's epilogue; otherwise it does the instruction
|
||||
// it replaced and the chain runs.
|
||||
__declspec(naked) void NpcFlagsThunk() {
|
||||
__asm {
|
||||
push ebx
|
||||
push esi
|
||||
call Pick
|
||||
test al, al
|
||||
jnz picked
|
||||
mov ecx, dword ptr [esi + 110h]
|
||||
ret
|
||||
picked:
|
||||
add esp, 4
|
||||
jmp dword ptr [g_classifyReturn]
|
||||
}
|
||||
}
|
||||
|
||||
void Patch() {
|
||||
static const uint8_t kFlagsRead[6] = {0x8B, 0x8E, 0x10, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_CLASSIFY_UNIT_NPC_FLAGS, kFlagsRead, sizeof kFlagsRead,
|
||||
reinterpret_cast<const void *>(&NpcFlagsThunk));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Patch};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Cursor::Npc
|
||||
+5
-3
@@ -25,11 +25,13 @@ const char *NormalizeEmpty(const char *s) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// The id is compared unsigned: a DBC column's "none" is -1, which a signed
|
||||
// compare passes as less than the count and then indexes records[-1]. The
|
||||
// engine rejects it with a signed `jl` before its `jg` against the count
|
||||
// (0x00472797). An unloaded store's count is -1, so it takes no id.
|
||||
const uint8_t *Record(uintptr_t recordsVar, uintptr_t countVar, uint32_t id) {
|
||||
if (id == 0)
|
||||
return nullptr;
|
||||
const int count = *reinterpret_cast<const int *>(countVar);
|
||||
if (static_cast<int>(id) > count)
|
||||
if (id == 0 || count <= 0 || id > static_cast<uint32_t>(count))
|
||||
return nullptr;
|
||||
const uint8_t *const *records =
|
||||
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@ namespace DBC {
|
||||
|
||||
// Resolves `id` to its record pointer in the table whose records-base
|
||||
// global is at `recordsVar` and count global at `countVar`. Returns
|
||||
// `nullptr` for `id == 0`, OOR ids (> count), and unpopulated slots.
|
||||
// `nullptr` for `id == 0`, OOR ids (> count, which includes -1 and every
|
||||
// other negative id), and unpopulated slots.
|
||||
// Caller reads whatever fields it needs off the returned record.
|
||||
//
|
||||
// Use this when `LocalizedField` / `StringField` don't fit — e.g. for
|
||||
|
||||
@@ -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
|
||||
@@ -23,15 +23,15 @@
|
||||
-- - The text is a ScrollingEditBoxTemplate with a MinimalScrollBar, the
|
||||
-- ScrollBox pair retail uses for scrolling edit boxes, not retail's
|
||||
-- ScrollFrameTemplate, which ClassicAPI does not port.
|
||||
-- - The stack is retail's GetErrorData without the locals, taken in the
|
||||
-- SetText slot: GetErrorCallstackHeight finds the code that failed
|
||||
-- however many handlers sit between it and the slot. 1.12 has no
|
||||
-- debuglocals.
|
||||
-- - The stack and locals are retail's GetErrorData, taken in the SetText
|
||||
-- slot: GetErrorCallstackHeight finds the code that failed however many
|
||||
-- handlers sit between it and the slot. No |K strings to remove, and no
|
||||
-- C_StringUtil.EscapeDecimalNonPrintables.
|
||||
-- - No scriptErrors cvar (1.12 always shows errors), no LUA_WARNING, no
|
||||
-- gamepad legend.
|
||||
-- - Lua 5.0: no `...` expression, no # operator.
|
||||
|
||||
local function GetErrorStack()
|
||||
local function GetErrorData()
|
||||
-- Example of how debug stack level is calculated
|
||||
-- Current stack: [1, 2, 3, 4, 5] (current function is at 1, total current height is 5)
|
||||
-- Stack at time of error: [1, 2] (these are currently now index 4 and 5, but at the time of error the stack height is 2)
|
||||
@@ -40,13 +40,20 @@ local function GetErrorStack()
|
||||
local errorCallStackHeight = GetErrorCallstackHeight();
|
||||
local errorStackOffset = errorCallStackHeight and (errorCallStackHeight - 1);
|
||||
local debugStackLevel = currentStackHeight - (errorStackOffset or 0);
|
||||
return debugstack(debugStackLevel);
|
||||
local skipFunctionsAndUserdata = true;
|
||||
|
||||
local stack = debugstack(debugStackLevel);
|
||||
local locals = debuglocals(debugStackLevel, skipFunctionsAndUserdata);
|
||||
|
||||
return stack, locals;
|
||||
end
|
||||
|
||||
local ERROR_FORMAT = "|cffffd200Message:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Time:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Count:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Stack:|r\n"
|
||||
.. "|cffffffff%s|r\n"
|
||||
.. "|cffffd200Locals:|r\n"
|
||||
.. "|cffffffff%s|r";
|
||||
|
||||
local INDEX_ORDER_FORMAT = "%d / %d";
|
||||
@@ -68,15 +75,16 @@ function ScriptErrorsFrame_OnLoad(self)
|
||||
ScrollUtil.RegisterScrollBoxWithScrollBar(scrollBox, self.ScrollBar);
|
||||
|
||||
ScriptErrors_Message.SetText = function(_, message)
|
||||
ScriptErrorsFrame_OnError(message, GetErrorStack());
|
||||
ScriptErrorsFrame_OnError(message, GetErrorData());
|
||||
end
|
||||
ScriptErrors.Show = function() end
|
||||
end
|
||||
|
||||
function ScriptErrorsFrame_OnError(message, stack)
|
||||
function ScriptErrorsFrame_OnError(message, stack, locals)
|
||||
local self = ScriptErrorsFrame;
|
||||
message = tostring(message);
|
||||
stack = stack or "";
|
||||
locals = locals or "<none>";
|
||||
|
||||
local messageKey = message .. "\n" .. stack;
|
||||
local index = self.seen[messageKey];
|
||||
@@ -84,7 +92,7 @@ function ScriptErrorsFrame_OnError(message, stack)
|
||||
local errorData = self.errorData[index];
|
||||
errorData.count = errorData.count + 1;
|
||||
else
|
||||
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack });
|
||||
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack, locals = locals });
|
||||
index = table.getn(self.errorData);
|
||||
self.seen[messageKey] = index;
|
||||
end
|
||||
@@ -106,7 +114,7 @@ function ScriptErrorsFrame_Update()
|
||||
-- SetText puts the cursor and the scroll back at the top.
|
||||
local errorData = self.errorData[self.index];
|
||||
if ( errorData ) then
|
||||
self.ScrollingEditBox:SetText(string.format(ERROR_FORMAT, errorData.message, errorData.time, errorData.count, errorData.stack));
|
||||
self.ScrollingEditBox:SetText(string.format(ERROR_FORMAT, errorData.message, errorData.time, errorData.count, errorData.stack, errorData.locals));
|
||||
self.ScrollingEditBox:GetEditBox():HighlightText(0);
|
||||
else
|
||||
self.ScrollingEditBox:ClearText();
|
||||
|
||||
@@ -607,6 +607,17 @@ AddKeybinding(bags, "AOELOOT")
|
||||
|
||||
local interface = NewPage(CLASSICAPI_INTERFACE, main.name)
|
||||
|
||||
-- The label ends with some of the cursors the setting shows. The DLL serves
|
||||
-- each image at its Interface\Cursor path, and the markup stays out of the
|
||||
-- translated string.
|
||||
local expandedCursorIcons = ""
|
||||
for _, name in ipairs({ "RepairNPC", "Innkeeper", "StableMaster", "Quest", "QuestTurnIn", "Fishing" }) do
|
||||
expandedCursorIcons = expandedCursorIcons .. " |TInterface\\Cursor\\" .. name .. ":16|t"
|
||||
end
|
||||
|
||||
AddHeader(interface, CLASSICAPI_CURSOR)
|
||||
AddCheckbox(interface, CLASSICAPI_EXPANDED_CURSORS .. expandedCursorIcons, CLASSICAPI_EXPANDED_CURSORS_TOOLTIP, CVarSetting("expandedCursors"))
|
||||
|
||||
AddHeader(interface, CLASSICAPI_NAMEPLATES)
|
||||
AddSlider(interface, CLASSICAPI_TARGET_NAMEPLATE_ALPHA, CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateSelectedAlpha"), 0, 1, 0.05)
|
||||
AddSlider(interface, CLASSICAPI_OTHER_NAMEPLATE_ALPHA, CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateNotSelectedAlpha"), 0, 1, 0.05)
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Mit Mouseover interagieren";
|
||||
BINDING_NAME_INTERACTTARGET = "Mit Ziel interagieren";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
CLASSICAPI_CURSOR = "Mauszeiger";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Erweiterte Mauszeiger anzeigen";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "Wenn Ihr mit der Maus auf einen NSC oder ein Objekt zeigt, stellt der Mauszeiger dar, was Ihr damit tun könnt. Zum Beispiel einen Amboss für Reparaturen, ! oder ? für Quests und einen Haken für Euren Angelschwimmer.";
|
||||
COMBAT_LABEL = "Kampf";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Zahlen für Abklingzeiten anzeigen";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Seid Ihr sicher, dass Ihr das Ausrüstungsset \"%s\" löschen wollt?";
|
||||
|
||||
@@ -13,15 +13,16 @@ CLASSICAPI_ACTION_TARGETING = "Action Targeting";
|
||||
CLASSICAPI_ADVANCED = "Advanced";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Icons for All Soft Targets";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "Auto Dismount";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell, use an object, loot, attack, or open the flight map while you are mounted.";
|
||||
CLASSICAPI_AUTO_STAND = "Auto Stand";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell, loot, or open the flight map while you sit.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, talk to an NPC, use an object or an item, or open the flight map.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "How many seconds the combat log keeps entries for addons.";
|
||||
CLASSICAPI_CURSOR = "Cursor";
|
||||
CLASSICAPI_DIRECTION_ANY = "Any direction";
|
||||
CLASSICAPI_DIRECTION_CONE = "Within 30 degrees";
|
||||
CLASSICAPI_DIRECTION_FRONT = "Directly in front";
|
||||
@@ -30,6 +31,8 @@ CLASSICAPI_ENEMY_DIRECTION = "Enemy Direction";
|
||||
CLASSICAPI_ENEMY_ICON = "Show Enemy Icon";
|
||||
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Shows an icon over the enemy that action targeting picks.";
|
||||
CLASSICAPI_ENEMY_RANGE = "Enemy Range";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Show Expanded Cursors";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "When you point at an NPC or an object, the cursor shows what you can do with it. For example, an anvil for repairs, ! or ? for quests, and a hook for your fishing bobber.";
|
||||
CLASSICAPI_FRIEND_DIRECTION = "Friend Direction";
|
||||
CLASSICAPI_FRIEND_ICON = "Show Friend Icon";
|
||||
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Shows an icon over the friendly unit that friendly action targeting picks.";
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interactuar al pasar el ratón";
|
||||
BINDING_NAME_INTERACTTARGET = "Interactuar con objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
CLASSICAPI_CURSOR = "Cursor";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Mostrar cursores ampliados";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "Al pasar el ratón sobre un PNJ o un objeto, el cursor muestra lo que puedes hacer con él. Por ejemplo, un yunque para las reparaciones, ! o ? para las misiones y un anzuelo para tu corcho de pesca.";
|
||||
COMBAT_LABEL = "Combate";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Mostrar tiempos de reutilización en cifras";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "¿Seguro que quieres eliminar el equipamiento %s?";
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interagir par survol de la souris";
|
||||
BINDING_NAME_INTERACTTARGET = "Interagir avec la cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
CLASSICAPI_CURSOR = "Curseur";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Afficher les curseurs étendus";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "Quand vous survolez un PNJ ou un objet, le curseur indique ce que vous pouvez en faire. Par exemple, une enclume pour les réparations, ! ou ? pour les quêtes et un hameçon pour votre flotteur de pêche.";
|
||||
COMBAT_LABEL = "Combat";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Temps de recharge en chiffres";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Voulez-vous vraiment supprimer l'ensemble d'équipement %s ?";
|
||||
|
||||
@@ -13,3 +13,6 @@ FOURTH_NUMBER = "조";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<지정을 해제하려면 오른쪽 클릭>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "\"%s\" 단축키 지정 또는 %s 눌러 취소";
|
||||
CLASSICAPI_CURSOR = "커서";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "확장 커서 표시";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "NPC나 물체를 가리키면 커서가 할 수 있는 행동을 보여줍니다. 예를 들어 수리는 모루, 퀘스트는 ! 또는 ?, 낚시찌는 낚싯바늘로 표시됩니다.";
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указателем мыши";
|
||||
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
CLASSICAPI_CURSOR = "Курсор";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Показывать расширенные курсоры";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "При наведении на NPC или объект курсор показывает, что с ним можно сделать. Например, наковальню для ремонта, ! или ? для заданий и крючок для вашего поплавка.";
|
||||
COMBAT_LABEL = "Бой";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Отсчет времени восстановления";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
|
||||
|
||||
@@ -4,10 +4,102 @@ ACTION_BUTTON_USE_KEY_DOWN = "按下快捷键时施法";
|
||||
ACTION_TARGETING_OPTION = "开启动作瞄准";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f KB) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f MB) %s";
|
||||
ADDON_OPTIONS = "插件设置";
|
||||
BINDING_NAME_AOELOOT = "范围拾取";
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "与鼠标悬停处互动";
|
||||
BINDING_NAME_INTERACTTARGET = "与目标互动";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
CLASSICAPI_ACTION_BARS = "动作条";
|
||||
CLASSICAPI_ACTION_TARGETING = "动作瞄准";
|
||||
CLASSICAPI_ADVANCED = "高级";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "所有软目标图标";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "自动下马";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术、使用物体、拾取、攻击或打开飞行地图时自动下马。";
|
||||
CLASSICAPI_AUTO_STAND = "自动站起";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术、拾取或打开飞行地图时自动站起。";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "自动取消形态";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、与NPC对话、使用物体或物品、或打开飞行地图时自动取消该形态。";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "背包与物品";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "施法与攻击";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "战斗记录保留时间";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "战斗记录为插件保留条目的秒数。";
|
||||
CLASSICAPI_CURSOR = "鼠标指针";
|
||||
CLASSICAPI_DIRECTION_ANY = "任意方向";
|
||||
CLASSICAPI_DIRECTION_CONE = "30度范围内";
|
||||
CLASSICAPI_DIRECTION_FRONT = "正前方";
|
||||
CLASSICAPI_ENEMIES = "敌人";
|
||||
CLASSICAPI_ENEMY_DIRECTION = "敌人方向";
|
||||
CLASSICAPI_ENEMY_ICON = "显示敌人图标";
|
||||
CLASSICAPI_ENEMY_ICON_TOOLTIP = "在动作瞄准选中的敌人上方显示图标。";
|
||||
CLASSICAPI_ENEMY_RANGE = "敌人范围";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "显示扩展鼠标指针";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "指向NPC或物体时,鼠标指针会显示你可以对其进行的操作。例如,铁砧表示修理,! 或 ? 表示任务,鱼钩表示你的鱼漂。";
|
||||
CLASSICAPI_FRIEND_DIRECTION = "友方方向";
|
||||
CLASSICAPI_FRIEND_ICON = "显示友方图标";
|
||||
CLASSICAPI_FRIEND_ICON_TOOLTIP = "在友方动作瞄准选中的友方单位上方显示图标。";
|
||||
CLASSICAPI_FRIEND_RANGE = "友方范围";
|
||||
CLASSICAPI_FRIEND_TARGETING = "开启友方动作瞄准";
|
||||
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "选中你视野方向最近的友方单位。当你没有目标时,你的有益法术会施放给它。";
|
||||
CLASSICAPI_FRIENDS = "友方";
|
||||
CLASSICAPI_GAME_OBJECT_ICON = "在物体上显示交互图标";
|
||||
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "同时在你无法选中的物体上方显示交互图标,例如邮箱和箱子。";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS = "在物品上隐藏装备方案";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "从物品提示信息中移除装备方案一行。";
|
||||
CLASSICAPI_ICON_SIZE = "图标大小";
|
||||
CLASSICAPI_ICON_SIZE_TOOLTIP = "交互、敌人和友方图标的大小。";
|
||||
CLASSICAPI_IGNORE_BACKPACK = "整理时忽略行囊";
|
||||
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "整理背包时,行囊中的物品保持原位。";
|
||||
CLASSICAPI_IGNORE_BANK = "整理时忽略主银行";
|
||||
CLASSICAPI_IGNORE_BANK_TOOLTIP = "整理银行时,主银行栏位中的物品保持原位。";
|
||||
CLASSICAPI_INTERACT_DIRECTION = "交互方向";
|
||||
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "NPC或物体需要处于什么方向,交互按键才会选中它。方向以镜头为准,而不是以角色为准。";
|
||||
CLASSICAPI_INTERACT_ICON = "显示交互图标";
|
||||
CLASSICAPI_INTERACT_ICON_TOOLTIP = "在交互按键选中的NPC或物体上方显示图标。";
|
||||
CLASSICAPI_INTERACT_KEY = "交互按键";
|
||||
CLASSICAPI_INTERACT_RANGE = "交互范围";
|
||||
CLASSICAPI_INTERFACE = "界面";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING = "保持动作瞄准";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "当你选定了自己选择的目标时,动作瞄准仍会选中敌人和友方。";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "自由目标跟随";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "软目标敌人";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "软目标友方";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "不跟随";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "动作瞄准设置的目标会跟随软目标敌人或软目标友方变化。你自己选择的目标则保持不变。";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS = "显示低优先级图标";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "同时在你无法对其做任何操作的单位上方显示图标,例如队伍成员。该图标为普通指针。";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES = "匹配名称中间的编辑";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "当你在名称中间输入时,建议的名称也会匹配光标之后的字母。";
|
||||
CLASSICAPI_MATCH_THE_COMMAND = "匹配命令";
|
||||
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "只建议符合命令的名称,例如 /gpromote 时只建议公会成员。关闭时,会建议你认识的所有名称。";
|
||||
CLASSICAPI_MOTION = "动态效果";
|
||||
CLASSICAPI_NAME_SUGGESTIONS = "名称建议";
|
||||
CLASSICAPI_NAMEPLATES = "姓名板";
|
||||
CLASSICAPI_NUMBERS = "数字";
|
||||
CLASSICAPI_OPTIONS_ABOUT = "ClassicAPI为游戏添加的功能设置。每个选项对应一项设置,修改后立即生效。";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "其他姓名板不透明度";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "非目标单位姓名板的不透明度。";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET = "优先你的目标";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "当你选择的目标可以被选中时,动作瞄准会优先选中它。关闭时,总是选中最近的单位。";
|
||||
CLASSICAPI_RANGE_TOOLTIP = "最大距离(码)。不能大于targetNearestDistance设置。";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST = "优先最近的密语";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "名称建议从你最近密语过的玩家开始。";
|
||||
CLASSICAPI_REMEMBER_PLAYERS = "记住玩家";
|
||||
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "保存你遇到的玩家的名字、职业、种族和性别,以便插件在以后的游戏中显示。你在该服务器的所有角色共享这些数据。";
|
||||
CLASSICAPI_SCRIPT_PROFILE = "脚本性能分析";
|
||||
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "统计插件占用的CPU时间。更改将在你重载界面后生效。";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "分隔大数字";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "当插件使用BreakUpLargeNumbers格式化数字时,每三位数字之间添加分隔符,例如1,234,567。";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "从右到左整理背包";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "整理时,将你的物品放到最后一个背包的末尾,将垃圾放到行囊的开头。";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE = "严格交互范围";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "绝不选中超出交互范围的NPC或物体。关闭时,即使超出范围,也会选中当前可以交互的目标。";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "目标姓名板不透明度";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "你目标姓名板的不透明度。";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH = "界面震动强度";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "缩放震动效果移动框架的距离。0表示关闭震动。";
|
||||
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "单位需要处于什么方向,动作瞄准才会选中它。方向以镜头为准,而不是以角色为准。";
|
||||
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "当你拥有目标时";
|
||||
COMBAT_LABEL = "战斗";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "显示冷却时间";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
|
||||
@@ -74,6 +166,7 @@ KEY_BINDING_TOOLTIP = "<右键解除键位>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "设置 \"%s\"的快捷键,或者按 %s 取消";
|
||||
SET_FOCUS = "设置焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<无焦点>";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "已装备%d件物品";
|
||||
ITEMS_IN_INVENTORY = "背包中有%d件物品";
|
||||
@@ -82,7 +175,6 @@ ITEM_MISSING = "%s缺失";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
STOP_AUTO_ATTACK = "停止自动攻击";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT = "%d铜币";
|
||||
COPPER_AMOUNT_SYMBOL = "铜";
|
||||
GEARSETS_POPUP_TEXT = "输入方案名称(最多16个字符):";
|
||||
@@ -141,6 +233,8 @@ FOURTH_NUMBER = "兆";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "装备管理";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新的方案";
|
||||
PLAYER_LIST_DELIMITER = "、";
|
||||
RELOADUI = "重载界面";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "为%s设置备注:";
|
||||
SET_NOTE = "设置备注";
|
||||
@@ -220,6 +314,7 @@ SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
SOCIAL_LABEL = "社交";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
LUA_ERROR = "Lua错误";
|
||||
TEXT_TO_SPEECH = "文本转语音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "调节讲话速度";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "音量";
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "與滑鼠指定的目標互動";
|
||||
BINDING_NAME_INTERACTTARGET = "與目標互動";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
CLASSICAPI_CURSOR = "游標";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "顯示擴充游標";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "指向NPC或物件時,游標會顯示你可以對其進行的操作。例如,鐵砧表示修理,! 或 ? 表示任務,魚鉤表示你的魚漂。";
|
||||
COMBAT_LABEL = "戰鬥";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "顯示冷卻時間";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed {
|
||||
@@ -30,4 +32,15 @@ struct Bytes {
|
||||
// resource is missing, does not inflate or split, or has no file `index`.
|
||||
Bytes File(int id, size_t index);
|
||||
|
||||
// The entry of `files`, a manifest in the generated `embedded_resources.h`,
|
||||
// whose path is the game path `path` (Common::SamePath); null when none is.
|
||||
template <class Entry, size_t N>
|
||||
const Entry *FindFile(const Entry (&files)[N], const char *path) {
|
||||
for (const Entry &file : files) {
|
||||
if (Common::SamePath(path, file.path))
|
||||
return &file;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace Embed
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,173 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// A script that re-anchors a region while that region is being laid out no
|
||||
// longer frees the anchor point the layout is reading.
|
||||
//
|
||||
// An edge resolver walks the region's points (FUN_LAYOUT_POINTS_X / Y) and asks
|
||||
// each for its coordinate (VTBL_ANCHOR_NODE_X / Y). When the point's relative
|
||||
// frame is deferred, the getter first ends the deferral with
|
||||
// FUN_REGION_LAYOUT_REALIZE(0), which lays the relative out at once: its
|
||||
// OnSizeChanged runs right there. The getter then reads the point again. If
|
||||
// that script moved the region with SetPoint or ClearAllPoints, the point is
|
||||
// freed, and the read gets heap memory: an access violation in
|
||||
// FUN_REGION_GET_RECT on the "relative frame" the freed point holds (crash
|
||||
// log 2026-11-03: `[ecx+0x3c]`, ecx = 5). The path is easy to reach: Show runs
|
||||
// a frame's OnSizeChanged before its children leave their deferral, and the
|
||||
// ported ScrollBox reads sizes from that handler.
|
||||
//
|
||||
// 3.3.5 fixed it in CFramePoint::GetRelativeRect (0x0049C830): the point is
|
||||
// marked busy around its relative's resolve, a point changed while busy is
|
||||
// flagged, and the getter stops on the flag without reading the point again.
|
||||
// GetFramePointX / Y then return a failure, and GetHorizontalFramePoint /
|
||||
// GetVerticalFramePoint (0x00488ED0 / 0x00488F10) walk the points again, up to
|
||||
// ten times, with the anchors as they are now. 1.12 has no flag word in its
|
||||
// point, and SetPoint never changes a point in place (it frees the old one and
|
||||
// allocates a new one), so "changed" here means "freed": the point's deleting
|
||||
// destructor (VTBL_ANCHOR_NODE_DELETE, every free goes through it) flags a busy
|
||||
// point.
|
||||
//
|
||||
// The getters end the deferral themselves, as 3.3.5 does, and stop when the
|
||||
// point is flagged. Otherwise they call the engine's own getter for the value.
|
||||
// If the resolve deferred the relative again (a Hide in its script), the
|
||||
// getter would end the deferral a second time. 3.3.5 ends it once and then
|
||||
// reads the rect, so the bit is hidden from the engine's getter during the
|
||||
// read. The walks are the engine's own, retried as 3.3.5 retries them.
|
||||
//
|
||||
// Nothing is hooked: the point vtable (VAR_ANCHOR_VTABLE_WITH_RELATIVE) is the
|
||||
// only way to reach the getters and the destructor, and the six edge
|
||||
// resolvers are the only callers of the walks.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Frame::AnchorResolve {
|
||||
|
||||
namespace {
|
||||
|
||||
using Coordinate_t = float(__thiscall *)(void *point, float scale);
|
||||
using Walk_t = float(__thiscall *)(void *layout, const int *points, int count);
|
||||
using Delete_t = void *(__thiscall *)(void *point, int flags);
|
||||
using Realize_t = void(__thiscall *)(void *layout, int defer);
|
||||
|
||||
// 3.3.5 GetHorizontalFramePoint / GetVerticalFramePoint: `for (i = 0; i < 10; i++)`.
|
||||
constexpr int kWalkAttempts = 10;
|
||||
|
||||
// A point whose relative is being laid out. Its destructor sets `freed`.
|
||||
struct BusyPoint {
|
||||
void *point;
|
||||
bool freed;
|
||||
BusyPoint *outer;
|
||||
};
|
||||
|
||||
// A point walk in progress. A getter sets `changed` when its point was freed.
|
||||
struct PointWalk {
|
||||
bool changed;
|
||||
PointWalk *outer;
|
||||
};
|
||||
|
||||
BusyPoint *g_busy = nullptr;
|
||||
PointWalk *g_walk = nullptr;
|
||||
|
||||
float Undefined() { return Game::Read<float>(Offsets::VAR_LAYOUT_UNDEFINED); }
|
||||
|
||||
float Coordinate(uintptr_t engine, void *point, float scale) {
|
||||
if (g_walk != nullptr && g_walk->changed)
|
||||
return Undefined(); // 3.3.5 stops the walk at the changed point
|
||||
|
||||
auto *relative = Game::Read<void *>(point, Offsets::OFF_ANCHOR_NODE_RELATIVE_TO);
|
||||
if ((Game::Read<uint32_t>(relative, Offsets::OFF_LAYOUT_FLAGS) & Offsets::LAYOUT_FLAG_DEFERRED) != 0) {
|
||||
BusyPoint busy{point, false, g_busy};
|
||||
g_busy = &busy;
|
||||
reinterpret_cast<Realize_t>(Offsets::FUN_REGION_LAYOUT_REALIZE)(relative, 0);
|
||||
g_busy = busy.outer;
|
||||
if (busy.freed) {
|
||||
if (g_walk != nullptr)
|
||||
g_walk->changed = true;
|
||||
return Undefined();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t &flags = Game::Ref<uint32_t>(relative, Offsets::OFF_LAYOUT_FLAGS);
|
||||
const uint32_t deferred = flags & Offsets::LAYOUT_FLAG_DEFERRED;
|
||||
flags &= ~deferred;
|
||||
const float value = reinterpret_cast<Coordinate_t>(engine)(point, scale);
|
||||
flags |= deferred;
|
||||
return value;
|
||||
}
|
||||
|
||||
float __fastcall PointX(void *point, void * /*edx*/, float scale) {
|
||||
return Coordinate(Offsets::FUN_ANCHOR_NODE_X, point, scale);
|
||||
}
|
||||
|
||||
float __fastcall PointY(void *point, void * /*edx*/, float scale) {
|
||||
return Coordinate(Offsets::FUN_ANCHOR_NODE_Y, point, scale);
|
||||
}
|
||||
|
||||
void *__fastcall DeletePoint(void *point, void * /*edx*/, int flags) {
|
||||
for (BusyPoint *busy = g_busy; busy != nullptr; busy = busy->outer) {
|
||||
if (busy->point == point)
|
||||
busy->freed = true;
|
||||
}
|
||||
return reinterpret_cast<Delete_t>(Offsets::FUN_ANCHOR_NODE_DELETE)(point, flags);
|
||||
}
|
||||
|
||||
float Walk(uintptr_t engine, void *layout, const int *points, int count) {
|
||||
for (int attempt = 0; attempt < kWalkAttempts; ++attempt) {
|
||||
PointWalk walk{false, g_walk};
|
||||
g_walk = &walk;
|
||||
const float value = reinterpret_cast<Walk_t>(engine)(layout, points, count);
|
||||
g_walk = walk.outer;
|
||||
if (!walk.changed)
|
||||
return value;
|
||||
}
|
||||
return Undefined();
|
||||
}
|
||||
|
||||
float __fastcall WalkX(void *layout, void * /*edx*/, const int *points, int count) {
|
||||
return Walk(Offsets::FUN_LAYOUT_POINTS_X, layout, points, count);
|
||||
}
|
||||
|
||||
float __fastcall WalkY(void *layout, void * /*edx*/, const int *points, int count) {
|
||||
return Walk(Offsets::FUN_LAYOUT_POINTS_Y, layout, points, count);
|
||||
}
|
||||
|
||||
void PatchCode() {
|
||||
constexpr uintptr_t vtable = Offsets::VAR_ANCHOR_VTABLE_WITH_RELATIVE;
|
||||
Common::RedirectSlot(vtable + Offsets::VTBL_ANCHOR_NODE_DELETE, Offsets::FUN_ANCHOR_NODE_DELETE,
|
||||
reinterpret_cast<const void *>(&DeletePoint));
|
||||
Common::RedirectSlot(vtable + Offsets::VTBL_ANCHOR_NODE_X, Offsets::FUN_ANCHOR_NODE_X,
|
||||
reinterpret_cast<const void *>(&PointX));
|
||||
Common::RedirectSlot(vtable + Offsets::VTBL_ANCHOR_NODE_Y, Offsets::FUN_ANCHOR_NODE_Y,
|
||||
reinterpret_cast<const void *>(&PointY));
|
||||
|
||||
const auto walkX = reinterpret_cast<const void *>(&WalkX);
|
||||
Common::RedirectCall(Offsets::CALL_LAYOUT_LEFT_POINTS, Offsets::FUN_LAYOUT_POINTS_X, walkX);
|
||||
Common::RedirectCall(Offsets::CALL_LAYOUT_CENTER_X_POINTS, Offsets::FUN_LAYOUT_POINTS_X, walkX);
|
||||
Common::RedirectCall(Offsets::CALL_LAYOUT_RIGHT_POINTS, Offsets::FUN_LAYOUT_POINTS_X, walkX);
|
||||
const auto walkY = reinterpret_cast<const void *>(&WalkY);
|
||||
Common::RedirectCall(Offsets::CALL_LAYOUT_TOP_POINTS, Offsets::FUN_LAYOUT_POINTS_Y, walkY);
|
||||
Common::RedirectCall(Offsets::CALL_LAYOUT_CENTER_Y_POINTS, Offsets::FUN_LAYOUT_POINTS_Y, walkY);
|
||||
Common::RedirectCall(Offsets::CALL_LAYOUT_BOTTOM_POINTS, Offsets::FUN_LAYOUT_POINTS_Y, walkY);
|
||||
}
|
||||
|
||||
// Each pass runs before its GlueXML or FrameXML loads; the patch is idempotent.
|
||||
const Game::GlueModuleAutoRegister _gluePatch{&PatchCode};
|
||||
const Game::ModuleAutoRegister _autoreg{&PatchCode};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Frame::AnchorResolve
|
||||
+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.
|
||||
|
||||
+5
-98
@@ -15,6 +15,7 @@
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -22,22 +23,6 @@ namespace Loot::Nearby {
|
||||
|
||||
namespace {
|
||||
|
||||
// Engine interact-range constants, read out of `.rdata` once and
|
||||
// pinned here as `constexpr`. Both are referenced by `FUN_005EC110`
|
||||
// (the master interact-state checker) and propagated through
|
||||
// `FUN_005DF130` / `FUN_005DF2A0` (CMSG_LOOT senders). Yards units;
|
||||
// values verified at runtime addresses `DAT_0080B058` and
|
||||
// `DAT_0080A1E8`.
|
||||
//
|
||||
// INTERACT_REACH (1.333…) is added to `target_radius + player_radius`
|
||||
// for the unclamped range; modeled as the "reach beyond your touch"
|
||||
// constant.
|
||||
// MIN_INTERACT_RANGE (5.0) clamps the result so even tiny
|
||||
// targets with near-zero bounding radius still get the standard
|
||||
// 5-yard right-click reach.
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// `ClntObjMgrEnumVisibleObjects(callback, context)` — walks the engine's
|
||||
// visible-object table. The callback shape's `void *unusedEdx` slot
|
||||
// absorbs the second `__fastcall` register so MSVC lays out the trailing
|
||||
@@ -49,84 +34,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
|
||||
// `__thiscall(self, outBuf) → const C3Vector *` at vtable slot 5
|
||||
// (byte offset `0x14`). Both CGUnit_C and CGPlayer_C populate the
|
||||
// caller-provided 12-byte buffer with the unit's world position and
|
||||
// return it. Same call shape `FUN_005EC110` uses to read the
|
||||
// player / target positions when deciding interact range.
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
// Reads `(unit.m_objectFields[+0x224] & UNIT_DYNFLAG_LOOTABLE) != 0`
|
||||
// — the server-driven bit it sets when the local player has loot
|
||||
// rights on this unit. Works uniformly for live and dead CGUnit
|
||||
// subclasses; the bit is simply 0 for non-lootable targets.
|
||||
//
|
||||
// Don't call `FUN_CGCORPSE_IS_LOOTABLE` here even though the engine
|
||||
// itself uses it in `FUN_005D6BF0`: that helper reads `+0x78` of
|
||||
// m_objectFields, which is the CORPSE dynamic-flags slot for
|
||||
// CGCorpse_C but the BYTES_0 (race / class / gender / power type)
|
||||
// slot for CGUnit_C. Routing live units through it returns the low
|
||||
// bit of *race* — yields false positives for any Human / Dwarf /
|
||||
// Undead / Gnome player and false negatives for dead mobs.
|
||||
bool IsLootable(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
// Reads the bounding-radius float from a unit's m_objectFields. The
|
||||
// engine uses this as the contribution from both sides (player + target)
|
||||
// in its interact-range computation.
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
// Invokes the unit's virtual `GetPosition` (vtable slot at offset
|
||||
// `0x14`). The engine's signature is `__thiscall(this, outBuf) →
|
||||
// outBuf`; we pass our own 12-byte stack buffer and read x/y/z out
|
||||
// of the returned pointer.
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
// Mirrors the range portion of `FUN_005EC110`. We skip the engine's
|
||||
// player-state checks (mounted / casting / prior-interaction GUID
|
||||
// gate) because they fire intermittently during normal play and would
|
||||
// make the enumerator return an empty list whenever the player has
|
||||
// already clicked something — surprising for a "what's lootable
|
||||
// nearby" reader. The pure distance test is what controls cursor
|
||||
// highlight, which is the contract the user asked for.
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
}
|
||||
|
||||
// Only `TYPEMASK_UNIT` carries lootable-mob state in 1.12 — dead
|
||||
// creatures stay as CGCreature_C objects with the lootable bit flipped
|
||||
// on, rather than transitioning to a separate corpse object. Dead
|
||||
@@ -135,7 +42,7 @@ bool InInteractRange(const void *player, const void *target) {
|
||||
|
||||
struct ScanCtx {
|
||||
void *L;
|
||||
const void *player; // resolved once outside the iterator
|
||||
void *player; // resolved once outside the iterator
|
||||
int idx; // 1-based slot of the next entry to append
|
||||
};
|
||||
|
||||
@@ -144,10 +51,10 @@ int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
if (obj == nullptr)
|
||||
return 1; // GUID isn't a unit; continue
|
||||
|
||||
if (!IsLootable(obj))
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1; // unit isn't lootable to us; continue
|
||||
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1; // out of click-loot range; continue
|
||||
|
||||
char guidBuf[Guid::STRING_SIZE];
|
||||
@@ -180,7 +87,7 @@ int __fastcall Script_GetNearbyLootableUnits(void *L) {
|
||||
// Resolve the local player once — the canonical CGPlayer_C the
|
||||
// engine's own interact-range checks use, distinct from the
|
||||
// `VAR_LOCAL_PLAYER_PTR` global. Bail if unavailable.
|
||||
const void *player = Game::ResolveUnitToken("player");
|
||||
void *player = Game::ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return 1;
|
||||
|
||||
|
||||
+35
-2
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../unit/Position.h"
|
||||
|
||||
namespace Loot::Request {
|
||||
|
||||
@@ -31,8 +32,11 @@ using CloseLootInner_t = void(__fastcall *)(int sendRelease,
|
||||
char earlyReturnOnGameObject,
|
||||
char showError);
|
||||
|
||||
uint64_t HeldTarget(const void *player) {
|
||||
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
|
||||
// The per-unit reach `CanLootNow` adds for each side (`[m_objectFields +
|
||||
// 0x1F0]`).
|
||||
float Reach(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -42,6 +46,35 @@ bool IsMounted(const void *player) {
|
||||
return Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID) != 0;
|
||||
}
|
||||
|
||||
bool IsLootable(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return (Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS) &
|
||||
Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
bool InReach(void *player, void *target) {
|
||||
float p[3];
|
||||
float t[3];
|
||||
if (!Unit::Position::Read(player, p) || !Unit::Position::Read(target, t))
|
||||
return false;
|
||||
// `CanLootNow`'s reach for a unit (`0x005EC142`..`0x005EC17E`), with the
|
||||
// engine's own constants read live.
|
||||
float reach = Reach(target) + Reach(player) +
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_LEEWAY));
|
||||
const float minReach =
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_MIN));
|
||||
if (reach < minReach)
|
||||
reach = minReach;
|
||||
const float dx = p[0] - t[0];
|
||||
const float dy = p[1] - t[1];
|
||||
const float dz = p[2] - t[2];
|
||||
return dx * dx + dy * dy + dz * dz <= reach * reach;
|
||||
}
|
||||
|
||||
uint64_t HeldTarget(const void *player) {
|
||||
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
|
||||
}
|
||||
|
||||
bool Send(void *player, void *target, uint64_t guid) {
|
||||
if (IsMounted(player))
|
||||
return false;
|
||||
|
||||
+17
-2
@@ -28,11 +28,26 @@ namespace Loot::Request {
|
||||
// (`CGUnit_C::Interact`, `0x0060BEA0`) applies before its loot leg.
|
||||
bool IsMounted(const void *player);
|
||||
|
||||
// Whether the server grants us loot on `unit` (`UNIT_DYNFLAG_LOOTABLE`).
|
||||
bool IsLootable(const void *unit);
|
||||
|
||||
// Whether `target` is within the reach `CanLootNow` (`0x005EC110`) demands
|
||||
// when the player is not walked up: both units' `+0x1F0` reach plus the
|
||||
// engine's live leeway, at least its live minimum, against the 3D distance.
|
||||
// `Send` never walks, so a target outside this reach is one the server
|
||||
// refuses with "too far away".
|
||||
bool InReach(void *player, void *target);
|
||||
|
||||
// The player's held loot target. The engine writes it when the request goes
|
||||
// out and clears it when the server answers with a failure or a release, so
|
||||
// a caller waiting on `guid` sees the refusal as this no longer being `guid`.
|
||||
uint64_t HeldTarget(const void *player);
|
||||
|
||||
// Sends the loot request the way a right-click would: refused while mounted
|
||||
// (the click's gate, which `FUN_CMSG_LOOT_UNIT` itself lacks), never
|
||||
// auto-walks. Returns whether the request went out — false when mounted or
|
||||
// when the engine's own pre-checks refused (another loot held, falling, not
|
||||
// in control, ...), in which case no response is coming.
|
||||
// when the engine's own pre-checks refused (another loot held, moving,
|
||||
// falling, not in control, ...), in which case no response is coming.
|
||||
bool Send(void *player, void *target, uint64_t guid);
|
||||
|
||||
// For a caller that gave up waiting: if the player still holds `guid` as
|
||||
|
||||
+63
-74
@@ -42,6 +42,7 @@
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
#include "../time/Clock.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -55,12 +56,11 @@ namespace {
|
||||
constexpr const char *kEventCompleted = "LOOT_SCAN_COMPLETED";
|
||||
const Event::Custom::AutoReserve _reserve{kEventCompleted};
|
||||
|
||||
// Per-step timeout for the controller hook to fire on a queued
|
||||
// CMSG_LOOT. WorldTick fires at the client frame rate (~30-60 Hz),
|
||||
// so 180 ticks ≈ 3-6s wall clock. Long enough for high-latency
|
||||
// servers, short enough that a single dropped response doesn't hang
|
||||
// the whole scan.
|
||||
constexpr int kStepTimeoutTicks = 180;
|
||||
// How long one step may wait: for the server to answer a CMSG_LOOT it
|
||||
// never answers, or for the engine to accept a send it keeps refusing
|
||||
// (the player is moving). Long enough for high-latency servers, short
|
||||
// enough that a dropped response doesn't hang the walk.
|
||||
constexpr uint32_t kStepTimeoutMs = 3000;
|
||||
|
||||
// Event IDs the engine fires through `FUN_FIRE_EVENT_NO_ARGS` for
|
||||
// loot-window transitions. The hook swallows these only when the
|
||||
@@ -71,6 +71,7 @@ constexpr int EVENT_LOOT_CLOSED = 0x10D;
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
WaitingToSend, // the engine refused the send for now; retried each tick
|
||||
WaitingForResponse, // CMSG_LOOT sent; awaiting our controller hook
|
||||
};
|
||||
|
||||
@@ -90,7 +91,7 @@ struct ScanCtx {
|
||||
State state = State::Idle;
|
||||
void *player = nullptr;
|
||||
uint64_t currentGuid = 0;
|
||||
int ticksSinceTransition = 0;
|
||||
uint32_t stepStartMs = 0;
|
||||
bool suppressEvents = false; // read by the FireEvent hook
|
||||
bool lootMode = false; // LootAllCorpses (true) vs ScanNearbyLoot (false)
|
||||
std::vector<uint64_t> queue;
|
||||
@@ -128,55 +129,6 @@ using LootMoney_t = void(__fastcall *)(void *player);
|
||||
using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// === Range / lootability helpers (identical to Loot::Nearby) ===
|
||||
|
||||
bool IsLootableUnit(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
}
|
||||
|
||||
// === Scrape ===
|
||||
|
||||
@@ -278,17 +230,37 @@ void TryStartNext() {
|
||||
StartLoot(guid);
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
// Not sent (mounted mid-walk, or the engine's pre-checks refused): no
|
||||
// response is coming, so skip without waiting out the timeout.
|
||||
if (target == nullptr || !Request::Send(s_scan.player, target, guid)) {
|
||||
void BeginStep(State state) {
|
||||
s_scan.state = state;
|
||||
s_scan.stepStartMs = Time::Clock::NowMs();
|
||||
}
|
||||
|
||||
// Sends the current corpse's loot request. The corpse is checked again
|
||||
// here, not only when the queue was built: the walk takes a round trip per
|
||||
// corpse, and by now the corpse may be gone, looted, or out of reach
|
||||
// because the player moved. The server refuses an out-of-reach corpse
|
||||
// with an error, so skip it instead.
|
||||
//
|
||||
// When the engine refuses the send itself (the player is moving or
|
||||
// falling, ...), the corpse stays current and `OnTick` retries it: the
|
||||
// refusal is about the player, not the corpse, so skipping would drop
|
||||
// every corpse left in the queue.
|
||||
void SendCurrent() {
|
||||
void *target =
|
||||
Object::ByGuid(Offsets::TYPEMASK_UNIT, s_scan.currentGuid, nullptr, 0);
|
||||
if (target == nullptr || !Request::IsLootable(target) ||
|
||||
!Request::InReach(s_scan.player, target)) {
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
if (Request::Send(s_scan.player, target, s_scan.currentGuid))
|
||||
BeginStep(State::WaitingForResponse);
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
s_scan.currentGuid = guid;
|
||||
s_scan.state = State::WaitingForResponse;
|
||||
s_scan.ticksSinceTransition = 0;
|
||||
BeginStep(State::WaitingToSend);
|
||||
SendCurrent();
|
||||
}
|
||||
|
||||
// === Hooks ===
|
||||
@@ -383,18 +355,35 @@ const Game::HookAutoRegister _hookController{
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
|
||||
|
||||
// === Tick (timeout only) ===
|
||||
// === Tick (refusals and timeouts) ===
|
||||
|
||||
void OnTick() {
|
||||
if (s_scan.state == State::Idle)
|
||||
return;
|
||||
++s_scan.ticksSinceTransition;
|
||||
if (s_scan.ticksSinceTransition >= kStepTimeoutTicks) {
|
||||
// Controller hook never fired — the server answered with a
|
||||
// failure, or never answered. A failure already cleared the
|
||||
// held target; an unanswered request still holds it, which
|
||||
// would block all looting, so release it. Skip this corpse.
|
||||
s_scan.suppressEvents = false;
|
||||
const bool timedOut = Time::Clock::Elapsed(s_scan.stepStartMs) >= kStepTimeoutMs;
|
||||
|
||||
if (s_scan.state == State::WaitingToSend) {
|
||||
// Still refused: the player has not stopped moving, or mounted,
|
||||
// or lost control. Every corpse left would be refused the same
|
||||
// way, so end the walk.
|
||||
if (timedOut) {
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
SendCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server refused the corpse ("too far away", not ours, ...): its
|
||||
// failure response and its release response both clear the held
|
||||
// target, and no window opens. Go on to the next corpse now.
|
||||
if (Request::HeldTarget(s_scan.player) != s_scan.currentGuid) {
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
if (timedOut) {
|
||||
// No answer at all. The held target would block all looting, so
|
||||
// release it, and skip this corpse.
|
||||
Request::ReleaseIfUnanswered(s_scan.player, s_scan.currentGuid);
|
||||
TryStartNext();
|
||||
}
|
||||
@@ -405,7 +394,7 @@ const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
// === Enumeration ===
|
||||
|
||||
struct EnumCtx {
|
||||
const void *player;
|
||||
void *player;
|
||||
std::vector<uint64_t> *out;
|
||||
};
|
||||
|
||||
@@ -413,9 +402,9 @@ int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
if (!IsLootableUnit(obj))
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1;
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1;
|
||||
ctx->out->push_back(guid);
|
||||
return 1;
|
||||
|
||||
+2
-53
@@ -111,6 +111,7 @@
|
||||
#include "dbc/Lookup.h"
|
||||
#include "map/Area.h"
|
||||
#include "map/Overlays.h"
|
||||
#include "texture/BlpReader.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -136,59 +137,7 @@ int NextPot(int v) {
|
||||
return p;
|
||||
}
|
||||
|
||||
// The engine's BLP reader, driven exactly as GetPortraitAlphaMask drives it
|
||||
// (see the FUN_BLP_OPEN block in Offsets.h). Open in the constructor, decode
|
||||
// on demand, free and reset in the destructor. Only one reader may be live
|
||||
// at a time: the engine keeps the file in one module-static buffer.
|
||||
class BlpReader {
|
||||
public:
|
||||
explicit BlpReader(const char *path) {
|
||||
std::memset(rec_, 0, sizeof(rec_));
|
||||
Put(Offsets::OFF_BLP_MAGIC, 0x32504C42); // 'BLP2'
|
||||
Put(Offsets::OFF_BLP_VERSION, 1);
|
||||
rec_[Offsets::OFF_BLP_ALPHA_TYPE] = 2;
|
||||
Put(Offsets::OFF_BLP_INIT_4A4, 100);
|
||||
open_ = reinterpret_cast<Open_t>(Offsets::FUN_BLP_OPEN)(rec_, path, 1) != 0;
|
||||
}
|
||||
~BlpReader() {
|
||||
if (decoded_)
|
||||
reinterpret_cast<FreeScratch_t>(Offsets::FUN_BLP_FREE_SCRATCH)(rec_, 0);
|
||||
reinterpret_cast<Reset_t>(Offsets::FUN_BLP_RESET)(rec_);
|
||||
}
|
||||
BlpReader(const BlpReader &) = delete;
|
||||
BlpReader &operator=(const BlpReader &) = delete;
|
||||
|
||||
bool IsOpen() const { return open_; }
|
||||
uint32_t Width() const { return Game::Read<uint32_t>(rec_, Offsets::OFF_BLP_WIDTH); }
|
||||
uint32_t Height() const { return Game::Read<uint32_t>(rec_, Offsets::OFF_BLP_HEIGHT); }
|
||||
|
||||
// Level 0 as BGRA8888 (alpha in byte 3), valid until destruction; null
|
||||
// on failure.
|
||||
const uint8_t *DecodeBgra() {
|
||||
if (!open_)
|
||||
return nullptr;
|
||||
void *out = nullptr;
|
||||
uint32_t size = 0;
|
||||
decoded_ = true;
|
||||
if (reinterpret_cast<DecodeLevel_t>(Offsets::FUN_BLP_DECODE_LEVEL)(
|
||||
rec_, Offsets::GX_TEX_FORMAT_BGRA8888, 0, &out, &size) == 0)
|
||||
return nullptr;
|
||||
return static_cast<const uint8_t *>(out);
|
||||
}
|
||||
|
||||
private:
|
||||
using Open_t = int(__thiscall *)(void *rec, const char *path, int flag);
|
||||
using DecodeLevel_t = int(__thiscall *)(void *rec, uint32_t fmt, uint32_t level,
|
||||
void **out, uint32_t *size);
|
||||
using FreeScratch_t = void(__thiscall *)(void *rec, uint32_t level);
|
||||
using Reset_t = void(__thiscall *)(void *rec);
|
||||
|
||||
void Put(uintptr_t off, uint32_t v) { std::memcpy(rec_ + off, &v, sizeof(v)); }
|
||||
|
||||
uint8_t rec_[Offsets::BLP_RECORD_SIZE];
|
||||
bool open_ = false;
|
||||
bool decoded_ = false;
|
||||
};
|
||||
using Texture::BlpReader;
|
||||
|
||||
// Where one overlay's art lives and where it sits on the zone canvas.
|
||||
struct OverlaySource {
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `PlayerModel:SetAnimation(anim [, variation])`, `FreezeAnimation(anim,
|
||||
// variation, frame)`, `SetDoBlend` / `GetDoBlend` and `HasAnimation(anim)` —
|
||||
// retail's CharacterModelBase animation methods.
|
||||
//
|
||||
// A pane rests on Stand: its per-paint UpdateModel (FUN_PLAYERMODEL_UPDATE_MODEL)
|
||||
// arms Stand whenever key bone -1 plays anything else and no turn is pending,
|
||||
// so any other animation lasts one frame. 4.3.4's CGCharacterModelBase has
|
||||
// the same UpdateModel with the pane's m_sequence / m_sequenceVariation in
|
||||
// place of Stand, reset to Stand each time the pane sets up a new model
|
||||
// (SetUpModel, the display setup, SetCustomRace), and a RotateModel that
|
||||
// skips the shuffle while m_sequence is not Stand. Retail's SetAnimation sets
|
||||
// that pair. 1.12.1 has no such fields, so:
|
||||
// - SetAnimation stores the pair per pane; UpdateModel is hooked to run
|
||||
// 4.3.4's version for a pane that has one, and stock otherwise.
|
||||
// - SetModelInstance (FUN_SIMPLEMODEL_SET_MODEL_INSTANCE, which every pane
|
||||
// model build and drop goes through) clears it.
|
||||
// - Model::Rotation asks HasRestingAnimation before it shuffles.
|
||||
// The re-arm also restarts the animation after the pane's done callback
|
||||
// (0x005062A0) arms Stand at its end, so it loops as in 4.3.4. A pane is
|
||||
// also re-armed once after each SetAnimation / FreezeAnimation, so a call
|
||||
// with the animation that already plays still starts or ends a freeze.
|
||||
//
|
||||
// FreezeAnimation rests the pane on the animation held at one time. The
|
||||
// engine's sequence time is start + (ftol(elapsed * speed) + offset) mod
|
||||
// length, so the arm plays at speed 0 and writes `frame` (milliseconds, the
|
||||
// unit of M2 sequence times) to the slot's offset. Speed 0 also puts the end
|
||||
// stamp on the start stamp, which would queue the done callback at once and
|
||||
// so arm Stand. The arm sets the slot's done latch, the flag Update sets
|
||||
// after it reports done: a held animation never ends.
|
||||
//
|
||||
// HasAnimation is PlayAnimation's own test on the raw id: the lookup index
|
||||
// must be below the sequence count.
|
||||
|
||||
#include "model/Animation.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Model::Animation {
|
||||
namespace {
|
||||
|
||||
using PaneFn_t = void(__thiscall *)(void *pane);
|
||||
using SetModelInstance_t = void(__thiscall *)(void *frame, void *model);
|
||||
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
|
||||
int offset, float speed, int blend, int primary);
|
||||
using GetKeyBoneAnim_t = uint32_t(__thiscall *)(void *model, int keyBone);
|
||||
using LookupIndex_t = uint16_t(__fastcall *)(uint32_t id, const void *table);
|
||||
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
|
||||
|
||||
struct PaneState {
|
||||
bool doBlend = false;
|
||||
bool resting = false; // SetAnimation gave the pane a resting animation
|
||||
bool pending = false; // set since the last arm
|
||||
bool frozen = false; // FreezeAnimation: held at `frame`
|
||||
uint32_t animation = 0;
|
||||
int variation = 0;
|
||||
uint32_t frame = 0; // ms into the sequence
|
||||
};
|
||||
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);
|
||||
// PlayAnimation stores the id only when it found a sequence to play.
|
||||
if (!s.frozen || KeyBoneAnim(model) != s.animation)
|
||||
return;
|
||||
auto *bone = Game::Read<uint8_t *>(model, Offsets::OFF_M2MODEL_BONE_STATES);
|
||||
Game::Ref<uint32_t>(bone, Offsets::OFF_M2BONE_SEQUENCE_OFFSET) = s.frame;
|
||||
Game::Ref<int>(bone, Offsets::OFF_M2BONE_DONE_REPORTED) = 1;
|
||||
}
|
||||
|
||||
void __fastcall UpdateModel_h(void *pane, void * /*edx*/) {
|
||||
const auto it = g_panes.find(pane);
|
||||
if (it == g_panes.end() || !it->second.resting) {
|
||||
g_origUpdateModel(pane);
|
||||
return;
|
||||
}
|
||||
PaneState &s = it->second;
|
||||
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEMODEL_UPDATE_MODEL)(pane);
|
||||
void *model = PaneModel(pane);
|
||||
const uint32_t deadline = Game::Read<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE);
|
||||
if (Game::Read<int>(pane, Offsets::OFF_PLAYERMODEL_TURNING) == 0 &&
|
||||
(deadline == 0 || Time::Clock::Reached(deadline)) && model != nullptr && IsLoaded(model) &&
|
||||
(s.pending || KeyBoneAnim(model) != s.animation)) {
|
||||
Arm(model, s);
|
||||
s.pending = false;
|
||||
Game::Ref<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE) = 0;
|
||||
}
|
||||
Game::Ref<int>(pane, Offsets::OFF_PLAYERMODEL_TURNING) = 0;
|
||||
}
|
||||
|
||||
void __fastcall SetModelInstance_h(void *frame, void * /*edx*/, void *model) {
|
||||
const auto it = g_panes.find(frame);
|
||||
if (it != g_panes.end())
|
||||
it->second.resting = false;
|
||||
g_origSetModelInstance(frame, model);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _updateHook{Offsets::FUN_PLAYERMODEL_UPDATE_MODEL,
|
||||
reinterpret_cast<void *>(&UpdateModel_h),
|
||||
reinterpret_cast<void **>(&g_origUpdateModel)};
|
||||
const Game::HookAutoRegister _setModelHook{Offsets::FUN_SIMPLEMODEL_SET_MODEL_INSTANCE,
|
||||
reinterpret_cast<void *>(&SetModelInstance_h),
|
||||
reinterpret_cast<void **>(&g_origSetModelInstance)};
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetAnimation(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: SetAnimation(animation [, variation])");
|
||||
return 0;
|
||||
}
|
||||
PaneState &s = g_panes[pane];
|
||||
s.resting = true;
|
||||
s.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;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_FreezeAnimation(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
|
||||
Game::Lua::Error(L, "Usage: FreezeAnimation(animation, variation, frame)");
|
||||
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;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetDoBlend(void *L) {
|
||||
if (void *pane = ResolvePane(L))
|
||||
g_panes[pane].doBlend = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetDoBlend(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const auto it = g_panes.find(pane);
|
||||
Game::Lua::PushBool(L, it != g_panes.end() && it->second.doBlend);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_HasAnimation(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: HasAnimation(animation)");
|
||||
return 0;
|
||||
}
|
||||
const auto id = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
bool has = false;
|
||||
void *model = PaneModel(pane);
|
||||
if (model != nullptr && IsLoaded(model)) {
|
||||
const auto *data = Game::Read<const uint8_t *>(
|
||||
Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED),
|
||||
Offsets::OFF_M2SHARED_DATA);
|
||||
const uint16_t index = reinterpret_cast<LookupIndex_t>(Offsets::FUN_M2_LOOKUP_INDEX)(
|
||||
id, data + Offsets::OFF_M2DATA_ANIMATION_LOOKUP);
|
||||
has = index < Game::Read<uint32_t>(data, Offsets::OFF_M2DATA_SEQUENCE_COUNT);
|
||||
}
|
||||
Game::Lua::PushBool(L, has);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetAnimation", &Script_SetAnimation},
|
||||
{"FreezeAnimation", &Script_FreezeAnimation},
|
||||
{"SetDoBlend", &Script_SetDoBlend},
|
||||
{"GetDoBlend", &Script_GetDoBlend},
|
||||
{"HasAnimation", &Script_HasAnimation},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kSetAnimationArgs[] = {
|
||||
Game::Doc::Req("animation", "number", "An AnimationData ID."),
|
||||
Game::Doc::Opt("variation", "number", "0", "The variation to play; -1 picks one at random."),
|
||||
};
|
||||
const Game::Doc::Function kSetAnimation{
|
||||
"Sets the animation the model plays until it loads a new model.", kSetAnimationArgs, {}};
|
||||
const Game::Doc::Field kFreezeArgs[] = {
|
||||
Game::Doc::Req("animation", "number", "An AnimationData ID."),
|
||||
Game::Doc::Req("variation", "number", "The variation; -1 picks one at random."),
|
||||
Game::Doc::Req("frame", "number", "The time into the animation, in milliseconds."),
|
||||
};
|
||||
const Game::Doc::Function kFreezeAnimation{
|
||||
"Holds the model in one pose of an animation until it loads a new model.", kFreezeArgs, {}};
|
||||
const Game::Doc::Field kDoBlendArgs[] = {
|
||||
Game::Doc::Opt("doBlend", "bool", "false"),
|
||||
};
|
||||
const Game::Doc::Function kSetDoBlend{
|
||||
"Sets whether the model blends into the animation that SetAnimation set.", kDoBlendArgs,
|
||||
{}};
|
||||
const Game::Doc::Field kDoBlendRets[] = {
|
||||
Game::Doc::Req("doBlend", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kGetDoBlend{"Returns whether the model blends into its animation.",
|
||||
{}, kDoBlendRets};
|
||||
const Game::Doc::Field kHasAnimationArgs[] = {
|
||||
Game::Doc::Req("animation", "number", "An AnimationData ID."),
|
||||
};
|
||||
const Game::Doc::Field kHasAnimationRets[] = {
|
||||
Game::Doc::Req("hasAnimation", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kHasAnimation{
|
||||
"Returns whether the model has the animation. False until the model has loaded.",
|
||||
kHasAnimationArgs, kHasAnimationRets};
|
||||
const Game::Doc::Method g_methodDocs[] = {
|
||||
{"SetAnimation", &kSetAnimation},
|
||||
{"FreezeAnimation", &kFreezeAnimation},
|
||||
{"SetDoBlend", &kSetDoBlend},
|
||||
{"GetDoBlend", &kGetDoBlend},
|
||||
{"HasAnimation", &kHasAnimation},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Frames are rebuilt on /reload and a new frame can reuse an old address.
|
||||
void PrepareForReload() {
|
||||
g_panes.clear();
|
||||
}
|
||||
|
||||
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool HasRestingAnimation(void *pane) {
|
||||
const auto it = g_panes.find(pane);
|
||||
return it != g_panes.end() && it->second.resting && it->second.animation != 0;
|
||||
}
|
||||
|
||||
} // namespace Model::Animation
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Model::Animation {
|
||||
|
||||
// True when SetAnimation gave the pane a resting animation other than Stand
|
||||
// (4.3.4's `m_sequence != 0`). RotateModel turns such a pane without the
|
||||
// shuffle animation.
|
||||
bool HasRestingAnimation(void *pane);
|
||||
|
||||
} // namespace Model::Animation
|
||||
+19
-14
@@ -37,6 +37,8 @@
|
||||
// both model cameras; vanilla's ConfigureCamera does not subtract it from
|
||||
// camera 1, so the portrait camera is used as is, in the same space.
|
||||
|
||||
#include "model/Camera.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
@@ -68,6 +70,23 @@ 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
|
||||
|
||||
// GetCameraById's lookup, bounded the way 4.3.4 null-checks its result.
|
||||
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);
|
||||
if (index >= count)
|
||||
return nullptr;
|
||||
return reinterpret_cast<GetCamera_t>(Offsets::FUN_M2MODEL_GET_CAMERA)(model, index);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct Vec3 {
|
||||
float x, y, z;
|
||||
};
|
||||
@@ -90,20 +109,6 @@ void SetScalar(void *camera, uint32_t id, float value) {
|
||||
reinterpret_cast<SetScalar_t>(Offsets::FUN_CAMERA_SET_SCALAR)(camera, id, value);
|
||||
}
|
||||
|
||||
// The model's camera with id `id` (0 = portrait), or null when it has none —
|
||||
// GetCameraById's lookup, bounded the way 4.3.4 null-checks its result.
|
||||
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);
|
||||
if (index >= count)
|
||||
return nullptr;
|
||||
return reinterpret_cast<GetCamera_t>(Offsets::FUN_M2MODEL_GET_CAMERA)(model, index);
|
||||
}
|
||||
|
||||
// ConfigureCamera's own evaluation pass: facing 0, rebuild the root
|
||||
// transform, animate the scene, restore the facing.
|
||||
void AnimateUnrotated(void *pane, void *model) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::Camera {
|
||||
|
||||
// The M2 model's camera with id `id` (0 = portrait), or null when it has none.
|
||||
void *CameraById(void *model, uint32_t id);
|
||||
|
||||
} // namespace Model::Camera
|
||||
+121
-85
@@ -11,7 +11,8 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `PlayerModel:SetDisplayInfo(displayID)` and `PlayerModel:SetCreature(creatureID)`
|
||||
// `PlayerModel:SetDisplayInfo(displayID)` and
|
||||
// `PlayerModel:SetCreature(creatureID [, displayID])`
|
||||
// — 4.3.4's CGCharacterModelBase methods (CharacterModelBase_Scripts.cpp), on
|
||||
// vanilla's own pane path.
|
||||
//
|
||||
@@ -32,6 +33,8 @@
|
||||
// path CGUnit_C::UpdateCharacterCustomization).
|
||||
// - SetCreature passes the creature's cache record straight to SetDisplay —
|
||||
// the same record 4.3.4 passes (CreatureStats_C) and the stable passes.
|
||||
// Retail's optional displayID shows that display in its place, through
|
||||
// the SetDisplayInfo record below.
|
||||
// - SetDisplayInfo has a display ID and no cache record. The pane only ever
|
||||
// reads the record's display-ID field (OFF_PLAYERMODEL_DISPLAY_RECORD), so
|
||||
// each pane gets a record of its own holding just that field, which lives
|
||||
@@ -51,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>
|
||||
@@ -118,8 +122,7 @@ DressJob g_jobs[8];
|
||||
constexpr int kMaxDressTicks = 600;
|
||||
|
||||
void CancelJob(DressJob &job) {
|
||||
if (job.builder != nullptr)
|
||||
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(job.builder);
|
||||
DestroyDresser(job.builder);
|
||||
job = DressJob{};
|
||||
}
|
||||
|
||||
@@ -153,14 +156,8 @@ DressJob *ClaimJob(void *frame) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Queue a dress for a character display just loaded into `frame`. Mirrors the
|
||||
// engine's own early-out: an Extra record with no bake name gets no
|
||||
// customization at all (FUN_005fb200 returns before creating the builder).
|
||||
// Queue a dress for a character display just loaded into `frame`.
|
||||
void ScheduleDress(void *frame, const uint8_t *extraRec) {
|
||||
const char *bake = Game::Read<const char *>(
|
||||
extraRec, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
if (bake == nullptr || bake[0] == '\0')
|
||||
return;
|
||||
DressJob *job = ClaimJob(frame);
|
||||
job->frame = frame;
|
||||
job->instance = Game::Read<void *>(frame,
|
||||
@@ -184,68 +181,19 @@ void StepJob(DressJob &job) {
|
||||
if (!job.configured) {
|
||||
if (!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(instance, 0, 0))
|
||||
return;
|
||||
|
||||
void *builder = reinterpret_cast<CharCompCreate_t>(Offsets::FUN_CHARCOMP_CREATE)();
|
||||
if (builder == nullptr) {
|
||||
job.builder = CreateDresser(instance, job.extraRec);
|
||||
if (job.builder == nullptr) {
|
||||
CancelJob(job);
|
||||
return;
|
||||
}
|
||||
job.builder = builder;
|
||||
|
||||
const uint8_t *extra = job.extraRec;
|
||||
auto field = [extra](int off) {
|
||||
return Game::Read<uint32_t>(extra, off);
|
||||
};
|
||||
CharCompInfo info = {};
|
||||
info.race = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_RACE);
|
||||
info.sex = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SEX);
|
||||
info.hairColor = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_COLOR);
|
||||
info.skin = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SKIN);
|
||||
info.face = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACE);
|
||||
info.facialHair = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACIAL_HAIR);
|
||||
info.hairStyle = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_STYLE);
|
||||
info.model = instance;
|
||||
info.useBake = 1;
|
||||
const char *bake = Game::Read<const char *>(
|
||||
extra, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
std::snprintf(info.bakePath, sizeof(info.bakePath), "%s%s",
|
||||
"Textures\\BakedNpcTextures\\", bake);
|
||||
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);
|
||||
if (!reinterpret_cast<CharCompSetInfo_t>(Offsets::FUN_CHARCOMP_SET_INFO)(builder, &info)) {
|
||||
CancelJob(job); // destroy releases the model ref already copied in
|
||||
return;
|
||||
}
|
||||
auto setItem = reinterpret_cast<CharCompSetItem_t>(Offsets::FUN_CHARCOMP_SET_ITEM);
|
||||
for (int slot = 0; slot < 10; ++slot) {
|
||||
const auto displayID = static_cast<int>(
|
||||
field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_EQUIP + slot * 4));
|
||||
if (displayID != 0)
|
||||
setItem(builder, slot, displayID);
|
||||
}
|
||||
|
||||
// The helm/shoulder feeds above 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;
|
||||
}
|
||||
|
||||
if (reinterpret_cast<CharCompPump_t>(Offsets::FUN_CHARCOMP_PUMP)(job.builder, nullptr))
|
||||
if (PumpDresser(job.builder))
|
||||
job.done = true; // builder kept — it owns the model ref for the dressed model
|
||||
}
|
||||
|
||||
@@ -264,28 +212,21 @@ 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)
|
||||
return;
|
||||
const uint8_t *displayRec = DBC::Record(
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_RECORDS, Offsets::VAR_CREATUREDISPLAYINFO_COUNT,
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID));
|
||||
if (displayRec == nullptr)
|
||||
return;
|
||||
const uint32_t extID =
|
||||
Game::Read<uint32_t>(displayRec, Offsets::OFF_CREATUREDISPLAYINFO_EXTENDED_ID);
|
||||
if (extID == 0)
|
||||
return;
|
||||
const uint8_t *extraRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFOEXTRA_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFOEXTRA_COUNT, extID);
|
||||
if (extraRec != nullptr)
|
||||
if (const uint8_t *extraRec =
|
||||
DressRecord(Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID)))
|
||||
ScheduleDress(pane, extraRec);
|
||||
}
|
||||
|
||||
@@ -310,6 +251,15 @@ struct DisplayRecord {
|
||||
};
|
||||
std::unordered_map<void *, DisplayRecord> g_displayRecords;
|
||||
|
||||
void ShowDisplayID(void *pane, uint32_t displayID) {
|
||||
if (displayID == 0)
|
||||
return;
|
||||
DisplayRecord &rec = g_displayRecords[pane];
|
||||
std::memset(rec.bytes, 0, sizeof(rec.bytes));
|
||||
std::memcpy(rec.bytes + Offsets::OFF_CREATURE_DISPLAYID, &displayID, sizeof(displayID));
|
||||
SetDisplay(pane, rec.bytes);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetDisplayInfo(void *L) {
|
||||
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
if (pane == nullptr)
|
||||
@@ -318,13 +268,7 @@ int __fastcall Script_SetDisplayInfo(void *L) {
|
||||
Game::Lua::Error(L, "Usage: SetDisplayInfo(displayID)");
|
||||
return 0;
|
||||
}
|
||||
const auto displayID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
if (displayID == 0)
|
||||
return 0;
|
||||
DisplayRecord &rec = g_displayRecords[pane];
|
||||
std::memset(rec.bytes, 0, sizeof(rec.bytes));
|
||||
std::memcpy(rec.bytes + Offsets::OFF_CREATURE_DISPLAYID, &displayID, sizeof(displayID));
|
||||
SetDisplay(pane, rec.bytes);
|
||||
ShowDisplayID(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -333,12 +277,16 @@ int __fastcall Script_SetCreature(void *L) {
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: SetCreature(creatureID)");
|
||||
Game::Lua::Error(L, "Usage: SetCreature(creatureID [, displayID])");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *record =
|
||||
Creature::Info::CacheRecord(static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)));
|
||||
if (record != nullptr)
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::IsNumber(L, 3))
|
||||
ShowDisplayID(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 3)));
|
||||
else
|
||||
SetDisplay(pane, record);
|
||||
return 0;
|
||||
}
|
||||
@@ -355,6 +303,8 @@ const Game::Doc::Function kSetDisplayInfo{
|
||||
"Shows a creature display on the model, dressed like the creature.", kDisplayArgs, {}};
|
||||
const Game::Doc::Field kCreatureArgs[] = {
|
||||
Game::Doc::Req("creatureID", "number", "A creature ID that is in the creature cache."),
|
||||
Game::Doc::Opt("displayID", "number", nullptr,
|
||||
"The CreatureDisplayInfo ID to show in place of the creature's own."),
|
||||
};
|
||||
const Game::Doc::Function kSetCreature{
|
||||
"Shows a creature on the model by its creature ID.", kCreatureArgs, {}};
|
||||
@@ -374,6 +324,92 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The engine's own early-out: an Extra record with no bake name gets no
|
||||
// customization at all (FUN_005fb200 returns before creating the builder).
|
||||
const uint8_t *DressRecord(uint32_t displayID) {
|
||||
const uint8_t *displayRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_COUNT, displayID);
|
||||
if (displayRec == nullptr)
|
||||
return nullptr;
|
||||
const uint32_t extID =
|
||||
Game::Read<uint32_t>(displayRec, Offsets::OFF_CREATUREDISPLAYINFO_EXTENDED_ID);
|
||||
if (extID == 0)
|
||||
return nullptr;
|
||||
const uint8_t *extraRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFOEXTRA_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFOEXTRA_COUNT, extID);
|
||||
if (extraRec == nullptr)
|
||||
return nullptr;
|
||||
const char *bake =
|
||||
Game::Read<const char *>(extraRec, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
return bake != nullptr && bake[0] != '\0' ? extraRec : nullptr;
|
||||
}
|
||||
|
||||
// Configure + feed equipment, the engine's order: SetInfo, then the 10 Extra
|
||||
// equipment slots. Field-for-field mirror of FUN_005fb200's NPC branch.
|
||||
void *CreateDresser(void *instance, const uint8_t *extraRec) {
|
||||
void *builder = reinterpret_cast<CharCompCreate_t>(Offsets::FUN_CHARCOMP_CREATE)();
|
||||
if (builder == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto field = [extraRec](int off) {
|
||||
return Game::Read<uint32_t>(extraRec, off);
|
||||
};
|
||||
CharCompInfo info = {};
|
||||
info.race = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_RACE);
|
||||
info.sex = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SEX);
|
||||
info.hairColor = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_COLOR);
|
||||
info.skin = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SKIN);
|
||||
info.face = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACE);
|
||||
info.facialHair = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACIAL_HAIR);
|
||||
info.hairStyle = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_STYLE);
|
||||
info.model = instance;
|
||||
info.useBake = 1;
|
||||
const char *bake =
|
||||
Game::Read<const char *>(extraRec, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
std::snprintf(info.bakePath, sizeof(info.bakePath), "%s%s", "Textures\\BakedNpcTextures\\",
|
||||
bake);
|
||||
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);
|
||||
if (!reinterpret_cast<CharCompSetInfo_t>(Offsets::FUN_CHARCOMP_SET_INFO)(builder, &info)) {
|
||||
DestroyDresser(builder); // releases the model ref already copied in
|
||||
return nullptr;
|
||||
}
|
||||
auto setItem = reinterpret_cast<CharCompSetItem_t>(Offsets::FUN_CHARCOMP_SET_ITEM);
|
||||
for (int slot = 0; slot < 10; ++slot) {
|
||||
const auto displayID =
|
||||
static_cast<int>(field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_EQUIP + slot * 4));
|
||||
if (displayID != 0)
|
||||
setItem(builder, slot, displayID);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
void DestroyDresser(void *dresser) {
|
||||
if (dresser != nullptr)
|
||||
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(dresser);
|
||||
}
|
||||
|
||||
void PrepareForReload() {
|
||||
for (auto &job : g_jobs)
|
||||
CancelJob(job);
|
||||
|
||||
@@ -10,8 +10,27 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::DisplayInfo {
|
||||
|
||||
// The CreatureDisplayInfoExtra record the engine dresses `displayID` from, or
|
||||
// null when the display gets no character customization.
|
||||
const uint8_t *DressRecord(uint32_t displayID);
|
||||
|
||||
// The engine character compositor, configured to dress the LOADED model
|
||||
// `instance` from `extraRec` (a DressRecord) and fed its equipment, or null
|
||||
// when the engine pool is empty or rejects the info. It holds a reference on
|
||||
// the model. Pump it with PumpDresser until that returns true; DestroyDresser
|
||||
// releases it (null is a no-op).
|
||||
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.
|
||||
|
||||
+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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,173 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `PlayerModel:SetKeepModelOnHide(keep)` / `GetKeepModelOnHide()` — retail's
|
||||
// CharacterModelBase methods.
|
||||
//
|
||||
// A vanilla pane drops its model when it hides (FUN_PLAYERMODEL_HIDE: slot 36
|
||||
// with null) and builds a new one from its unit or display record when it
|
||||
// shows (FUN_PLAYERMODEL_SHOW). That is retail's behavior with keep off, the
|
||||
// default. With keep on:
|
||||
// - Hide skips the drop and runs only the base hide.
|
||||
// - Show skips the build while the pane still holds its model, so the
|
||||
// model comes back as it was (tried-on items, animation).
|
||||
// - SetUnit, RefreshUnit, Dress (FUN_PLAYERMODEL_SET_UNIT_GUID) and
|
||||
// SetDisplay (FUN_PLAYERMODEL_SET_DISPLAY) only store the new subject
|
||||
// while the pane is hidden. A kept model would then be the old subject,
|
||||
// so they drop it first, and the next show builds the new one as it
|
||||
// does with keep off.
|
||||
// - Turning keep off while hidden drops the kept model.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Model::KeepOnHide {
|
||||
namespace {
|
||||
|
||||
using PaneFn_t = void(__thiscall *)(void *pane);
|
||||
using SetUnitGuid_t = void(__thiscall *)(void *pane, uint64_t guid);
|
||||
using SetDisplay_t = void(__thiscall *)(void *pane, const uint8_t *record);
|
||||
using SetModelInstance_t = void(__thiscall *)(void *pane, void *model);
|
||||
|
||||
PaneFn_t g_origHide = nullptr;
|
||||
PaneFn_t g_origShow = nullptr;
|
||||
SetUnitGuid_t g_origSetUnitGuid = nullptr;
|
||||
SetDisplay_t g_origSetDisplay = nullptr;
|
||||
|
||||
std::unordered_set<void *> g_keeping; // panes with SetKeepModelOnHide(true)
|
||||
|
||||
bool Keeps(void *pane) {
|
||||
return g_keeping.count(pane) != 0;
|
||||
}
|
||||
|
||||
bool HasModel(void *pane) {
|
||||
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE) != nullptr;
|
||||
}
|
||||
|
||||
// The pane Hide's own drop: slot 36 with null.
|
||||
void DropModel(void *pane) {
|
||||
Game::Read<SetModelInstance_t>(Game::Read<const void *>(pane, 0),
|
||||
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, nullptr);
|
||||
}
|
||||
|
||||
void DropIfKeptWhileHidden(void *pane) {
|
||||
if (Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 && HasModel(pane) && Keeps(pane))
|
||||
DropModel(pane);
|
||||
}
|
||||
|
||||
void __fastcall Hide_h(void *pane, void * /*edx*/) {
|
||||
if (Keeps(pane)) {
|
||||
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEFRAME_HIDE)(pane);
|
||||
return;
|
||||
}
|
||||
g_origHide(pane);
|
||||
}
|
||||
|
||||
void __fastcall Show_h(void *pane, void * /*edx*/) {
|
||||
if (Keeps(pane) && HasModel(pane)) {
|
||||
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEFRAME_SHOW)(pane);
|
||||
return;
|
||||
}
|
||||
g_origShow(pane);
|
||||
}
|
||||
|
||||
void __fastcall SetUnitGuid_h(void *pane, void * /*edx*/, uint64_t guid) {
|
||||
DropIfKeptWhileHidden(pane);
|
||||
g_origSetUnitGuid(pane, guid);
|
||||
}
|
||||
|
||||
void __fastcall SetDisplay_h(void *pane, void * /*edx*/, const uint8_t *record) {
|
||||
DropIfKeptWhileHidden(pane);
|
||||
g_origSetDisplay(pane, record);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hideHook{Offsets::FUN_PLAYERMODEL_HIDE,
|
||||
reinterpret_cast<void *>(&Hide_h),
|
||||
reinterpret_cast<void **>(&g_origHide)};
|
||||
const Game::HookAutoRegister _showHook{Offsets::FUN_PLAYERMODEL_SHOW,
|
||||
reinterpret_cast<void *>(&Show_h),
|
||||
reinterpret_cast<void **>(&g_origShow)};
|
||||
const Game::HookAutoRegister _setUnitHook{Offsets::FUN_PLAYERMODEL_SET_UNIT_GUID,
|
||||
reinterpret_cast<void *>(&SetUnitGuid_h),
|
||||
reinterpret_cast<void **>(&g_origSetUnitGuid)};
|
||||
const Game::HookAutoRegister _setDisplayHook{Offsets::FUN_PLAYERMODEL_SET_DISPLAY,
|
||||
reinterpret_cast<void *>(&SetDisplay_h),
|
||||
reinterpret_cast<void **>(&g_origSetDisplay)};
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetKeepModelOnHide(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::ToBoolean(L, 2) != 0) {
|
||||
g_keeping.insert(pane);
|
||||
return 0;
|
||||
}
|
||||
if (g_keeping.erase(pane) != 0 && Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 &&
|
||||
HasModel(pane))
|
||||
DropModel(pane);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetKeepModelOnHide(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, Keeps(pane));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetKeepModelOnHide", &Script_SetKeepModelOnHide},
|
||||
{"GetKeepModelOnHide", &Script_GetKeepModelOnHide},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kKeepArgs[] = {
|
||||
Game::Doc::Req("keepModelOnHide", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kSetKeep{
|
||||
"Sets whether the model stays loaded while the frame is hidden.", kKeepArgs, {}};
|
||||
const Game::Doc::Field kKeepRets[] = {
|
||||
Game::Doc::Req("keepModelOnHide", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kGetKeep{
|
||||
"Returns whether the model stays loaded while the frame is hidden.", {}, kKeepRets};
|
||||
const Game::Doc::Method g_methodDocs[] = {
|
||||
{"SetKeepModelOnHide", &kSetKeep},
|
||||
{"GetKeepModelOnHide", &kGetKeep},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Frames are rebuilt on /reload and a new frame can reuse an old address.
|
||||
void PrepareForReload() {
|
||||
g_keeping.clear();
|
||||
}
|
||||
|
||||
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
} // namespace Model::KeepOnHide
|
||||
@@ -0,0 +1,368 @@
|
||||
// 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/>.
|
||||
|
||||
// `SetPortraitTextureFromCreatureDisplayID(texture, displayID)` — a creature
|
||||
// portrait from a display ID alone, with no unit.
|
||||
//
|
||||
// Vanilla bakes unit portraits in BakePortrait (0x00524F60, benilla
|
||||
// ui/portraits): it copies the unit's live, dressed model into a fresh scene,
|
||||
// draws it with the model's portrait camera into the 64x64 top-left corner of
|
||||
// the back buffer, reads that back, cuts the round alpha mask in, and keeps
|
||||
// the texture by display ID. A display ID has no live model, so this builds
|
||||
// one the way AttachDisplayModel does for a PlayerModel — CreateModelFromPath
|
||||
// on the CreatureModelData path, then ApplyCreatureDisplay — in a scene of its
|
||||
// own, dresses it through the engine compositor the way a unit is dressed
|
||||
// (Model::DisplayInfo), and runs the bake's own sequence on it, call for call.
|
||||
//
|
||||
// The texture is bound at once and its pixels arrive later. Each display ID
|
||||
// owns one callback texture, transparent until baked; the bake writes the
|
||||
// pixels and re-latches them with GxTexMarkRectDirty, the engine's own path
|
||||
// for re-baking a cached portrait in place. A region the caller points
|
||||
// somewhere else before the bake finishes keeps what it was given, so no
|
||||
// region is tracked.
|
||||
//
|
||||
// The display model loads asynchronously. The engine bakes from Lua, before
|
||||
// anything draws over the corner it borrows; pending displays are stepped at
|
||||
// the head of the UI render callback, the same point in the frame as an
|
||||
// OnUpdate script that calls SetPortraitTexture. The bake saves and restores
|
||||
// exactly what BakePortrait saves (projection, view, viewport, fog).
|
||||
//
|
||||
// The bake's ready gate is IsReady(model, 0, 1), which only a DRAWN model
|
||||
// passes: a model's device textures are made the first time something asks
|
||||
// for them with wait set (GetTextureDeviceObject), and nothing draws this one.
|
||||
// So the gate here is the load (IsLoaded) plus the dress, and the bake's own
|
||||
// IsReady(copy, 1, 1) makes the device textures, as it does for the copy of a
|
||||
// unit's model.
|
||||
//
|
||||
// Portraits are kept until the client leaves the world, the lifetime of the
|
||||
// engine's display-ID portrait cache (ClearPortraitCaches, from ShutdownGame).
|
||||
// A model with no portrait camera stays transparent; the engine binds an empty
|
||||
// texture for one.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/Camera.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "texture/Load.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Model::Portrait {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kSize = 64;
|
||||
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);
|
||||
using GetViewport_t = void(__fastcall *)(float *, float *, float *, float *, float *, float *);
|
||||
using SetViewport_t = void(__stdcall *)(float, float, float, float, float, float);
|
||||
using ScreenSize_t = void(__fastcall *)(float *rect);
|
||||
using DdcToNdc_t = void(__fastcall *)(float *x, float *y, float inX, float inY);
|
||||
using DeviceGate_t = int(__cdecl *)();
|
||||
using MasterGet_t = bool(__fastcall *)(uint32_t cap);
|
||||
using MasterSet_t = void(__fastcall *)(uint32_t cap, uint32_t on);
|
||||
using SetClearColor_t = void(__stdcall *)(uint32_t argb);
|
||||
using Clear_t = void(__fastcall *)(uint32_t mask);
|
||||
using AlphaMask_t = const uint8_t *(__fastcall *)(uint32_t size);
|
||||
using GetDeviceTexture_t = void *(__fastcall *)(void *texture, uint32_t wait, void *callback);
|
||||
using MarkDirty_t = void(__fastcall *)(void *gxTex, const int32_t *rect, uint32_t immediate);
|
||||
using SetImage_t = void(__thiscall *)(void *region, void *texture);
|
||||
|
||||
// The engine's GrowableArray<uint32>: capacity, count, data, growth step.
|
||||
// FUN_GX_READ_PIXELS sets the count to the rectangle's texels, which fit the
|
||||
// capacity given, so it writes them into `data` in place.
|
||||
struct TexelArray {
|
||||
uint32_t capacity;
|
||||
uint32_t count;
|
||||
uint32_t *data;
|
||||
uint32_t increment;
|
||||
};
|
||||
// GetPortraitAlphaMask's result: one alpha byte per texel, from +8.
|
||||
struct AlphaMask {
|
||||
uint32_t reserved[2];
|
||||
const uint8_t *alpha;
|
||||
};
|
||||
|
||||
struct Portrait {
|
||||
std::array<uint32_t, kTexels> texels{}; // the texture's texel source, kept as long
|
||||
void *texture = nullptr; // HTEXTURE, one reference
|
||||
// The display model while it loads and dresses.
|
||||
void *scene = nullptr;
|
||||
void *model = nullptr;
|
||||
const uint8_t *dressRecord = nullptr;
|
||||
void *dresser = nullptr;
|
||||
};
|
||||
std::unordered_map<uint32_t, std::unique_ptr<Portrait>> g_portraits;
|
||||
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);
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(p.scene);
|
||||
p.dresser = p.model = p.scene = nullptr;
|
||||
p.dressRecord = nullptr;
|
||||
}
|
||||
|
||||
// BakePortrait from the copy on, against `p.model`. False when it could not
|
||||
// run this frame (no scene / copy), so the next frame tries again.
|
||||
bool Bake(Portrait &p) {
|
||||
void *source = p.model;
|
||||
if (Model::Camera::CameraById(source, 0) == nullptr)
|
||||
return true;
|
||||
|
||||
// The 64x64 pixel box at the top-left, in DDC, in the bake's own order
|
||||
// and precision: x87 operations, a float store after each (0x005251B7).
|
||||
float screen[4] = {};
|
||||
reinterpret_cast<ScreenSize_t>(Offsets::FUN_GX_CAPS_SCREEN_SIZE)(screen);
|
||||
const float width = static_cast<float>(static_cast<double>(screen[3]) - screen[1]);
|
||||
const float height = static_cast<float>(static_cast<double>(screen[2]) - screen[0]);
|
||||
const double scaleX = Game::Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const double scaleY = Game::Read<float>(Offsets::VAR_COORD_DDC_HEIGHT_SCALE);
|
||||
float x0 = 0.0f;
|
||||
float y1 = static_cast<float>(scaleY);
|
||||
float x1 = static_cast<float>(kSize / static_cast<double>(width) * scaleX);
|
||||
float y0 = static_cast<float>(scaleY - scaleY / height * kSize);
|
||||
|
||||
void *scene = reinterpret_cast<CreateScene_t>(Offsets::FUN_M2_CREATE_SCENE)();
|
||||
if (scene == nullptr)
|
||||
return false;
|
||||
void *copy = reinterpret_cast<CopyModel_t>(Offsets::FUN_M2SCENE_COPY_MODEL)(scene, source, 0);
|
||||
if (copy == nullptr) {
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
|
||||
float projection[16];
|
||||
float view[16];
|
||||
float viewport[6];
|
||||
reinterpret_cast<GetMatrix_t>(Offsets::FUN_GX_GET_PROJECTION)(projection);
|
||||
reinterpret_cast<GetMatrix_t>(Offsets::FUN_GX_GET_VIEW)(view);
|
||||
reinterpret_cast<GetViewport_t>(Offsets::FUN_GX_GET_VIEWPORT)(
|
||||
&viewport[0], &viewport[1], &viewport[2], &viewport[3], &viewport[4], &viewport[5]);
|
||||
const bool fog = reinterpret_cast<MasterGet_t>(Offsets::FUN_GX_MASTER_ENABLE_GET)(
|
||||
Offsets::GX_MASTER_FOG);
|
||||
auto setMaster = reinterpret_cast<MasterSet_t>(Offsets::FUN_GX_MASTER_ENABLE_SET);
|
||||
setMaster(Offsets::GX_MASTER_FOG, 0);
|
||||
|
||||
const float aspect = static_cast<float>((static_cast<double>(x1) - x0) /
|
||||
(static_cast<double>(y1) - y0));
|
||||
auto toNdc = reinterpret_cast<DdcToNdc_t>(Offsets::FUN_COORD_DDC_TO_NDC);
|
||||
toNdc(&x0, &y0, x0, y0);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
reinterpret_cast<SetClearColor_t>(Offsets::FUN_GX_SET_CLEAR_COLOR)(0);
|
||||
auto clear = reinterpret_cast<Clear_t>(Offsets::FUN_GX_CLEAR);
|
||||
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);
|
||||
auto draw = reinterpret_cast<DrawScene_t>(Offsets::FUN_M2SCENE_DRAW);
|
||||
draw(scene, 0);
|
||||
draw(scene, 1);
|
||||
|
||||
const int32_t box[4] = {0, 0, static_cast<int32_t>(kSize), static_cast<int32_t>(kSize)};
|
||||
TexelArray out{kTexels, kTexels, p.texels.data(), kTexels};
|
||||
reinterpret_cast<void(__fastcall *)(const int32_t *, TexelArray *)>(
|
||||
Offsets::FUN_GX_READ_PIXELS)(box, &out);
|
||||
clear(3);
|
||||
const auto *mask = reinterpret_cast<const AlphaMask *>(
|
||||
reinterpret_cast<AlphaMask_t>(Offsets::FUN_PORTRAIT_ALPHA_MASK)(kSize));
|
||||
if (out.data == p.texels.data()) {
|
||||
for (uint32_t i = 0; i < kTexels; ++i)
|
||||
p.texels[i] = (p.texels[i] & 0x00FFFFFFu) | static_cast<uint32_t>(mask->alpha[i]) << 24;
|
||||
void *gxTex = reinterpret_cast<GetDeviceTexture_t>(Offsets::FUN_TEXTURE_GET_RENDERABLE)(
|
||||
p.texture, 1, nullptr);
|
||||
reinterpret_cast<MarkDirty_t>(Offsets::FUN_GX_TEX_MARK_RECT_DIRTY)(gxTex, box, 1);
|
||||
}
|
||||
|
||||
reinterpret_cast<SetMatrix_t>(Offsets::FUN_GX_SET_PROJECTION)(projection);
|
||||
reinterpret_cast<SetMatrix_t>(Offsets::FUN_GX_SET_VIEW)(view);
|
||||
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);
|
||||
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))
|
||||
return false;
|
||||
if (p.dressRecord != nullptr) {
|
||||
// A dress the engine rejects bakes undressed, as a unit with no
|
||||
// compositor does.
|
||||
if (p.dresser == nullptr)
|
||||
p.dresser = Model::DisplayInfo::CreateDresser(p.model, p.dressRecord);
|
||||
if (p.dresser == nullptr)
|
||||
p.dressRecord = nullptr;
|
||||
else if (!Model::DisplayInfo::PumpDresser(p.dresser))
|
||||
return false;
|
||||
}
|
||||
if (!reinterpret_cast<DeviceGate_t>(Offsets::FUN_GX_WINDOW_ACTIVE)() ||
|
||||
!reinterpret_cast<DeviceGate_t>(Offsets::FUN_GX_DEVICE_ALIVE)())
|
||||
return false;
|
||||
return Bake(p);
|
||||
}
|
||||
|
||||
void Pump() {
|
||||
for (size_t i = 0; i < g_loading.size();) {
|
||||
Portrait &p = *g_loading[i];
|
||||
if (!Step(p)) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
ReleaseModel(p);
|
||||
g_loading[i] = g_loading.back();
|
||||
g_loading.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribeBefore _tick{&Pump};
|
||||
|
||||
// The display's portrait, started on first use. Null for a display ID with no
|
||||
// CreatureDisplayInfo / CreatureModelData row (AttachDisplayModel's gate).
|
||||
Portrait *Acquire(uint32_t displayID) {
|
||||
auto found = g_portraits.find(displayID);
|
||||
if (found != g_portraits.end())
|
||||
return found->second.get();
|
||||
const uint8_t *display = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_COUNT, displayID);
|
||||
if (display == nullptr)
|
||||
return nullptr;
|
||||
const uint8_t *modelData = DBC::Record(
|
||||
Offsets::VAR_CREATUREMODELDATA_RECORDS, Offsets::VAR_CREATUREMODELDATA_COUNT,
|
||||
Game::Read<uint32_t>(display, Offsets::OFF_CREATUREDISPLAYINFO_MODEL_ID));
|
||||
if (modelData == nullptr)
|
||||
return nullptr;
|
||||
|
||||
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);
|
||||
if (model == nullptr) {
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return nullptr;
|
||||
}
|
||||
reinterpret_cast<ApplyDisplay_t>(Offsets::FUN_APPLY_CREATURE_DISPLAY)(model, display,
|
||||
modelData);
|
||||
|
||||
// The bake's own texture: 64x64, one mip, ARGB8888, flags (1, 0, 0, 0, 0,
|
||||
// 0, 1, 0), "Portrait1" (0x005257D3).
|
||||
auto p = std::make_unique<Portrait>();
|
||||
p->scene = scene;
|
||||
p->model = model;
|
||||
p->dressRecord = Model::DisplayInfo::DressRecord(displayID);
|
||||
p->texture = Texture::CreateFromPixels("Portrait1", kSize, kSize, p->texels.data(),
|
||||
Texture::Flags(1, false, false));
|
||||
if (p->texture == nullptr) {
|
||||
ReleaseModel(*p);
|
||||
return nullptr;
|
||||
}
|
||||
g_loading.push_back(p.get());
|
||||
return g_portraits.emplace(displayID, std::move(p)).first->second.get();
|
||||
}
|
||||
|
||||
int __fastcall Script_SetPortraitTextureFromCreatureDisplayID(void *L) {
|
||||
void *texture = Game::Lua::ResolveTexture(L, 1);
|
||||
if (texture == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: SetPortraitTextureFromCreatureDisplayID(texture, displayID)");
|
||||
return 0;
|
||||
}
|
||||
if (Portrait *p = Acquire(static_cast<uint32_t>(Game::Lua::ToNumber(L, 2))))
|
||||
reinterpret_cast<SetImage_t>(Offsets::FUN_SIMPLETEXTURE_SET_IMAGE)(texture, p->texture);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("textureObject", "SimpleTexture"),
|
||||
Game::Doc::Req("creatureDisplayID", "number", "A CreatureDisplayInfo ID."),
|
||||
};
|
||||
const Game::Doc::Function kDoc{
|
||||
"Shows a creature's portrait in a texture, by its display ID.", kArgs, {}, "UnitGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("SetPortraitTextureFromCreatureDisplayID",
|
||||
&Script_SetPortraitTextureFromCreatureDisplayID, &kDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _register{&RegisterLuaFunctions};
|
||||
|
||||
// Leaving the world: drop every portrait, as ShutdownGame drops the engine's.
|
||||
// The world UI and every region holding one of these textures are gone by
|
||||
// the time the glue state boots.
|
||||
void ClearOnLogout() {
|
||||
for (Portrait *p : g_loading)
|
||||
ReleaseModel(*p);
|
||||
g_loading.clear();
|
||||
for (auto &entry : g_portraits)
|
||||
Texture::ReleaseHandle(entry.second->texture);
|
||||
g_portraits.clear();
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
|
||||
|
||||
} // namespace
|
||||
} // namespace Model::Portrait
|
||||
@@ -16,12 +16,14 @@
|
||||
// Vanilla's SetRotation (FUN_SCRIPT_PLAYERMODEL_SET_ROTATION) always turns the
|
||||
// model with a shuffle animation (TurnTo 0x00505BB0). 4.3.4's RotateModel
|
||||
// (CharacterModelBase.cpp) does the same when `animate` is true or absent, and
|
||||
// with `animate` false only stores the facing: no shuffle, no turn latch. So a
|
||||
// false `animate` stores the facing here and anything else runs vanilla's
|
||||
// method unchanged.
|
||||
// with `animate` false only stores the facing: no shuffle, no turn latch. It
|
||||
// also only stores the facing while the pane rests on an animation other than
|
||||
// Stand (`m_sequence`, set by SetAnimation, see Model::Animation). Otherwise
|
||||
// vanilla's method runs unchanged.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "model/Animation.h"
|
||||
|
||||
namespace Model::Rotation {
|
||||
namespace {
|
||||
@@ -31,11 +33,12 @@ constexpr int kTypeNone = -1; // lua_type of an absent argument
|
||||
using LuaMethod_t = int(__fastcall *)(void *L);
|
||||
|
||||
int __fastcall Script_SetRotation(void *L) {
|
||||
if (Game::Lua::Type(L, 3) == kTypeNone || Game::Lua::ToBoolean(L, 3) != 0)
|
||||
return reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
|
||||
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const bool animate = Game::Lua::Type(L, 3) == kTypeNone || Game::Lua::ToBoolean(L, 3) != 0;
|
||||
if (animate && !Model::Animation::HasRestingAnimation(pane))
|
||||
return reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: SetRotation(rotation (in radians))");
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `PlayerModel:SetUnit(unit)` → success, `CanSetUnit(unit)` and
|
||||
// `GetDisplayInfo()` — retail's CharacterModelBase unit methods.
|
||||
//
|
||||
// Vanilla's SetUnit (FUN_SCRIPT_PLAYERMODEL_SET_UNIT) returns nothing. It
|
||||
// stores the token's GUID and builds the model only when that GUID resolves
|
||||
// to a unit (type mask 8, FUN_PLAYERMODEL_SET_UNIT_GUID). So SetUnit runs
|
||||
// the engine's method unchanged (Turtle patches it) and reports whether the
|
||||
// stored GUID resolves; CanSetUnit is the same resolve without the set
|
||||
// (FUN_RESOLVE_UNIT_TOKEN = token to GUID, then type mask 8).
|
||||
//
|
||||
// GetDisplayInfo reads what the pane shows: the display record's ID
|
||||
// (SetDisplayInfo / SetCreature), or the unit's UNIT_FIELD_DISPLAYID.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::Subject {
|
||||
namespace {
|
||||
|
||||
using LuaMethod_t = int(__fastcall *)(void *L);
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
void *PaneUnit(void *pane) {
|
||||
return Object::ByGuid(Offsets::TYPEMASK_UNIT,
|
||||
Game::Read<uint64_t>(pane, Offsets::OFF_PLAYERMODEL_UNIT_GUID));
|
||||
}
|
||||
|
||||
int __fastcall Script_SetUnit(void *L) {
|
||||
reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_UNIT)(L);
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, PaneUnit(pane) != nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CanSetUnit(void *L) {
|
||||
if (ResolvePane(L) == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: CanSetUnit(unit)");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, Game::ResolveUnitToken(Game::Lua::ToString(L, 2)) != nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetDisplayInfo(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
uint32_t displayID = 0;
|
||||
if (const auto *record =
|
||||
Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD)) {
|
||||
displayID = Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID);
|
||||
} else if (void *unit = PaneUnit(pane)) {
|
||||
displayID = Game::Read<uint32_t>(Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR),
|
||||
Offsets::OFF_UNIT_FIELD_DISPLAYID);
|
||||
}
|
||||
Game::Lua::PushNumber(L, displayID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetUnit", &Script_SetUnit},
|
||||
{"CanSetUnit", &Script_CanSetUnit},
|
||||
{"GetDisplayInfo", &Script_GetDisplayInfo},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kUnitArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
};
|
||||
const Game::Doc::Field kSuccessRets[] = {
|
||||
Game::Doc::Req("success", "bool", "True when the unit exists."),
|
||||
};
|
||||
const Game::Doc::Function kSetUnit{"Shows a unit on the model.", kUnitArgs, kSuccessRets};
|
||||
const Game::Doc::Field kCanSetUnitRets[] = {
|
||||
Game::Doc::Req("canSetUnit", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kCanSetUnit{"Returns whether SetUnit can show the unit.", kUnitArgs,
|
||||
kCanSetUnitRets};
|
||||
const Game::Doc::Field kDisplayRets[] = {
|
||||
Game::Doc::Req("displayID", "number", "The CreatureDisplayInfo ID, or 0."),
|
||||
};
|
||||
const Game::Doc::Function kGetDisplayInfo{"Returns the display the model shows.", {},
|
||||
kDisplayRets};
|
||||
const Game::Doc::Method g_methodDocs[] = {
|
||||
{"SetUnit", &kSetUnit},
|
||||
{"CanSetUnit", &kCanSetUnit},
|
||||
{"GetDisplayInfo", &kGetDisplayInfo},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
} // namespace Model::Subject
|
||||
@@ -0,0 +1,235 @@
|
||||
// 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:ApplySpellVisualKit(spellVisualKitID [, oneShot])`: a spell's
|
||||
// visual kit on the pane's model.
|
||||
//
|
||||
// The world plays a kit on a unit (0x0060EDF0) through effect objects, spell
|
||||
// durations and the unit's own animation. A pane has none of these. The
|
||||
// engine's own kit on a bare model is the char-select ghost
|
||||
// (0x004727F0), and this is that routine for any kit, on the pane's model:
|
||||
// - Each of the nine effect columns: a model of the SpellVisualEffectName
|
||||
// file, made in the pane's scene and attached at the column's attachment
|
||||
// when the model has it (0x00472780). The children get the frame's light
|
||||
// like the item glows (Model::DisplayInfo::LightChildren).
|
||||
// - The procedures the ghost applies: kind 1 sets the tint, kind 14 the
|
||||
// alpha clamped to 0..1, as plain stores on the model.
|
||||
// The kit's animation, sound and the world-only procedures are not played,
|
||||
// as retail does not play them on a model frame either (probed: no cast
|
||||
// pose, no sound; the tint applies).
|
||||
//
|
||||
// oneShot is the world's one-shot kind: CEffect::AddEffect (0x0061FDD0)
|
||||
// gives the effect model a done callback, and EffectDoneCallback
|
||||
// (0x005FBF50) stops the effect when the sequence ends cleanly (reason 0).
|
||||
// CEffect::Stop (0x006203E0) clears the model's callbacks and hides it, so
|
||||
// its emitters stop and the particles already out finish; each frame the
|
||||
// drain (0x0061F680) releases a stopped effect once HasLiveParticles is
|
||||
// false. Here the release is the detach from the pane's model, run on
|
||||
// FrameTick's tail where no model is mid-draw. Without oneShot an effect
|
||||
// loops until the model is replaced, as on retail: an effect model's sequence
|
||||
// does not loop by itself, so it gets the world's loop callback
|
||||
// (KitEffectLoopCallback, 0x0060ED00), which plays the ended animation again.
|
||||
//
|
||||
// The effects are children of the pane's model, so they go when the model
|
||||
// does: SetUnit, SetItem, a hide and show, and so on. A model destroyed with
|
||||
// stopped effects still on it detaches them first (0x0070E3F5).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
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};
|
||||
constexpr int kProcedures = 4;
|
||||
constexpr int32_t kProcTint = 1;
|
||||
constexpr int32_t kProcAlpha = 14;
|
||||
// The ghost kit's byte scale (0x008026C8).
|
||||
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);
|
||||
g_stopped.push_back(model);
|
||||
}
|
||||
|
||||
// KitEffectLoopCallback: a sequence that ended cleanly plays again.
|
||||
void __fastcall OnEffectLoop(void *model, uint32_t /*keyBone*/, uint32_t animID, uint32_t reason,
|
||||
uint32_t /*overshoot*/, uint32_t /*userLow*/,
|
||||
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);
|
||||
}
|
||||
|
||||
// Detaches a stopped effect that is still on its model, then drops our
|
||||
// reference.
|
||||
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);
|
||||
}
|
||||
|
||||
// The dying-effect drain: a stopped effect goes once its particles are gone,
|
||||
// or at once when its model has already dropped it.
|
||||
void Drain() {
|
||||
for (size_t i = 0; i < g_stopped.size();) {
|
||||
void *model = g_stopped[i];
|
||||
if (Game::Read<void *>(model, Offsets::OFF_M2MODEL_PARENT) != nullptr &&
|
||||
reinterpret_cast<HasLiveParticles_t>(Offsets::FUN_M2MODEL_HAS_LIVE_PARTICLES)(model)) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
Discard(model);
|
||||
g_stopped[i] = g_stopped.back();
|
||||
g_stopped.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribe g_drain{&Drain};
|
||||
|
||||
// The ghost kit's procedure step: kind 1 tints, kind 14 sets the alpha.
|
||||
void ApplyProcedures(void *model, const uint8_t *kit) {
|
||||
for (int i = 0; i < kProcedures; ++i) {
|
||||
const auto kind = Game::Read<int32_t>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_KIND + i * 4);
|
||||
const float value = Game::Read<float>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_VALUE + i * 4);
|
||||
if (kind == kProcTint) {
|
||||
const auto color = static_cast<uint32_t>(static_cast<int32_t>(value));
|
||||
const float rgb[3] = {static_cast<float>((color >> 16) & 0xFF) * kByteScale,
|
||||
static_cast<float>((color >> 8) & 0xFF) * kByteScale,
|
||||
static_cast<float>(color & 0xFF) * kByteScale};
|
||||
reinterpret_cast<SetDiffuse_t>(Offsets::FUN_M2MODEL_SET_DIFFUSE_COLOR)(model, rgb);
|
||||
} else if (kind == kProcAlpha) {
|
||||
const float alpha = value < 0.0f ? 0.0f : value > 1.0f ? 1.0f : value;
|
||||
reinterpret_cast<SetAlpha_t>(Offsets::FUN_M2MODEL_SET_ALPHA)(model, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Apply(void *pane, const uint8_t *kit, bool oneShot) {
|
||||
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
if (model == nullptr)
|
||||
return;
|
||||
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_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,
|
||||
Game::Read<uint32_t>(kit, Offsets::OFF_SPELLVISUALKIT_EFFECTS + i * 4));
|
||||
if (effect == nullptr ||
|
||||
!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);
|
||||
if (child == nullptr)
|
||||
continue;
|
||||
SetDoneCallback(child, oneShot ? reinterpret_cast<const void *>(&OnEffectDone)
|
||||
: reinterpret_cast<const void *>(&OnEffectLoop));
|
||||
reinterpret_cast<Attach_t>(Offsets::FUN_M2MODEL_ATTACH)(child, model, kAttachments[i]);
|
||||
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);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: ApplySpellVisualKit(spellVisualKitID [, oneShot])");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *kit =
|
||||
DBC::Record(Offsets::VAR_SPELLVISUALKIT_RECORDS, Offsets::VAR_SPELLVISUALKIT_COUNT,
|
||||
static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)));
|
||||
if (kit != nullptr)
|
||||
Apply(pane, kit, Game::Lua::ToBoolean(L, 3) != 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"ApplySpellVisualKit", &Script_ApplySpellVisualKit},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kApplyArgs[] = {
|
||||
Game::Doc::Req("spellVisualKitID", "number", "A SpellVisualKit ID."),
|
||||
Game::Doc::Opt("oneShot", "boolean", "false",
|
||||
"True plays the effects once; false keeps them until the model changes."),
|
||||
};
|
||||
const Game::Doc::Function kApply{"Shows a spell visual kit's effects on the model.", kApplyArgs,
|
||||
{}};
|
||||
const Game::Doc::Method g_methodDocs[] = {
|
||||
{"ApplySpellVisualKit", &kApply},
|
||||
};
|
||||
|
||||
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};
|
||||
|
||||
// Drop every stopped effect before the reload tears the panes down.
|
||||
void PrepareForReload() {
|
||||
for (void *model : g_stopped)
|
||||
Discard(model);
|
||||
g_stopped.clear();
|
||||
}
|
||||
|
||||
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
} // namespace Model::VisualKit
|
||||
+33
-24
@@ -12,29 +12,22 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `IsLoggedIn()` — exposed on both Lua states (glue and in-game).
|
||||
// Returns `true` once a character has been loaded *and identified*
|
||||
// (GUID populated), `false` on glue, pre-load, and the brief window
|
||||
// between struct allocation and character-data unpack.
|
||||
// Retail's answer: true once PLAYER_LOGIN has fired for this UI instance.
|
||||
// So it is false on glue and while the addons load, at a cold login and
|
||||
// after a /reload, and true from PLAYER_LOGIN's own handlers on.
|
||||
//
|
||||
// Check is two-step:
|
||||
// 1. `[VAR_LOCAL_PLAYER_PTR]` non-NULL — the player aggregator
|
||||
// struct has been allocated
|
||||
// 2. `*(uint64*)(player + OFF_LOCAL_PLAYER_GUID)` non-zero — the
|
||||
// character's GUID has been populated into the struct
|
||||
//
|
||||
// The pointer alone is too lax: the engine allocates the struct
|
||||
// shortly before the character data lands, so there's a window where
|
||||
// the pointer is non-NULL but the GUID is still zero. Most addons
|
||||
// that check `IsLoggedIn` go straight to `UnitGUID("player")` next,
|
||||
// and we don't want them to see GUID==nil right after IsLoggedIn
|
||||
// reported true.
|
||||
//
|
||||
// 3.3.5 uses a single byte flag at `[0x00BD0793]` cleared on world
|
||||
// entry — we couldn't pin down the analogous 1.12 flag (the engine
|
||||
// path that signals event 253 wires through CVar-init machinery and
|
||||
// doesn't map cleanly across the binary). The pointer+GUID check is
|
||||
// the same effective signal: both flip together at the moment the
|
||||
// player object is ready, and stay stable across zone transitions.
|
||||
// 3.3.5's Script_IsLoggedIn is `!CGGameUI::m_loggingIn`: InitializeFrameXML
|
||||
// sets m_loggingIn first thing, before FrameScript_Initialize and the addon
|
||||
// load, and the enter cascade clears it just before it fires PLAYER_LOGIN.
|
||||
// 1.12's arm byte 0x00B4E260 is the same flag but cannot be read the same
|
||||
// way: the initializer sets it after the addons load (0x0049011D) and the
|
||||
// cascade clears it after the fire (0x0049095E). So the flag here keeps
|
||||
// 3.3.5's timing: the PLAYER_LOGIN call (CALL_ENTER_WORLD_FIRE_PLAYER_LOGIN)
|
||||
// is redirected to set it before the event goes out, and the
|
||||
// FrameScript_Initialize cleanup, which runs before every /reload and
|
||||
// /logout, clears it. The player GUID is no answer: UnitGUID("player") serves
|
||||
// the char-select GUID before the world loads, and on a /reload the live GUID
|
||||
// is set while the addons load.
|
||||
//
|
||||
// `IsLoggingOut()` / `IsPlayerInWorld()` — in-game only, as in 4.3.4.
|
||||
// IsLoggingOut is 4.3.4's ClientServices::Connection()->CharacterLoggingOut();
|
||||
@@ -43,9 +36,9 @@
|
||||
// CGGameUI::s_inWorld; 1.12's is VAR_IN_WORLD, with the same EnterWorld /
|
||||
// LeaveWorld writers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -53,8 +46,17 @@ namespace Player::LoginStatus {
|
||||
|
||||
namespace {
|
||||
|
||||
using FireEvent_t = void(__fastcall *)(int eventID);
|
||||
|
||||
bool g_loginFired = false; // PLAYER_LOGIN has fired for this UI instance
|
||||
|
||||
void __fastcall FirePlayerLogin(int eventID) {
|
||||
g_loginFired = true;
|
||||
reinterpret_cast<FireEvent_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS)(eventID);
|
||||
}
|
||||
|
||||
int __fastcall Script_IsLoggedIn(void *L) {
|
||||
Game::Lua::PushBool(L, Unit::Identity::PlayerGuid() != 0);
|
||||
Game::Lua::PushBool(L, g_loginFired);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -88,6 +90,8 @@ const Game::Doc::Function kIsPlayerInWorld{
|
||||
"SystemGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Common::RedirectCall(Offsets::CALL_ENTER_WORLD_FIRE_PLAYER_LOGIN, Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<const void *>(&FirePlayerLogin));
|
||||
Game::Lua::RegisterGlobalFunction("IsLoggedIn", &Script_IsLoggedIn);
|
||||
Game::Lua::RegisterGlobalFunction("IsLoggingOut", &Script_IsLoggingOut, &kIsLoggingOut);
|
||||
Game::Lua::RegisterGlobalFunction("IsPlayerInWorld", &Script_IsPlayerInWorld, &kIsPlayerInWorld);
|
||||
@@ -97,9 +101,14 @@ void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("IsLoggedIn", &Script_IsLoggedIn);
|
||||
}
|
||||
|
||||
void PrepareForReload() {
|
||||
g_loginFired = false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
static const Game::GlueModuleAutoRegister _gluereg{&RegisterGlue};
|
||||
|
||||
} // namespace Player::LoginStatus
|
||||
|
||||
+3
-11
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -46,17 +47,8 @@ namespace {
|
||||
|
||||
constexpr uint32_t MAP_INSTANCE_TYPE_PVP = 3;
|
||||
|
||||
const uint8_t *DbcRecord(uintptr_t recordsVar, uintptr_t countVar, uint32_t id) {
|
||||
const int count = *reinterpret_cast<const int *>(countVar);
|
||||
if (id == 0 || static_cast<int>(id) > count)
|
||||
return nullptr;
|
||||
const uint8_t *const *records =
|
||||
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
|
||||
return records != nullptr ? records[id] : nullptr;
|
||||
}
|
||||
|
||||
bool IsArenaArea(uint32_t areaID) {
|
||||
const uint8_t *rec = DbcRecord(Offsets::VAR_AREATABLE_RECORDS,
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATABLE_RECORDS,
|
||||
Offsets::VAR_AREATABLE_COUNT, areaID);
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
@@ -69,7 +61,7 @@ bool InArena() {
|
||||
const uint32_t mapID = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
|
||||
const uint8_t *map =
|
||||
DbcRecord(Offsets::VAR_MAP_RECORDS, Offsets::VAR_MAP_COUNT, mapID);
|
||||
DBC::Record(Offsets::VAR_MAP_RECORDS, Offsets::VAR_MAP_COUNT, mapID);
|
||||
if (map == nullptr)
|
||||
return false;
|
||||
const uint32_t type =
|
||||
|
||||
+7
-27
@@ -44,6 +44,7 @@
|
||||
// Muted entries stay in the list, flagged. Dropping them would make a sound
|
||||
// vanish from the one view you would use to un-mute it.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "time/Clock.h"
|
||||
@@ -80,18 +81,13 @@ uint32_t g_seq = 0;
|
||||
// is one `empty()` test on the hook path.
|
||||
std::unordered_set<std::string> g_muted;
|
||||
|
||||
// Lowercase, and fold '/' to '\', so a caller may pass either separator and
|
||||
// any casing — the engine's own path compares are case-insensitive.
|
||||
// Fold each character as Common::SamePath compares it, so a caller may pass
|
||||
// either separator and any casing — the engine's own path compares are
|
||||
// case-insensitive.
|
||||
void Normalize(const char *path, std::string &out) {
|
||||
out.clear();
|
||||
for (const char *p = path; *p != '\0'; ++p) {
|
||||
char c = *p;
|
||||
if (c == '/')
|
||||
c = '\\';
|
||||
else if (c >= 'A' && c <= 'Z')
|
||||
c = static_cast<char>(c + ('a' - 'A'));
|
||||
out.push_back(c);
|
||||
}
|
||||
for (const char *p = path; *p != '\0'; ++p)
|
||||
out.push_back(Common::FoldPathChar(*p));
|
||||
}
|
||||
|
||||
bool IsMuted(const char *path) {
|
||||
@@ -102,22 +98,6 @@ bool IsMuted(const char *path) {
|
||||
return g_muted.find(key) != g_muted.end();
|
||||
}
|
||||
|
||||
// Case-insensitive equality, tolerant of separator style — the same rule
|
||||
// Normalize applies, without building a string.
|
||||
bool SamePath(const char *a, const char *b) {
|
||||
for (;; ++a, ++b) {
|
||||
char ca = *a, cb = *b;
|
||||
if (ca == '/') ca = '\\';
|
||||
if (cb == '/') cb = '\\';
|
||||
if (ca >= 'A' && ca <= 'Z') ca = static_cast<char>(ca + ('a' - 'A'));
|
||||
if (cb >= 'A' && cb <= 'Z') cb = static_cast<char>(cb + ('a' - 'A'));
|
||||
if (ca != cb)
|
||||
return false;
|
||||
if (ca == '\0')
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Note a path as played. Re-playing a path refreshes its existing slot
|
||||
// rather than taking a new one, so a looping footstep cannot push the rest
|
||||
// of the window out — the list holds the last N DISTINCT sounds, which is
|
||||
@@ -127,7 +107,7 @@ void Record(const char *path, bool muted) {
|
||||
int oldest = 0;
|
||||
for (int i = 0; i < kRecentSlots; ++i) {
|
||||
Recent &r = g_recent[i];
|
||||
if (r.seq != 0 && SamePath(r.path, path)) {
|
||||
if (r.seq != 0 && Common::SamePath(r.path, path)) {
|
||||
r.seq = ++g_seq;
|
||||
r.timeMs = now;
|
||||
r.muted = muted;
|
||||
|
||||
+403
-8
@@ -35,10 +35,52 @@
|
||||
// for the mount / form aura (1.12 has no CMSG_CANCEL_MOUNT_AURA; cancelling
|
||||
// the mount aura is how its own UI dismounts). The server applies the
|
||||
// cancel / stand before the cast packet that follows it.
|
||||
//
|
||||
// autoUnshift and autoDismount also cover interaction, as in 4.3.4:
|
||||
// - NPC right-click: 4.3.4's CGPlayer_C::HandleOnRightClick cancels a form
|
||||
// without SHAPESHIFT_FORM_FLAG_CAN_INTERACT in place of
|
||||
// ERR_CANT_INTERACT_SHAPESHIFTED, before its range check, so we do the
|
||||
// same at 1.12's gate. Neither client gates an NPC on the mount. The
|
||||
// hover cursor asks CanInteractNow, whose form test 4.3.4 also passes
|
||||
// on CanAutoUnshift, so the NPC's service cursor shows while shifted.
|
||||
// - game objects: for an object with no Lock row, 4.3.4's CanUseNow lets
|
||||
// the mount pass when CanAutoDismount holds and the form when
|
||||
// CanAutoUnshift holds. The 1.12 use check is also the hover cursor's
|
||||
// query, so it only passes. Then TypeBase::Use, on its no-spell branch
|
||||
// only, dismounts (unless the object is usable mounted, the mailbox) and
|
||||
// cancels the form before CMSG_GAMEOBJ_USE. 4.3.4 does that with no
|
||||
// cvar test, the gate having run; we keep the cvars, so with them off
|
||||
// an object with a Lock row is still used as 1.12 uses it. 1.12's
|
||||
// mailbox and text types replace that use slot, which 4.3.4 does not,
|
||||
// so they get the same step. A locked object (a chest, a vein) opens by
|
||||
// a spell whose cast passes the caster check above.
|
||||
//
|
||||
// The remaining 4.3.4 sites:
|
||||
// - loot (autoStand): CGUnit_C / CGCorpse_C::OnRightClick stand up in
|
||||
// place of ERR_LOOT_NOTSTANDING. 1.12's two loot steps read the stand
|
||||
// state the same way, so the read stands the player and reports 0.
|
||||
// - loot (autoDismount): 4.3.4's unit right-click has no mount test on
|
||||
// its loot step, and LootUnit dismounts before CMSG_LOOT. 1.12's loot
|
||||
// step skips a mounted player, so its read passes, and 1.12's LootUnit
|
||||
// dismounts at the same point. A corpse is not looted mounted in either.
|
||||
// - item use (autoUnshift): CGItem_C::Use leaves the form in place of
|
||||
// ERR_NO_ITEMS_WHILE_SHAPESHIFTED; we do that at 1.12's refusal.
|
||||
// - attack (autoDismount): CanAttackNow (the attack cursor, the right-click
|
||||
// attack) and VerifyAttackIconPressed (ERR_ATTACK_MOUNTED) pass a mount
|
||||
// when CanAutoDismount holds; CombatModeEnter then dismounts in melee
|
||||
// reach, else shows ERR_OUT_OF_RANGE and does not attack. 1.12 inlines
|
||||
// the CanAttackNow mount test in four places and has the resolver's
|
||||
// refusal; each read passes, and StartAttack, past all its tests, runs
|
||||
// the CombatModeEnter step. The reach is Combat::Swing::InMeleeRange,
|
||||
// 4.3.4's GetMeleeRange formula on the engine's own constants.
|
||||
// - taxi: the taxi map's open stands, dismounts and leaves the form when
|
||||
// it has more than one node. 4.3.4 does that with no cvar; here each
|
||||
// action needs its cvar, so with them off the map opens as in 1.12.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "combat/Swing.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "spell/Cancel.h"
|
||||
@@ -56,6 +98,16 @@ using CheckForm_t = char(__fastcall *)(void *unit, void *edx,
|
||||
const uint8_t *record, int *reason);
|
||||
using GetStandState_t = int(__fastcall *)(const void *unit, void *edx);
|
||||
using SendStandState_t = void(__fastcall *)(const void *player, void *edx, uint32_t state);
|
||||
using IsShapeShifted_t = char(__fastcall *)(const void *unit);
|
||||
using UsableMounted_t = bool(__fastcall *)(const void *descriptorSlot);
|
||||
using ObjectUse_t = char(__fastcall *)(void *handler);
|
||||
using DisplayError_t = void(__cdecl *)(int id, ...);
|
||||
using TriggerTutorial_t = void(__fastcall *)(int id, int arg);
|
||||
using CanAutoInteract_t = bool(__fastcall *)(const void *player);
|
||||
|
||||
// Continuation addresses the naked thunks load from memory.
|
||||
uintptr_t g_itemUseFormPassed = Offsets::ITEM_USE_FORM_PASSED;
|
||||
uintptr_t g_startAttackReturn = Offsets::START_ATTACK_RETURN;
|
||||
|
||||
constexpr int kGetStandStateSlot = 0xA4 / 4;
|
||||
|
||||
@@ -112,10 +164,276 @@ bool CanAutoUnshift(const void *unit) {
|
||||
Offsets::SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT) == 0;
|
||||
}
|
||||
|
||||
int StandState(const void *unit) {
|
||||
auto getStandState =
|
||||
(*reinterpret_cast<const GetStandState_t *const *>(unit))[kGetStandStateSlot];
|
||||
return getStandState(unit, nullptr);
|
||||
}
|
||||
|
||||
int MountDisplay(const void *unit) {
|
||||
return Game::Read<int>(Fields(unit), Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID);
|
||||
}
|
||||
|
||||
// The engine's actions: the stand setter, and CMSG_CANCEL_AURA for the
|
||||
// mount / form aura.
|
||||
void Stand(const void *player) {
|
||||
reinterpret_cast<SendStandState_t>(Offsets::FUN_PLAYER_SEND_STAND_STATE)(player, nullptr, 0);
|
||||
}
|
||||
|
||||
void Dismount() { Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOUNTED); }
|
||||
|
||||
void CancelForm(const void *unit) {
|
||||
Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOD_SHAPESHIFT, Form(unit));
|
||||
}
|
||||
|
||||
char IsShapeShifted(const void *unit) {
|
||||
return reinterpret_cast<IsShapeShifted_t>(Offsets::FUN_UNIT_IS_SHAPESHIFTED)(unit);
|
||||
}
|
||||
|
||||
// The gates' test after IsShapeShifted: a form with no row, or one
|
||||
// without the interact flag.
|
||||
bool FormBlocksInteraction(const void *unit) {
|
||||
const uint8_t *form = DBC::Record(Offsets::VAR_SPELL_SHAPESHIFT_FORM_RECORDS,
|
||||
Offsets::VAR_SPELL_SHAPESHIFT_FORM_COUNT,
|
||||
static_cast<uint32_t>(Form(unit)));
|
||||
return form == nullptr ||
|
||||
(Game::Read<uint32_t>(form, Offsets::OFF_SPELL_SHAPESHIFT_FORM_FLAGS) &
|
||||
Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT) == 0;
|
||||
}
|
||||
|
||||
// True when the gate would refuse and autoUnshift takes the form off instead.
|
||||
bool UnshiftsForInteraction(const void *player) {
|
||||
return IsShapeShifted(player) && FormBlocksInteraction(player) && CanAutoUnshift(player);
|
||||
}
|
||||
|
||||
// The NPC right-click's IsShapeShifted: leaves the form and reports "not
|
||||
// shifted", as 4.3.4's HandleOnRightClick does.
|
||||
char __fastcall IsShapeShiftedForTalk(const void *player) {
|
||||
if (UnshiftsForInteraction(player)) {
|
||||
CancelForm(player);
|
||||
return 0;
|
||||
}
|
||||
return IsShapeShifted(player);
|
||||
}
|
||||
|
||||
// The game-object use check's and CanInteractNow's IsShapeShifted (4.3.4's
|
||||
// CanUseNow / CanInteractNow): the same verdict, nothing done. Both are
|
||||
// hover-cursor queries, so the cursor shows the service while shifted.
|
||||
char __fastcall IsShapeShiftedForQuery(const void *player) {
|
||||
return UnshiftsForInteraction(player) ? 0 : IsShapeShifted(player);
|
||||
}
|
||||
|
||||
// The mount reads of the gates 4.3.4 passes on CanAutoDismount (an object
|
||||
// use, a loot, an attack): the mount display, or 0 when the action that
|
||||
// follows dismounts first.
|
||||
int __fastcall MountDisplayForAction(const void *player) {
|
||||
const int display = MountDisplay(player);
|
||||
return display > 0 && CanAutoDismount(player) ? 0 : display;
|
||||
}
|
||||
|
||||
// Stands in for `mov ecx,[eax+0x1FC]` at SITE_GAMEOBJECT_CAN_USE_MOUNT_READ,
|
||||
// player in EDI. EAX and EDX are dead there; ECX takes the display.
|
||||
__declspec(naked) void UseMountReadThunk() {
|
||||
__asm {
|
||||
mov ecx, edi
|
||||
call MountDisplayForAction
|
||||
mov ecx, eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Stand in for a `mov eax,[reg+0x1FC]` mount read, the player in ESI or EDI;
|
||||
// EAX takes the display, every other register is kept.
|
||||
__declspec(naked) void MountReadEsiThunk() {
|
||||
__asm {
|
||||
push ecx
|
||||
push edx
|
||||
mov ecx, esi
|
||||
call MountDisplayForAction
|
||||
pop edx
|
||||
pop ecx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
__declspec(naked) void MountReadEdiThunk() {
|
||||
__asm {
|
||||
push ecx
|
||||
push edx
|
||||
mov ecx, edi
|
||||
call MountDisplayForAction
|
||||
pop edx
|
||||
pop ecx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Stands in for `mov edx,[eax+0x1FC]` at SITE_ATTACK_RESOLVER_CAN_ATTACK_-
|
||||
// MOUNT_READ, player in ECX, which the can-attack call after it still uses.
|
||||
__declspec(naked) void ResolverMountReadThunk() {
|
||||
__asm {
|
||||
push ecx
|
||||
call MountDisplayForAction
|
||||
mov edx, eax
|
||||
pop ecx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The loot steps' GetStandState call (ECX = the player): stands the player
|
||||
// up and reports 0 when autoStand allows, as 4.3.4's OnRightClick does.
|
||||
int __fastcall StandStateForLoot(const void *player) {
|
||||
const int state = StandState(player);
|
||||
if (state != 0 && CanAutoStand(player)) {
|
||||
Stand(player);
|
||||
return 0;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// LootUnit before CMSG_LOOT: 4.3.4's dismount.
|
||||
void __fastcall DismountForLoot(const void *player) {
|
||||
if (MountDisplay(player) > 0 && CanAutoDismount(player))
|
||||
Dismount();
|
||||
}
|
||||
|
||||
// Stands in for `push 0; xor dl,dl; mov cl,1` at SITE_LOOT_UNIT_SEND, player
|
||||
// in ESI: the dismount, then the replaced code with its push kept under the
|
||||
// return address.
|
||||
__declspec(naked) void LootSendThunk() {
|
||||
__asm {
|
||||
mov ecx, esi
|
||||
call DismountForLoot
|
||||
pop eax
|
||||
push 0
|
||||
push eax
|
||||
xor dl, dl
|
||||
mov cl, 1
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The item use's form refusal: leaves the form and lets the use go on when
|
||||
// autoUnshift allows.
|
||||
bool __fastcall UnshiftForItem(const void *player) {
|
||||
if (!CanAutoUnshift(player))
|
||||
return false;
|
||||
CancelForm(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stands in for `push 0x15E` at SITE_ITEM_USE_FORM_REFUSAL, player in ESI:
|
||||
// continues at ITEM_USE_FORM_PASSED, or pushes the error for the `jmp` to
|
||||
// the error tail that follows.
|
||||
__declspec(naked) void ItemFormRefusalThunk() {
|
||||
__asm {
|
||||
mov ecx, esi
|
||||
call UnshiftForItem
|
||||
test al, al
|
||||
jz refuse
|
||||
mov eax, g_itemUseFormPassed
|
||||
mov [esp], eax
|
||||
ret
|
||||
refuse:
|
||||
pop eax
|
||||
push 0x15E
|
||||
jmp eax
|
||||
}
|
||||
}
|
||||
|
||||
// StartAttack past its tests: 4.3.4 CombatModeEnter's dismount, in melee
|
||||
// reach only; out of reach it does not attack, and shows ERR_OUT_OF_RANGE
|
||||
// unless click-to-move is walking the player there (4.3.4's `silent`).
|
||||
bool __fastcall PrepareAttack(const void *player, void *target) {
|
||||
if (MountDisplay(player) <= 0 || !CanAutoDismount(player))
|
||||
return true;
|
||||
if (!Combat::Swing::InMeleeRange(const_cast<void *>(player), target)) {
|
||||
if (!reinterpret_cast<CanAutoInteract_t>(Offsets::FUN_PLAYER_CAN_AUTO_INTERACT)(player))
|
||||
reinterpret_cast<DisplayError_t>(Offsets::FUN_GAMEUI_DISPLAY_ERROR)(
|
||||
Offsets::GAME_ERROR_OUT_OF_RANGE);
|
||||
return false;
|
||||
}
|
||||
Dismount();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stands in for `mov eax,[esi+0xC8C]` at SITE_START_ATTACK_PASSED, player in
|
||||
// ESI, target in EDI: on a refusal, returns from StartAttack.
|
||||
__declspec(naked) void StartAttackPassedThunk() {
|
||||
__asm {
|
||||
mov ecx, esi
|
||||
mov edx, edi
|
||||
call PrepareAttack
|
||||
test al, al
|
||||
jz abort
|
||||
mov eax, [esi + 0xC8C]
|
||||
ret
|
||||
abort:
|
||||
mov eax, g_startAttackReturn
|
||||
mov [esp], eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The taxi map opener's tutorial call, its last step: then 4.3.4's
|
||||
// stand, dismount and form cancel for a map with more than one node.
|
||||
void __fastcall TaxiMapOpenedTutorial(int id, int arg) {
|
||||
reinterpret_cast<TriggerTutorial_t>(Offsets::FUN_TRIGGER_TUTORIAL)(id, arg);
|
||||
if (Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_TAXI_MAP_NODE_COUNT)) <= 1)
|
||||
return;
|
||||
const void *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return;
|
||||
if (StandState(player) != 0 && CanAutoStand(player))
|
||||
Stand(player);
|
||||
if (MountDisplay(player) > 0 && CanAutoDismount(player))
|
||||
Dismount();
|
||||
if (IsShapeShifted(player) && CanAutoUnshift(player))
|
||||
CancelForm(player);
|
||||
}
|
||||
|
||||
// 4.3.4 TypeBase::Use's step before CMSG_GAMEOBJ_USE: dismount unless the
|
||||
// object is usable mounted, then leave a form that cannot interact.
|
||||
void __fastcall PrepareObjectUse(const void *handler) {
|
||||
const void *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return;
|
||||
const uint8_t *object =
|
||||
Game::Read<const uint8_t *>(handler, Offsets::OFF_GAMEOBJECT_HANDLER_OBJECT);
|
||||
if (MountDisplay(player) > 0 && CanAutoDismount(player) &&
|
||||
!reinterpret_cast<UsableMounted_t>(Offsets::FUN_GAMEOBJECT_USABLE_MOUNTED)(
|
||||
object + Offsets::OFF_GAMEOBJECT_DESCRIPTOR))
|
||||
Dismount();
|
||||
if (UnshiftsForInteraction(player))
|
||||
CancelForm(player);
|
||||
}
|
||||
|
||||
// Stands in for `mov edi,0x7FF9E4` at SITE_GAMEOBJECT_USE_SEND, handler at
|
||||
// [EBP-4]. EAX, ECX and EDX are dead there.
|
||||
__declspec(naked) void UseSendThunk() {
|
||||
__asm {
|
||||
mov ecx, [ebp - 4]
|
||||
call PrepareObjectUse
|
||||
mov edi, 0x7FF9E4
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The mailbox and text use slots: the same step, then the type's own use.
|
||||
char __fastcall MailboxUse(void *handler) {
|
||||
PrepareObjectUse(handler);
|
||||
return reinterpret_cast<ObjectUse_t>(Offsets::FUN_GAMEOBJECT_MAILBOX_USE)(handler);
|
||||
}
|
||||
|
||||
char __fastcall TextUse(void *handler) {
|
||||
PrepareObjectUse(handler);
|
||||
return reinterpret_cast<ObjectUse_t>(Offsets::FUN_GAMEOBJECT_TEXT_USE)(handler);
|
||||
}
|
||||
|
||||
// The mount rung's read: the mount display, or 0 when the cast will
|
||||
// dismount first.
|
||||
int __fastcall MountDisplayForCast(const void *caster) {
|
||||
const int display = Game::Read<int>(Fields(caster), Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID);
|
||||
const int display = MountDisplay(caster);
|
||||
if (display > 0 && CanAutoDismount(caster)) {
|
||||
g_pending.dismount = true;
|
||||
return 0;
|
||||
@@ -141,9 +459,7 @@ __declspec(naked) void MountReadThunk() {
|
||||
// SITE_CASTER_STATE_STAND_CALL, caster in ECX: the stand state, or 0 when
|
||||
// the cast will stand first.
|
||||
int __fastcall StandStateForCast(const void *caster) {
|
||||
auto getStandState =
|
||||
(*reinterpret_cast<const GetStandState_t *const *>(caster))[kGetStandStateSlot];
|
||||
const int state = getStandState(caster, nullptr);
|
||||
const int state = StandState(caster);
|
||||
if (state != 0 && CanAutoStand(caster)) {
|
||||
g_pending.stand = true;
|
||||
return 0;
|
||||
@@ -184,12 +500,11 @@ char __fastcall CheckCasterState(void *caster, void *, const uint8_t *record, vo
|
||||
if (!passed)
|
||||
return 0;
|
||||
if (pending.stand)
|
||||
reinterpret_cast<SendStandState_t>(Offsets::FUN_PLAYER_SEND_STAND_STATE)(
|
||||
caster, nullptr, 0);
|
||||
Stand(caster);
|
||||
if (pending.dismount)
|
||||
Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOUNTED);
|
||||
Dismount();
|
||||
if (pending.unshift)
|
||||
Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOD_SHAPESHIFT, Form(caster));
|
||||
CancelForm(caster);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -201,6 +516,86 @@ void Register() {
|
||||
g_autoDismount = CVar::Factory::Register("autoDismount", "Automatically dismount when needed", Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
g_autoUnshift = CVar::Factory::Register("autoUnshift", "Automatically leave shapeshift form when needed", Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
|
||||
// Interaction. The use check passes the mount and form only together
|
||||
// with the step at every use; without it the object would be used still
|
||||
// mounted or shifted.
|
||||
Common::RedirectCall(Offsets::CALL_NPC_INTERACT_IS_SHAPESHIFTED,
|
||||
Offsets::FUN_UNIT_IS_SHAPESHIFTED,
|
||||
reinterpret_cast<const void *>(&IsShapeShiftedForTalk));
|
||||
static const uint8_t kUseSend[5] = {0xBF, 0xE4, 0xF9, 0x7F, 0x00};
|
||||
if (Common::ReplaceWithCall(Offsets::SITE_GAMEOBJECT_USE_SEND, kUseSend, sizeof kUseSend,
|
||||
reinterpret_cast<const void *>(&UseSendThunk)) &&
|
||||
Common::RedirectSlot(Offsets::SLOT_GAMEOBJECT_MAILBOX_USE,
|
||||
Offsets::FUN_GAMEOBJECT_MAILBOX_USE,
|
||||
reinterpret_cast<const void *>(&MailboxUse)) &&
|
||||
Common::RedirectSlot(Offsets::SLOT_GAMEOBJECT_TEXT_USE, Offsets::FUN_GAMEOBJECT_TEXT_USE,
|
||||
reinterpret_cast<const void *>(&TextUse))) {
|
||||
static const uint8_t kUseMountRead[6] = {0x8B, 0x88, 0xFC, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_GAMEOBJECT_CAN_USE_MOUNT_READ, kUseMountRead,
|
||||
sizeof kUseMountRead,
|
||||
reinterpret_cast<const void *>(&UseMountReadThunk));
|
||||
Common::RedirectCall(Offsets::CALL_GAMEOBJECT_CAN_USE_IS_SHAPESHIFTED,
|
||||
Offsets::FUN_UNIT_IS_SHAPESHIFTED,
|
||||
reinterpret_cast<const void *>(&IsShapeShiftedForQuery));
|
||||
}
|
||||
Common::RedirectCall(Offsets::CALL_UNIT_CAN_INTERACT_NOW_IS_SHAPESHIFTED,
|
||||
Offsets::FUN_UNIT_IS_SHAPESHIFTED,
|
||||
reinterpret_cast<const void *>(&IsShapeShiftedForQuery));
|
||||
|
||||
// Loot. The loot step passes the mount only together with LootUnit's
|
||||
// dismount.
|
||||
static const uint8_t kUnitStandCall[6] = {0xFF, 0x90, 0xA4, 0x00, 0x00, 0x00};
|
||||
static const uint8_t kCorpseStandCall[6] = {0xFF, 0x92, 0xA4, 0x00, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_UNIT_LOOT_STAND_CALL, kUnitStandCall,
|
||||
sizeof kUnitStandCall,
|
||||
reinterpret_cast<const void *>(&StandStateForLoot));
|
||||
Common::ReplaceWithCall(Offsets::SITE_CORPSE_LOOT_STAND_CALL, kCorpseStandCall,
|
||||
sizeof kCorpseStandCall,
|
||||
reinterpret_cast<const void *>(&StandStateForLoot));
|
||||
static const uint8_t kLootSend[6] = {0x6A, 0x00, 0x32, 0xD2, 0xB1, 0x01};
|
||||
static const uint8_t kLootMountRead[6] = {0x8B, 0x81, 0xFC, 0x01, 0x00, 0x00};
|
||||
if (Common::ReplaceWithCall(Offsets::SITE_LOOT_UNIT_SEND, kLootSend, sizeof kLootSend,
|
||||
reinterpret_cast<const void *>(&LootSendThunk)))
|
||||
Common::ReplaceWithCall(Offsets::SITE_UNIT_LOOT_MOUNT_READ, kLootMountRead,
|
||||
sizeof kLootMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEsiThunk));
|
||||
|
||||
// Item use.
|
||||
static const uint8_t kItemFormRefusal[5] = {0x68, 0x5E, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_ITEM_USE_FORM_REFUSAL, kItemFormRefusal,
|
||||
sizeof kItemFormRefusal,
|
||||
reinterpret_cast<const void *>(&ItemFormRefusalThunk));
|
||||
|
||||
// Attack. The mount reads pass only together with StartAttack's
|
||||
// dismount; without it the attack would start mounted.
|
||||
static const uint8_t kStartAttackPassed[6] = {0x8B, 0x86, 0x8C, 0x0C, 0x00, 0x00};
|
||||
if (Common::ReplaceWithCall(Offsets::SITE_START_ATTACK_PASSED, kStartAttackPassed,
|
||||
sizeof kStartAttackPassed,
|
||||
reinterpret_cast<const void *>(&StartAttackPassedThunk))) {
|
||||
static const uint8_t kEdxMountRead[6] = {0x8B, 0x82, 0xFC, 0x01, 0x00, 0x00};
|
||||
static const uint8_t kEcxMountRead[6] = {0x8B, 0x81, 0xFC, 0x01, 0x00, 0x00};
|
||||
static const uint8_t kEaxMountReadToEdx[6] = {0x8B, 0x90, 0xFC, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_START_ATTACK_MOUNT_READ, kEcxMountRead,
|
||||
sizeof kEcxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEsiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_RIGHT_CLICK_ATTACK_MOUNT_READ, kEdxMountRead,
|
||||
sizeof kEdxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEsiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_CURSOR_ATTACK_MOUNT_READ, kEdxMountRead,
|
||||
sizeof kEdxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEdiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_ATTACK_RESOLVER_MOUNT_READ, kEcxMountRead,
|
||||
sizeof kEcxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEdiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_ATTACK_RESOLVER_CAN_ATTACK_MOUNT_READ,
|
||||
kEaxMountReadToEdx, sizeof kEaxMountReadToEdx,
|
||||
reinterpret_cast<const void *>(&ResolverMountReadThunk));
|
||||
}
|
||||
|
||||
// Taxi.
|
||||
Common::RedirectCall(Offsets::CALL_TAXI_MAP_OPENED_TUTORIAL, Offsets::FUN_TRIGGER_TUTORIAL,
|
||||
reinterpret_cast<const void *>(&TaxiMapOpenedTutorial));
|
||||
|
||||
// The rungs only note what to do; the caster-check call runs it. Without
|
||||
// that call a passed rung would send the cast still mounted or seated.
|
||||
if (!Common::RedirectCall(Offsets::CALL_TRY_CAST_CHECK_CASTER_STATE,
|
||||
|
||||
+2
-3
@@ -20,6 +20,7 @@
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/Range.h"
|
||||
#include "unit/Position.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -147,10 +148,8 @@ static bool ReadSpellInfo(int spellID, int pet, SpellInfoData &out) {
|
||||
out.powerType = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
|
||||
using PowerHelper_t = int32_t(__fastcall *)(int spellID, int pet);
|
||||
using DisplayDivisor_t = uint32_t(__fastcall *)(int powerType);
|
||||
using CastTime_t = int32_t(__fastcall *)(int spellID, int pet, int allowNegative);
|
||||
const auto divisor = static_cast<int32_t>(reinterpret_cast<DisplayDivisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_POWER_DISPLAY_DIVISOR))(out.powerType));
|
||||
const auto divisor = static_cast<int32_t>(Unit::Power::DisplayDivisor(out.powerType));
|
||||
const int32_t cost = reinterpret_cast<PowerHelper_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_COST))(spellID, pet) / divisor;
|
||||
const int32_t costPerSecond = reinterpret_cast<PowerHelper_t>(
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
// type Enum.PowerType (0 mana, 1 rage, 2 focus, 3 energy,
|
||||
// 4 happiness) — the spell's PowerType (+0x7C)
|
||||
// name power token ("MANA", "RAGE", …)
|
||||
// cost effective cost (see above)
|
||||
// cost effective cost (see above), in displayed units — the
|
||||
// helper answers in stored units (rage in tenths), so it
|
||||
// is divided by `Unit::Power::DisplayDivisor`
|
||||
// minCost == cost; vanilla has no "optional" cost component
|
||||
// costPercent ManaCostPercent (+0x270) — the spell's %-of-base-
|
||||
// resource cost, or 0 for a flat cost
|
||||
@@ -49,7 +51,8 @@ namespace {
|
||||
|
||||
using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int pet);
|
||||
|
||||
|
||||
// The engine's cost helper, in STORED units (rage in tenths); 0xFFFFFFFF on
|
||||
// error. Callers divide by `Unit::Power::DisplayDivisor` before surfacing it.
|
||||
uint32_t EffectiveCost(int spellID) {
|
||||
return reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST)(spellID, 0);
|
||||
}
|
||||
@@ -60,11 +63,12 @@ int __fastcall Script_GetSpellPowerCost(void *L) {
|
||||
if (rec == nullptr)
|
||||
return 0; // nil — spell not found
|
||||
|
||||
const uint32_t cost = EffectiveCost(spellID);
|
||||
if (cost == 0xFFFFFFFF)
|
||||
const uint32_t storedCost = EffectiveCost(spellID);
|
||||
if (storedCost == 0xFFFFFFFF)
|
||||
return 0; // engine error (no player context, etc.)
|
||||
|
||||
const int powerType = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
const uint32_t cost = storedCost / Unit::Power::DisplayDivisor(powerType);
|
||||
const int costPercent = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_MANA_COST_PERCENT);
|
||||
if (cost == 0 && costPercent == 0)
|
||||
return 0; // nil — no resource cost
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
#include "cursor/Hover.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "event/Custom.h"
|
||||
@@ -88,6 +89,8 @@ Reach ReachOf(void *object) {
|
||||
return Reach::None;
|
||||
if (mode == Offsets::CURSOR_MODE_CUSTOM)
|
||||
return Cursor::Hover::IsUsable(object) ? Reach::Now : Reach::Out;
|
||||
if (Cursor::Extended::Is(mode))
|
||||
return Cursor::Extended::IsUnable(mode) ? Reach::Out : Reach::Now;
|
||||
const bool unable = mode > Offsets::CURSOR_MODE_LAST_NAMED;
|
||||
const uint32_t kind = unable ? mode - Offsets::CURSOR_MODE_UNABLE_OFFSET : mode;
|
||||
if (kind == 0 || kind > Offsets::CURSOR_MODE_LAST_NAMED || kind == Offsets::CURSOR_MODE_POINT ||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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/>.
|
||||
|
||||
#include "texture/BlpReader.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Texture {
|
||||
|
||||
namespace {
|
||||
|
||||
using Open_t = int(__thiscall *)(void *rec, const char *path, int flag);
|
||||
using Parse_t = int(__thiscall *)(void *rec, const void *file);
|
||||
using DecodeLevel_t = int(__thiscall *)(void *rec, uint32_t fmt, uint32_t level, void **out,
|
||||
uint32_t *size);
|
||||
using FreeScratch_t = void(__thiscall *)(void *rec, uint32_t level);
|
||||
using Reset_t = void(__thiscall *)(void *rec);
|
||||
|
||||
} // namespace
|
||||
|
||||
BlpReader::BlpReader(const char *path) {
|
||||
Init();
|
||||
open_ = reinterpret_cast<Open_t>(Offsets::FUN_BLP_OPEN)(rec_, path, 1) != 0;
|
||||
}
|
||||
|
||||
BlpReader::BlpReader(const void *file, std::size_t size) {
|
||||
Init();
|
||||
// The parse copies a whole header out of the file.
|
||||
if (file != nullptr && size >= Offsets::BLP_HEADER_SIZE)
|
||||
open_ = reinterpret_cast<Parse_t>(Offsets::FUN_BLP_PARSE)(rec_, file) != 0;
|
||||
}
|
||||
|
||||
BlpReader::~BlpReader() {
|
||||
if (decoded_)
|
||||
reinterpret_cast<FreeScratch_t>(Offsets::FUN_BLP_FREE_SCRATCH)(rec_, 0);
|
||||
reinterpret_cast<Reset_t>(Offsets::FUN_BLP_RESET)(rec_);
|
||||
}
|
||||
|
||||
uint32_t BlpReader::Width() const { return Game::Read<uint32_t>(rec_, Offsets::OFF_BLP_WIDTH); }
|
||||
|
||||
uint32_t BlpReader::Height() const { return Game::Read<uint32_t>(rec_, Offsets::OFF_BLP_HEIGHT); }
|
||||
|
||||
const uint8_t *BlpReader::DecodeBgra() {
|
||||
if (!open_)
|
||||
return nullptr;
|
||||
// The engine has no DXT5 converter: DecodeLevel would report success
|
||||
// with the buffer unwritten (BLP_ALPHA_TYPE_DXT5 in Offsets.h).
|
||||
if (rec_[Offsets::OFF_BLP_COMPRESSION] == Offsets::BLP_COMPRESSION_DXT &&
|
||||
rec_[Offsets::OFF_BLP_ALPHA_TYPE] == Offsets::BLP_ALPHA_TYPE_DXT5)
|
||||
return nullptr;
|
||||
void *out = nullptr;
|
||||
uint32_t size = 0;
|
||||
decoded_ = true;
|
||||
if (reinterpret_cast<DecodeLevel_t>(Offsets::FUN_BLP_DECODE_LEVEL)(
|
||||
rec_, Offsets::GX_TEX_FORMAT_BGRA8888, 0, &out, &size) == 0)
|
||||
return nullptr;
|
||||
return static_cast<const uint8_t *>(out);
|
||||
}
|
||||
|
||||
void BlpReader::Init() {
|
||||
std::memset(rec_, 0, sizeof(rec_));
|
||||
Put(Offsets::OFF_BLP_MAGIC, 0x32504C42); // 'BLP2'
|
||||
Put(Offsets::OFF_BLP_VERSION, 1);
|
||||
rec_[Offsets::OFF_BLP_ALPHA_TYPE] = 2;
|
||||
Put(Offsets::OFF_BLP_INIT_4A4, 100);
|
||||
}
|
||||
|
||||
void BlpReader::Put(uintptr_t off, uint32_t v) { std::memcpy(rec_ + off, &v, sizeof(v)); }
|
||||
|
||||
} // namespace Texture
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Texture {
|
||||
|
||||
// The engine's BLP reader, driven exactly as GetPortraitAlphaMask drives it
|
||||
// (see the FUN_BLP_OPEN block in Offsets.h). Open or parse in the
|
||||
// constructor, decode on demand, free and reset in the destructor.
|
||||
class BlpReader {
|
||||
public:
|
||||
// Reads the file at `path` (patch MPQs honoured). Only one reader made
|
||||
// this way may be live at a time: the engine keeps the file in one
|
||||
// module-static buffer.
|
||||
explicit BlpReader(const char *path);
|
||||
// Parses `size` bytes of a BLP file that stay valid and unchanged for
|
||||
// the reader's life: the open without the read (FUN_BLP_PARSE).
|
||||
BlpReader(const void *file, std::size_t size);
|
||||
~BlpReader();
|
||||
BlpReader(const BlpReader &) = delete;
|
||||
BlpReader &operator=(const BlpReader &) = delete;
|
||||
|
||||
bool IsOpen() const { return open_; }
|
||||
uint32_t Width() const;
|
||||
uint32_t Height() const;
|
||||
|
||||
// Level 0 as BGRA8888 (alpha in byte 3), valid until destruction; null
|
||||
// on failure, and for a DXT5 file, which the engine cannot convert.
|
||||
const uint8_t *DecodeBgra();
|
||||
|
||||
private:
|
||||
void Init();
|
||||
void Put(uintptr_t off, uint32_t v);
|
||||
|
||||
uint8_t rec_[Offsets::BLP_RECORD_SIZE];
|
||||
bool open_ = false;
|
||||
bool decoded_ = false;
|
||||
};
|
||||
|
||||
} // namespace Texture
|
||||
+55
-11
@@ -20,12 +20,28 @@
|
||||
// texture hash and inserts what it returns under the name and flags, so the
|
||||
// engine's own cache deduplicates and reference-counts the result like any
|
||||
// file's. A real file of the same name (a patch MPQ, a loose file) still wins.
|
||||
//
|
||||
// The texels come from two places:
|
||||
// - interface\cooldown\edge.blp is built here from its decoded texels;
|
||||
// - the files under src/embed/Textures sit at their game paths
|
||||
// (Texture::Builtin::Manifest), each the later client's own BLP. One
|
||||
// decodes the first time a load or Find asks for it, through the engine's
|
||||
// BLP reader on the DLL's copy (Texture::BlpReader), and its texels stay.
|
||||
|
||||
#include "texture/Builtin.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
#include "texture/BlpReader.h"
|
||||
#include "texture/Load.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace Texture::Builtin {
|
||||
|
||||
@@ -53,14 +69,24 @@ struct EdgeTexels {
|
||||
};
|
||||
const EdgeTexels g_edge;
|
||||
|
||||
// `path` names `file` (the loader's path, extension included), either slash, any case.
|
||||
bool IsPath(const char *path, const char *file) {
|
||||
for (; *file != '\0'; ++file, ++path) {
|
||||
const char c = *path == '/' ? '\\' : *path;
|
||||
if (c == '\0' || (c | 0x20) != (*file | 0x20))
|
||||
return false;
|
||||
}
|
||||
return *path == '\0';
|
||||
// One embedded file, decoded at most once. Like the edge texels, the texels
|
||||
// outlive every handle made from them.
|
||||
struct Embedded {
|
||||
bool tried = false;
|
||||
std::vector<uint32_t> texels;
|
||||
Image image = {};
|
||||
};
|
||||
Embedded g_embedded[Manifest::kFileCount];
|
||||
|
||||
void Decode(const Manifest::File &file, Embedded &out) {
|
||||
const Embed::Bytes bytes = Embed::File(file.resourceId, static_cast<size_t>(file.index));
|
||||
BlpReader blp(bytes.data, bytes.size);
|
||||
const uint8_t *bgra = blp.DecodeBgra();
|
||||
if (bgra == nullptr)
|
||||
return;
|
||||
out.texels.resize(size_t{blp.Width()} * blp.Height());
|
||||
std::memcpy(out.texels.data(), bgra, out.texels.size() * sizeof(uint32_t));
|
||||
out.image = {blp.Width(), blp.Height(), out.texels.data()};
|
||||
}
|
||||
|
||||
using CreateFromBlp_t = void *(__fastcall *)(const char *path, uint32_t fileFlag, uint32_t flags,
|
||||
@@ -69,9 +95,13 @@ CreateFromBlp_t CreateFromBlp_o = nullptr;
|
||||
|
||||
void *__fastcall CreateFromBlp_h(const char *path, uint32_t fileFlag, uint32_t flags, void *log) {
|
||||
void *handle = CreateFromBlp_o(path, fileFlag, flags, log);
|
||||
if (handle == nullptr && path != nullptr && IsPath(path, "interface\\cooldown\\edge.blp"))
|
||||
handle = CreateFromPixels(path, kEdgeSize, kEdgeSize, g_edge.texels, flags);
|
||||
return handle;
|
||||
if (handle != nullptr || path == nullptr)
|
||||
return handle;
|
||||
if (Common::SamePath(path, "interface\\cooldown\\edge.blp"))
|
||||
return CreateFromPixels(path, kEdgeSize, kEdgeSize, g_edge.texels, flags);
|
||||
if (const Image *image = Find(path))
|
||||
return CreateFromPixels(path, image->width, image->height, image->texels, flags);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _createFromBlpHook{Offsets::FUN_TEXTURE_CREATE_FROM_BLP,
|
||||
@@ -80,4 +110,18 @@ const Game::HookAutoRegister _createFromBlpHook{Offsets::FUN_TEXTURE_CREATE_FROM
|
||||
|
||||
} // namespace
|
||||
|
||||
const Image *Find(const char *path) {
|
||||
if (path == nullptr)
|
||||
return nullptr;
|
||||
const Manifest::File *file = Embed::FindFile(Manifest::kFiles, path);
|
||||
if (file == nullptr)
|
||||
return nullptr;
|
||||
Embedded &embedded = g_embedded[file - Manifest::kFiles];
|
||||
if (!embedded.tried) {
|
||||
embedded.tried = true;
|
||||
Decode(*file, embedded);
|
||||
}
|
||||
return embedded.texels.empty() ? nullptr : &embedded.image;
|
||||
}
|
||||
|
||||
} // namespace Texture::Builtin
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 Texture::Builtin {
|
||||
|
||||
// An embedded image: level 0 as 0xAARRGGBB texels (BGRA8888 in memory), row
|
||||
// by row, kept for the life of the DLL.
|
||||
struct Image {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
const uint32_t *texels;
|
||||
};
|
||||
|
||||
// The embedded BLP at the game path `path` (either slash, any case), decoded
|
||||
// on the first call; null when none is embedded there or it does not decode.
|
||||
// A texture load of the path that finds no file gets the same image.
|
||||
const Image *Find(const char *path);
|
||||
|
||||
} // namespace Texture::Builtin
|
||||
+11
-17
@@ -19,7 +19,7 @@
|
||||
// is the same: read POWER[type] / MAXPOWER[type] off the unit's
|
||||
// descriptor, defaulting `type` to the unit's primary power (the
|
||||
// `UNIT_FIELD_BYTES_0` byte 3 at `descriptor + 0x7B`), then divide
|
||||
// by the per-power-type display divisor (`VAR_UNIT_POWER_DIVISOR_TABLE`)
|
||||
// by the engine's per-power-type display divisor (`DisplayDivisor`)
|
||||
// before pushing. Rage internally stores 0..1000 but displays 0..100
|
||||
// (divisor 10); happiness uses divisor 1000. Same divisor logic
|
||||
// vanilla's own `Script_UnitMana` (`0x00517670`) applies.
|
||||
@@ -58,6 +58,12 @@ const char *PowerTypeToken(int type) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
unsigned int DisplayDivisor(int type) {
|
||||
using Divisor_t = unsigned int(__fastcall *)(int powerType);
|
||||
return reinterpret_cast<Divisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(type);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
@@ -73,18 +79,6 @@ const uint8_t *ResolveUnit(const char *token) {
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
}
|
||||
|
||||
// Per-power-type display divisor. Vanilla stores rage as 0..1000
|
||||
// internally and displays 0..100 (divide by 10); happiness is
|
||||
// stored at 1000x scale. Same table the engine's own `Script_UnitMana`
|
||||
// (`0x00517670`) reads at `VAR_UNIT_POWER_DIVISOR_TABLE`.
|
||||
uint32_t PowerDivisor(int type) {
|
||||
if (type < 0)
|
||||
return 1;
|
||||
auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_UNIT_POWER_DIVISOR_TABLE));
|
||||
return table[type];
|
||||
}
|
||||
|
||||
|
||||
// `Enum.PowerType` — modern Blizzard-style enum table. Slot 4 is
|
||||
// `Happiness` in 1.12 (pet happiness 0..3); modern retail relabels
|
||||
@@ -167,7 +161,7 @@ double RosterPowerFallback(void *L, RosterKind kind) {
|
||||
return 0.0;
|
||||
|
||||
const uint32_t divisor =
|
||||
Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(s.powerType);
|
||||
Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(s.powerType);
|
||||
const uint32_t curDisp = s.power / divisor;
|
||||
const uint32_t maxDisp = s.maxPower / divisor;
|
||||
switch (kind) {
|
||||
@@ -202,7 +196,7 @@ static int __fastcall Script_UnitPower(void *L) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
|
||||
// `unmodified` (arg 3, truthy) returns the raw internal value; otherwise
|
||||
// apply the per-type display divisor. Matches retail's third UnitPower arg.
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
|
||||
return 1;
|
||||
}
|
||||
@@ -223,7 +217,7 @@ static int __fastcall Script_UnitPowerMax(void *L) {
|
||||
}
|
||||
const uint32_t raw = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
|
||||
return 1;
|
||||
}
|
||||
@@ -255,7 +249,7 @@ static int __fastcall Script_UnitPowerMissing(void *L) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
|
||||
const uint32_t max = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
|
||||
const uint32_t curDisp = cur / divisor;
|
||||
const uint32_t maxDisp = max / divisor;
|
||||
Game::Lua::PushNumber(
|
||||
|
||||
@@ -20,4 +20,10 @@ namespace Unit::Power {
|
||||
// Used by `UnitPowerType`'s second return and by spell power-cost APIs.
|
||||
const char *PowerTypeToken(int type);
|
||||
|
||||
// The engine's divisor from a stored power amount to displayed units
|
||||
// (`FUN_POWER_DISPLAY_DIVISOR`): 10 for rage, 1000 for happiness, 1 otherwise.
|
||||
// Every power amount surfaced to Lua — unit power, spell costs, combat-log
|
||||
// amounts — goes through this.
|
||||
unsigned int DisplayDivisor(int type);
|
||||
|
||||
} // namespace Unit::Power
|
||||
|
||||
Reference in New Issue
Block a user