mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 07:30:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85c5c924a9 | ||
|
|
f2af2734f0 | ||
|
|
dc13d7bbfc | ||
|
|
99c0ab3ba3 | ||
|
|
87acb70c5b | ||
|
|
92a959c381 | ||
|
|
efc6b29b6b | ||
|
|
b09db4d5a5 | ||
|
|
0ce9d159e9 | ||
|
|
23b622acff | ||
|
|
f806f17f40 | ||
|
|
e9407f6564 | ||
|
|
f42f7459e2 | ||
|
|
968335debe | ||
|
|
04d4bf3326 | ||
|
|
c54556f6a1 | ||
|
|
bd14a93a11 | ||
|
|
5978d7dd62 | ||
|
|
67172c95f7 | ||
|
|
59c3bf9e0f | ||
|
|
54782df95e | ||
|
|
3ab690ac53 | ||
|
|
6b13368e65 | ||
|
|
5456295e2e | ||
|
|
9b34ceff9e | ||
|
|
a373a775cf | ||
|
|
e9014801b6 | ||
|
|
dc8a510611 | ||
|
|
d514dac465 | ||
|
|
621d1a9db9 | ||
|
|
7d566f3787 | ||
|
|
acc9a85bc6 | ||
|
|
207d47c8b8 | ||
|
|
76f48de67f | ||
|
|
72c4185026 | ||
|
|
d366fec43e | ||
|
|
c6d99cf843 | ||
|
|
39d22ac124 | ||
|
|
b497bb7fb0 | ||
|
|
4d5fe8d41b |
@@ -45,3 +45,4 @@ Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
|
||||
@@ -132,4 +132,9 @@ INSCRIPTION_AVAILABLE = false;
|
||||
EQUIPPED_FIRST = 1;
|
||||
EQUIPPED_LAST = 19;
|
||||
|
||||
BASE_MOVEMENT_SPEED = 7;
|
||||
BASE_MOVEMENT_SPEED = 7;
|
||||
|
||||
UPPER_LEFT_VERTEX = 1;
|
||||
LOWER_LEFT_VERTEX = 2;
|
||||
UPPER_RIGHT_VERTEX = 3;
|
||||
LOWER_RIGHT_VERTEX = 4;
|
||||
@@ -2,12 +2,15 @@
|
||||
-- 1.12 / Lua 5.0.
|
||||
--
|
||||
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
|
||||
-- The currency/cost helpers (C_CurrencyInfo etc.) are omitted. GetMoneyString
|
||||
-- uses text symbols, not coin texture markup (markup doesn't render in 1.12).
|
||||
-- GetMoneyString gives the text-symbol form ("12g 34s 56c"). The coin-icon form
|
||||
-- (GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString) is provided by the
|
||||
-- ClassicAPI DLL in C++ (src/currency/CoinText.cpp) — like retail, where it's a
|
||||
-- C function — now that the inline-texture backport renders |T markup in 1.12.
|
||||
--
|
||||
-- The FrameXML format-string globals these functions consume
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL, …) are
|
||||
-- defined in the locale layer (locales/enUS.lua), which loads first.
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL,
|
||||
-- GOLD_AMOUNT_TEXTURE, …) are defined in the locale layer (locales/enUS.lua),
|
||||
-- which loads first.
|
||||
|
||||
COPPER_PER_SILVER = 100;
|
||||
SILVER_PER_GOLD = 100;
|
||||
@@ -73,10 +76,9 @@ function SplitTextIntoHeaderAndNonHeader(text)
|
||||
end
|
||||
|
||||
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
|
||||
-- Text-symbol form ("12g 34s 56c"); the coin-texture form from the
|
||||
-- reference is dropped since texture markup doesn't render in 1.12. The
|
||||
-- checkGoldThreshold arg is accepted for signature compatibility but not
|
||||
-- applied (it relied on modern Constants tables).
|
||||
-- Text-symbol form ("12g 34s 56c"); for the coin-icon form use
|
||||
-- GetCoinTextureString below. The checkGoldThreshold arg is accepted for
|
||||
-- signature compatibility but not applied (it relied on modern Constants tables).
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
|
||||
local gold = math.floor(money / COPPER_PER_GOLD);
|
||||
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
TextureUtil = {};
|
||||
|
||||
function SetClampedTextureRotation(texture, rotationDegrees)
|
||||
if (rotationDegrees ~= 0 and rotationDegrees ~= 90 and rotationDegrees ~= 180 and rotationDegrees ~= 270) then
|
||||
error("SetRotation: rotationDegrees must be 0, 90, 180, or 270");
|
||||
return;
|
||||
end
|
||||
|
||||
if not (texture.rotationDegrees) then
|
||||
texture.origTexCoords = {texture:GetTexCoord()};
|
||||
texture.origWidth = texture:GetWidth();
|
||||
texture.origHeight = texture:GetHeight();
|
||||
end
|
||||
|
||||
if (texture.rotationDegrees == rotationDegrees) then
|
||||
return;
|
||||
end
|
||||
|
||||
texture.rotationDegrees = rotationDegrees;
|
||||
|
||||
if (rotationDegrees == 0 or rotationDegrees == 180) then
|
||||
texture:SetWidth(texture.origWidth);
|
||||
texture:SetHeight(texture.origHeight);
|
||||
else
|
||||
texture:SetWidth(texture.origHeight);
|
||||
texture:SetHeight(texture.origWidth);
|
||||
end
|
||||
|
||||
if (rotationDegrees == 0) then
|
||||
texture:SetTexCoord( texture.origTexCoords[1], texture.origTexCoords[2],
|
||||
texture.origTexCoords[3], texture.origTexCoords[4],
|
||||
texture.origTexCoords[5], texture.origTexCoords[6],
|
||||
texture.origTexCoords[7], texture.origTexCoords[8] );
|
||||
elseif (rotationDegrees == 90) then
|
||||
texture:SetTexCoord( texture.origTexCoords[3], texture.origTexCoords[4],
|
||||
texture.origTexCoords[7], texture.origTexCoords[8],
|
||||
texture.origTexCoords[1], texture.origTexCoords[2],
|
||||
texture.origTexCoords[5], texture.origTexCoords[6] );
|
||||
elseif (rotationDegrees == 180) then
|
||||
texture:SetTexCoord( texture.origTexCoords[7], texture.origTexCoords[8],
|
||||
texture.origTexCoords[5], texture.origTexCoords[6],
|
||||
texture.origTexCoords[3], texture.origTexCoords[4],
|
||||
texture.origTexCoords[1], texture.origTexCoords[2] );
|
||||
elseif (rotationDegrees == 270) then
|
||||
texture:SetTexCoord( texture.origTexCoords[5], texture.origTexCoords[6],
|
||||
texture.origTexCoords[1], texture.origTexCoords[2],
|
||||
texture.origTexCoords[7], texture.origTexCoords[8],
|
||||
texture.origTexCoords[3], texture.origTexCoords[4] );
|
||||
end
|
||||
end
|
||||
|
||||
function ClearClampedTextureRotation(texture)
|
||||
if (texture.rotationDegrees) then
|
||||
SetClampedTextureRotation(texture, 0);
|
||||
texture.origTexCoords = nil;
|
||||
texture.origWidth = nil;
|
||||
texture.origHeight = nil;
|
||||
texture.rotationDegrees = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function GetTexCoordsByGrid(xOffset, yOffset, textureWidth, textureHeight, gridWidth, gridHeight)
|
||||
local widthPerGrid = gridWidth / textureWidth;
|
||||
local heightPerGrid = gridHeight / textureHeight;
|
||||
return (xOffset - 1) * widthPerGrid, (xOffset) * widthPerGrid, (yOffset - 1) * heightPerGrid, (yOffset) * heightPerGrid;
|
||||
end
|
||||
|
||||
-- left/right/top/bottom are normalized [0..1] coords within the file (the
|
||||
-- markup's texel fields are produced by multiplying back up by the file size).
|
||||
function CreateTextureMarkup(file, fileWidth, fileHeight, width, height, left, right, top, bottom, xOffset, yOffset)
|
||||
return string.format("|T%s:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d|t",
|
||||
file
|
||||
, height
|
||||
, width
|
||||
, xOffset or 0
|
||||
, yOffset or 0
|
||||
, fileWidth
|
||||
, fileHeight
|
||||
, left * fileWidth
|
||||
, right * fileWidth
|
||||
, top * fileHeight
|
||||
, bottom * fileHeight
|
||||
);
|
||||
end
|
||||
|
||||
function CreateSimpleTextureMarkup(file, width, height, xOffset, yOffset)
|
||||
return string.format("|T%s:%d:%d:%d:%d|t",
|
||||
file
|
||||
, height or width
|
||||
, width
|
||||
, xOffset or 0
|
||||
, yOffset or 0
|
||||
);
|
||||
end
|
||||
@@ -754,8 +754,8 @@ local function ShowTradeSkillLink(link, text)
|
||||
frame.bar:SetMinMaxValues(0, (max and max > 0) and max or 1);
|
||||
frame.bar:SetValue(cur or 0);
|
||||
frame.barName:SetText(prof);
|
||||
frame.barRank:SetText(string.format("%d/%d", cur or 0, max or 0));
|
||||
frame.summary:SetText(string.format("%d of %d recipes known", known, total));
|
||||
frame.barRank:SetFormattedText("%d/%d", cur or 0, max or 0);
|
||||
frame.summary:SetFormattedText("%d of %d recipes known", known, total);
|
||||
frame.recipes = knownList;
|
||||
frame.subclassFilter = nil;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Cu";
|
||||
DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
@@ -31,6 +32,8 @@ FOCUSTARGET = "Focus Target";
|
||||
GEARSETS_POPUP_TEXT = "Enter Set Name (Max 16 Characters):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "All Icons";
|
||||
ICON_FILTER_ITEM = "Items";
|
||||
@@ -75,6 +78,7 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
|
||||
+4
-1
@@ -149,5 +149,8 @@ set(CMAKE_SHARED_LINKER_FLAGS
|
||||
# crash dumps back to functions. Published as a build/release artifact so
|
||||
# any crash from a shipped DLL can be triaged exactly. (/OPT:ICF folds
|
||||
# identical functions, so a few map symbols share an address — harmless.)
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP"
|
||||
# /DEF exports `Load` (VanillaFixes' post-injection entry point) undecorated
|
||||
# so its GetProcAddress(module, "Load") resolves. See src/ClassicAPI.def and
|
||||
# the loader-lock rationale in src/DllMain.cpp.
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP /DEF:${CMAKE_CURRENT_SOURCE_DIR}/src/ClassicAPI.def"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
@@ -29,13 +30,14 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
@@ -60,7 +62,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
@@ -70,7 +72,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
@@ -188,6 +190,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
| Item quality | `Enum.ItemQuality.{Poor,Common,Uncommon,Rare,Epic,Legendary,Artifact}` (0–6) |
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
|
||||
### Unit tokens
|
||||
|
||||
@@ -199,6 +203,17 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|
||||
### Bindings
|
||||
|
||||
ClassicAPI backports the later-client direct-action and temporary override
|
||||
binding families. Permanent bindings use the standard `SPELL`, `ITEM`,
|
||||
`MACRO`, and `CLICK` command strings and can be saved normally. Overrides are
|
||||
session-only, frame-owned, and support the usual priority flag.
|
||||
|
||||
See the [Bindings API reference](docs/API.md#bindings) for signatures,
|
||||
precedence, execution behavior, macro-text usage, and 1.12 compatibility
|
||||
notes.
|
||||
|
||||
#### Predefined focus bindings
|
||||
|
||||
Injected into the engine's **Targeting Functions** group at FrameXML
|
||||
Bindings.xml load time, so they appear in the keybind UI alongside
|
||||
native targeting bindings instead of orphaned at the bottom.
|
||||
@@ -208,7 +223,8 @@ native targeting bindings instead of orphaned at the bottom.
|
||||
| `FOCUSTARGET` | `FocusUnit("target")` — pin current target as focus |
|
||||
| `TARGETFOCUS` | `TargetUnit("focus")` — switch target to the focus |
|
||||
|
||||
See [Focus / Bindings](docs/API.md#bindings-focustarget--targetfocus) for the implementation note.
|
||||
See [Predefined focus bindings](docs/API.md#predefined-focus-bindings-focustarget--targetfocus)
|
||||
for the implementation note.
|
||||
|
||||
### Behaviors
|
||||
|
||||
@@ -217,6 +233,7 @@ Transparent engine tweaks — no API to call, they just fix a vanilla limitation
|
||||
| Tweak | Effect |
|
||||
|-------|--------|
|
||||
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) as icons in FontStrings, chat, and tooltips, the way 4.3.4+ clients do. Vanilla 1.12 shows the raw escape as literal text instead. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
+604
-7
@@ -79,6 +79,9 @@ build instructions.
|
||||
- [`C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)`](#c_creatureinfogetcreaturetypeinfocreaturetypeid)
|
||||
- [`C_CreatureInfo.GetCreatureTypeIDs()`](#c_creatureinfogetcreaturetypeids)
|
||||
|
||||
- [Currency](#currency)
|
||||
- [`GetCoinTextureString(amount [, fontHeight])` / `C_CurrencyInfo.GetCoinTextureString(amount [, fontHeight])`](#getcointexturestringamount--fontheight--c_currencyinfogetcointexturestringamount--fontheight)
|
||||
|
||||
- [CVar](#cvar)
|
||||
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
|
||||
|
||||
@@ -162,6 +165,18 @@ build instructions.
|
||||
- [`region:IsDragging()`](#regionisdragging)
|
||||
- [`GetMouseFoci()`](#getmousefoci)
|
||||
- [`frame:SetShown(shown)`](#framesetshownshown)
|
||||
- [`fontstring:GetStringHeight()`](#fontstringgetstringheight)
|
||||
- [`fontstring:GetUnboundedStringWidth()`](#fontstringgetunboundedstringwidth)
|
||||
- [`fontstring:GetWrappedWidth()`](#fontstringgetwrappedwidth)
|
||||
- [`fontstring:GetNumLines()`](#fontstringgetnumlines)
|
||||
- [`fontstring:GetLineHeight()`](#fontstringgetlineheight)
|
||||
- [`fontstring:IsTruncated()`](#fontstringistruncated)
|
||||
- [`fontstring:SetMaxLines(maxLines)` / `fontstring:GetMaxLines()`](#fontstringsetmaxlinesmaxlines--fontstringgetmaxlines)
|
||||
- [`fontstring:SetFormattedText(format [, ...])`](#fontstringsetformattedtextformat--)
|
||||
- [`texture:SetRotation(angle [, cx, cy])`](#texturesetrotationangle--cx-cy)
|
||||
- [`texture:SetVertexOffset(vertexIndex, offsetX, offsetY)`](#texturesetvertexoffsetvertexindex-offsetx-offsety)
|
||||
- [`texture:SetColorTexture(colorR, colorG, colorB [, a])`](#texturesetcolortexturecolorr-colorg-colorb--a)
|
||||
- [`fontstring:SetRotation(angle [, cx, cy])`](#fontstringsetrotationangle--cx-cy)
|
||||
- [`frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`](#framesetresizeboundsminwidth-minheight--maxwidth-maxheight)
|
||||
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
|
||||
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
|
||||
@@ -217,6 +232,8 @@ build instructions.
|
||||
- [`Enum.ItemClass`](#enumitemclass)
|
||||
- [`Enum.ItemQuality`](#enumitemquality)
|
||||
- [`Enum.PowerType`](#enumpowertype)
|
||||
- [`Enum.SpellBookSpellBank`](#enumspellbookspellbank)
|
||||
- [`Enum.SpellBookItemType`](#enumspellbookitemtype)
|
||||
|
||||
- [Glue](#glue)
|
||||
- [`C_Glue.IsFirstLoadThisSession()`](#c_glueisfirstloadthissession)
|
||||
@@ -375,6 +392,10 @@ build instructions.
|
||||
- [`C_MerchantFrame.IsMerchantItemRefundable(slot)`](#c_merchantframeismerchantitemrefundableslot)
|
||||
- [`C_MerchantFrame.IsSellAllJunkEnabled()`](#c_merchantframeissellalljunkenabled)
|
||||
|
||||
- [Model](#model)
|
||||
- [`model:SetDisplayInfo(creatureDisplayID)`](#modelsetdisplayinfocreaturedisplayid)
|
||||
- [`model:SetCreature(creatureID)`](#modelsetcreaturecreatureid)
|
||||
|
||||
- [ChatBubbles](#chatbubbles)
|
||||
- [`C_ChatBubbles.GetAllChatBubbles([includeForbidden])`](#c_chatbubblesgetallchatbubblesincludeforbidden)
|
||||
|
||||
@@ -456,7 +477,7 @@ build instructions.
|
||||
- [`IsHarmfulSpell(spell)` / `IsHelpfulSpell(spell)`](#isharmfulspellspell--ishelpfulspellspell)
|
||||
- [`C_Spell.IsSpellHarmful(spellID)` / `C_Spell.IsSpellHelpful(spellID)`](#c_spellisspellharmfulspellid--c_spellisspellhelpfulspellid)
|
||||
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
|
||||
- [`CastSpellNoToggle(name | spellID)`](#castspellnotogglename--spellid)
|
||||
- [`CastSpellNoToggle(name | spellID [, unit])`](#castspellnotogglename--spellid--unit)
|
||||
- [`C_Spell.CastAtCursor(spellIDOrName)`](#c_spellcastatcursorspellidorname)
|
||||
- [`C_Spell.CastAtUnit(spellIDOrName, unit)`](#c_spellcastatunitspellidorname-unit)
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
@@ -467,6 +488,7 @@ build instructions.
|
||||
|
||||
- [SpellBook](#spellbook)
|
||||
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
|
||||
- [`C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)`](#c_spellbookgetspellbookiteminfoslotindex-spellbank)
|
||||
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
|
||||
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
|
||||
- [`C_SpellBook.GetSkillLineName(skillLineID)`](#c_spellbookgetskilllinenameskilllineid)
|
||||
@@ -547,6 +569,7 @@ build instructions.
|
||||
- [`UnitSubName(unit)`](#unitsubnameunit)
|
||||
- [`UnitCreatureFamilyID(unit)`](#unitcreaturefamilyidunit)
|
||||
- [`UnitCreatureTypeID(unit)`](#unitcreaturetypeidunit)
|
||||
- [`UnitCreatureID(unit)`](#unitcreatureidunit)
|
||||
- [`GetUnitSpeed(unit)`](#getunitspeedunit)
|
||||
- [`UnitClassBase(unit)`](#unitclassbaseunit)
|
||||
- [`UnitRaceBase(unit)`](#unitracebaseunit)
|
||||
@@ -570,6 +593,7 @@ build instructions.
|
||||
- [`UnitPowerMissing(unit [, powerType [, unmodified]])`](#unitpowermissingunit--powertype--unmodified)
|
||||
- [`UnitPowerType(unit)`](#unitpowertypeunit)
|
||||
- [`UnitSpellHaste(unit)`](#unitspellhasteunit)
|
||||
- [`UnitSpellTargetName(unit)`](#unitspelltargetnameunit)
|
||||
|
||||
- [UnitAuras](#unitauras)
|
||||
- [`C_UnitAuras.GetAuraDataByIndex(unit, index [, filter])`](#c_unitaurasgetauradatabyindexunit-index--filter)
|
||||
@@ -2008,6 +2032,35 @@ local ids = C_CreatureInfo.GetCreatureTypeIDs()
|
||||
-- { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }
|
||||
```
|
||||
|
||||
## Currency
|
||||
|
||||
### `GetCoinTextureString(amount [, fontHeight])` / `C_CurrencyInfo.GetCoinTextureString(amount [, fontHeight])`
|
||||
|
||||
Returns the amount of copper as a money string with real coin icons —
|
||||
for example `"12<gold> 34<silver> 56<copper>"`. The icons are inline
|
||||
`|T…|t` markup over vanilla's `UI-MoneyIcons` sprite sheet, rendered by
|
||||
ClassicAPI's inline-texture backport. Both names call the same C
|
||||
function, like retail.
|
||||
|
||||
Only the non-zero denominations appear, in gold → silver → copper
|
||||
order. An `amount` of `0` returns `"0<copper>"`. A negative or
|
||||
fractional `amount` is clamped to `0` / rounded.
|
||||
|
||||
`fontHeight` sets the icon height in UI pixels. When omitted (or `0`),
|
||||
each coin sizes itself to the font of the fontstring that shows the
|
||||
string — the retail default.
|
||||
|
||||
```lua
|
||||
GetCoinTextureString(123456) -- "12g 34s 56c" with coin icons
|
||||
GetCoinTextureString(5) -- "5c" with a copper icon
|
||||
GetCoinTextureString(5, 24) -- same, with a 24px coin
|
||||
```
|
||||
|
||||
The per-denomination formats are the GlobalStrings
|
||||
`GOLD_AMOUNT_TEXTURE` / `SILVER_AMOUNT_TEXTURE` /
|
||||
`COPPER_AMOUNT_TEXTURE`, backported in the embedded `!!!ClassicAPI`
|
||||
addon's locale layer — a locale can override them.
|
||||
|
||||
## CVar
|
||||
|
||||
### `C_CVar.GetCVarBool(cvar)`
|
||||
@@ -3760,7 +3813,118 @@ after `"target"` (matching retail order), so a focused unit's GUID
|
||||
reverse-resolves to `"focus"` only if it isn't already addressable
|
||||
as `"player"` / `"party*"` / `"raid*"` / `"nameplate*"` / `"target"`.
|
||||
|
||||
### Bindings (`FOCUSTARGET` / `TARGETFOCUS`)
|
||||
## Bindings
|
||||
|
||||
Backports the direct-action and temporary override binding APIs added in WoW
|
||||
2.0. The permanent helpers write later-client action command strings into
|
||||
vanilla's native binding table; the override helpers add a separate,
|
||||
session-only layer in front of that table.
|
||||
|
||||
### Permanent direct-action bindings
|
||||
|
||||
| Function | Equivalent command |
|
||||
|----------|--------------------|
|
||||
| `ok = SetBindingSpell(key, spell)` | `SetBinding(key, "SPELL " .. spell)` |
|
||||
| `ok = SetBindingItem(key, item)` | `SetBinding(key, "ITEM " .. item)` |
|
||||
| `ok = SetBindingMacro(key, macroNameOrIndex)` | `SetBinding(key, "MACRO " .. macroNameOrIndex)` |
|
||||
| `ok = SetBindingClick(key, buttonName [, mouseButton])` | `SetBinding(key, "CLICK " .. buttonName .. (mouseButton and ":" .. mouseButton or ""))` |
|
||||
|
||||
`key` is any binding string accepted by the client, such as `"CTRL-F"`,
|
||||
`"SHIFT-BUTTON4"`, or `"F8"`. `spell`, `item`, `macroNameOrIndex`, and
|
||||
`buttonName` identify the action to perform. `macroNameOrIndex` accepts either
|
||||
a saved macro's name or its decimal index. `mouseButton` is passed to the
|
||||
button's click handler and defaults to `"LeftButton"`; when supplied, it is
|
||||
stored as the `:MouseButton` suffix of the `CLICK` command.
|
||||
|
||||
Each helper returns `1` if vanilla's `SetBinding` changed the binding and
|
||||
`nil` otherwise. It changes whichever binding set is currently loaded, fires
|
||||
vanilla's normal `UPDATE_BINDINGS` notification, and is not persisted until
|
||||
the addon calls `SaveBindings`. Use vanilla `SetBinding(key)` to unbind a key;
|
||||
the typed helpers always require an action argument.
|
||||
|
||||
The command hook also understands manually constructed `SPELL`, `ITEM`,
|
||||
`MACRO`, and `CLICK` strings passed directly to vanilla `SetBinding`.
|
||||
|
||||
### Temporary override bindings
|
||||
|
||||
```lua
|
||||
SetOverrideBinding(owner, isPriority, key [, command])
|
||||
SetOverrideBindingSpell(owner, isPriority, key, spell)
|
||||
SetOverrideBindingItem(owner, isPriority, key, item)
|
||||
SetOverrideBindingMacro(owner, isPriority, key, macroNameOrIndex)
|
||||
SetOverrideBindingClick(owner, isPriority, key, buttonName [, mouseButton])
|
||||
ClearOverrideBindings(owner)
|
||||
```
|
||||
|
||||
`owner` must be a frame and `isPriority` must be a Boolean. The typed helpers
|
||||
construct the same four action strings shown above. The generic
|
||||
`SetOverrideBinding` also accepts ordinary Bindings.xml command names, which
|
||||
are delegated to vanilla's command executor. Override setters and
|
||||
`ClearOverrideBindings` return no values.
|
||||
|
||||
Passing no `command` (or `nil`) to `SetOverrideBinding` removes that owner's
|
||||
override for `key`. `ClearOverrideBindings(owner)` removes every override
|
||||
belonging to that owner. Overrides never change or save the native binding
|
||||
table and are cleared whenever the FrameScript Lua state is recreated, such
|
||||
as on `/reload` or logout.
|
||||
|
||||
Resolution order is:
|
||||
|
||||
1. Priority override bindings.
|
||||
2. Non-priority override bindings.
|
||||
3. The normal native binding.
|
||||
|
||||
When multiple owners set the same key at the same priority, the most recently
|
||||
set override wins. Removing it immediately reveals the next matching override
|
||||
or the unchanged native binding.
|
||||
|
||||
`key` is matched case-insensitively, and its modifiers are accepted in any
|
||||
order: `"ctrl-alt-F"`, `"ALT-CTRL-F"`, and `"Alt-Ctrl-F"` all bind the same
|
||||
key. The override layer normalizes the key to the engine's own keypress form
|
||||
(`ALT-CTRL-SHIFT-` before the base key) before matching, so the override fires
|
||||
regardless of how the caller spelled it.
|
||||
|
||||
A generic `SetOverrideBinding` command that is an ordinary Bindings.xml command
|
||||
runs through the engine's own command executor with the same execution context
|
||||
a native keypress establishes, so it behaves like the equivalent permanent
|
||||
binding.
|
||||
|
||||
### Action execution
|
||||
|
||||
| Command | Execution path |
|
||||
|---------|----------------|
|
||||
| `SPELL name` | Calls vanilla `CastSpellByName(name)`. The usual `/cast` spell-name syntax is accepted. |
|
||||
| `ITEM item` | Calls `C_Item.UseItemByName(item)`, accepting the names, item strings, and item IDs supported by that API. |
|
||||
| `MACRO nameOrIndex` | Resolves a saved macro by name or decimal index. An addon-provided `RunMacro` is used when available; otherwise ClassicAPI resolves it with `GetMacroInfo` and dispatches its body through `ChatEdit_ParseText`. |
|
||||
| `CLICK ButtonName[:MouseButton]` | Resolves the named global frame and calls its ordinary `:Click(mouseButton)` method. The default is `LeftButton`. |
|
||||
|
||||
`SetBindingMacro` and `SetOverrideBindingMacro` take a saved macro identifier,
|
||||
not literal macro text. To bind literal macro text, bind a named button whose
|
||||
ClassicAPI attributes describe a macro action:
|
||||
|
||||
```lua
|
||||
local button = CreateFrame("Button", "MyMacroTextBindingButton")
|
||||
button:SetAttribute("type", "macro")
|
||||
button:SetAttribute("macrotext", "/say Bound macro text")
|
||||
|
||||
SetBindingClick("CTRL-F7", button:GetName())
|
||||
```
|
||||
|
||||
### 1.12 compatibility notes
|
||||
|
||||
- There is no combat lockdown or secure execution. These functions remain
|
||||
callable in combat, and `CLICK` invokes the frame's ordinary click path.
|
||||
- Direct actions run on key-down. The corresponding key-up is consumed; a
|
||||
click binding does not emulate later clients' separate mouse-down and
|
||||
mouse-up delivery.
|
||||
- The override layer is not reflected by vanilla `GetBindingAction`.
|
||||
In particular, the later-client `GetBindingAction(key, true)` override query
|
||||
is not backported.
|
||||
- Override ownership is explicit. Call `ClearOverrideBindings(owner)` when the
|
||||
addon's owner is no longer needed; all remaining overrides are also cleared
|
||||
when the Lua state is recreated.
|
||||
|
||||
### Predefined focus bindings (`FOCUSTARGET` / `TARGETFOCUS`)
|
||||
|
||||
Two key bindings appear in the keybind UI (`Esc` → Key Bindings)
|
||||
under the **Targeting Functions** group, between `PETATTACK` and the
|
||||
@@ -3879,6 +4043,225 @@ end
|
||||
frames, textures and fontstrings (each branch has its own engine
|
||||
Show/Hide implementation).
|
||||
|
||||
### `fontstring:GetStringHeight()`
|
||||
|
||||
This is the companion to vanilla's `GetStringWidth`. Blizzard first
|
||||
shipped it in 2.3.0. It returns the height of the rendered text, in
|
||||
UI pixels. The result includes word wrap: wrapped text measures
|
||||
`lines × fontHeight + (lines − 1) × spacing` (the `SetSpacing`
|
||||
value). Empty or unset text returns `0`.
|
||||
|
||||
```lua
|
||||
local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
f:SetWidth(100)
|
||||
f:SetText("a long line that wraps a few times")
|
||||
frame:SetHeight(f:GetStringHeight() + 16) -- size the box to the text
|
||||
```
|
||||
|
||||
Related: the stock `fontstring:GetStringWidth()` now **includes inline
|
||||
`|T…|t` icon widths**, so the measured width matches the rendered width.
|
||||
Editbox text is not adjusted — an editbox shows and measures raw markup.
|
||||
|
||||
### `fontstring:GetUnboundedStringWidth()`
|
||||
|
||||
Returns the width of the text on one line, in UI pixels, with no wrap
|
||||
or size limit applied. Note: `GetStringWidth` ALSO ignores word wrap —
|
||||
that is a classic API trap on every client. The two methods differ
|
||||
only on modern clients, and only for text truncated with an ellipsis:
|
||||
there `GetStringWidth` reports the displayed width and this method
|
||||
reports the full string. On this client the two always agree; the
|
||||
method exists so modern code that calls it works. The width of the
|
||||
text AS RENDERED (the widest wrapped line) is `GetWrappedWidth`, below.
|
||||
Inline `|T…|t` icon widths are included. Empty or unset text returns
|
||||
`0`.
|
||||
|
||||
### `fontstring:GetWrappedWidth()`
|
||||
|
||||
Returns the width of the text as it renders, in UI pixels: the width
|
||||
of the widest wrapped line. This is the method to use for the visible
|
||||
size of wrapping text — `GetStringWidth` and `GetUnboundedStringWidth`
|
||||
both measure the string on one line and ignore wrapping. Later clients
|
||||
added the method. Text that does not wrap returns the same value as
|
||||
`GetStringWidth`. Inline `|T…|t` icons are counted. Empty or unset
|
||||
text returns `0`.
|
||||
|
||||
The wrapped width can carry a ≤1px difference from a direct measure of
|
||||
the same line text — the same rounding noise `GetStringWidth` itself
|
||||
carries.
|
||||
|
||||
```lua
|
||||
local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
f:SetWidth(100)
|
||||
f:SetText("a long line that wraps a few times")
|
||||
-- f:GetStringWidth() -> ~165 (one line, ignores wrap)
|
||||
-- f:GetWrappedWidth() -> ~94 (the widest rendered line)
|
||||
```
|
||||
|
||||
### `fontstring:GetNumLines()`
|
||||
|
||||
Returns the number of wrapped lines the text renders as. Later
|
||||
clients added the method. It works whether or not the text has
|
||||
rendered yet. Empty or unset text returns `0`.
|
||||
|
||||
### `fontstring:GetLineHeight()`
|
||||
|
||||
Returns the height of one text line in UI pixels — the font height,
|
||||
without the `SetSpacing` value. Later clients added the method. Use
|
||||
it with `GetNumLines` and `GetSpacing` to reconstruct
|
||||
`GetStringHeight` per line.
|
||||
|
||||
### `fontstring:IsTruncated()`
|
||||
|
||||
Returns `true` when the engine cut the text off with an ellipsis
|
||||
(`...`). Later clients added the method. The engine truncates only
|
||||
when the text does not fit a bounded box. The fontstring needs a
|
||||
width and a height, and the text must overflow that box. Vanilla
|
||||
fontstrings always wrap at spaces. So a box that is only one line
|
||||
tall truncates, but a taller box wraps the text and fits it.
|
||||
|
||||
The result reflects what is on screen, so the fontstring must have
|
||||
drawn at least once. `GetText` still returns the full text, not the
|
||||
cut-off line.
|
||||
|
||||
```lua
|
||||
local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
f:SetWidth(60)
|
||||
f:SetHeight(14) -- one line tall
|
||||
f:SetText("a name too long to fit")
|
||||
-- after it draws: f:IsTruncated() -> true (draws "a name too...")
|
||||
```
|
||||
|
||||
### `fontstring:SetMaxLines(maxLines)` / `fontstring:GetMaxLines()`
|
||||
|
||||
Caps the fontstring to `maxLines` wrapped lines. Text past the cap
|
||||
is cut off with an ellipsis (`...`). Pass `0` (or nothing) for no
|
||||
cap. Later clients added the method for fontstrings.
|
||||
|
||||
This is the way to make a truncating label in 1.12. Vanilla
|
||||
fontstrings always wrap at spaces, and there is no `SetWordWrap`. So
|
||||
`SetMaxLines(1)` gives a single-line label that ends in `...` when
|
||||
the text is too wide, even inside a tall box.
|
||||
|
||||
The cap needs a width to take effect: the text must have an edge to
|
||||
overflow. `GetMaxLines` returns the current cap (`0` when unset). Use
|
||||
`IsTruncated` to check whether the cap actually cut the text.
|
||||
|
||||
```lua
|
||||
local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
f:SetWidth(120)
|
||||
f:SetMaxLines(1) -- one line, then "..."
|
||||
f:SetText("a really long guild or player name that will not fit")
|
||||
-- draws "a really long gu..." ; f:IsTruncated() -> true
|
||||
```
|
||||
|
||||
### `fontstring:SetFormattedText(format [, ...])`
|
||||
|
||||
Sets the text to `string.format(format, ...)`. Later clients added
|
||||
the method as a convenience over `SetText(format(...))`. A bad format
|
||||
string raises a normal Lua error. The set goes through the same engine
|
||||
path as `SetText`, so escape handling is identical.
|
||||
|
||||
```lua
|
||||
f:SetFormattedText("%d/%d (%.1f%%)", cur, max, cur / max * 100)
|
||||
```
|
||||
|
||||
### `texture:SetRotation(angle [, cx, cy])`
|
||||
|
||||
Rotates a texture by `angle` radians. A positive angle turns the texture
|
||||
counter-clockwise. Later clients added this method. Vanilla has no texture
|
||||
rotation of its own.
|
||||
|
||||
The optional `cx, cy` set the pivot point, as a normalized position inside the
|
||||
texture from `0` to `1`. The default pivot is the center, `(0.5, 0.5)`. An
|
||||
angle of `0` clears the rotation and returns the texture to upright.
|
||||
|
||||
The method turns the four corners of the drawn quad, so the whole texture stays
|
||||
visible and no corner is clipped. It works on any texture, including
|
||||
engine-created ones. The rotation holds across moves and resizes, and costs
|
||||
nothing per frame while it stays still. `GetRotation()` returns the current
|
||||
angle in radians.
|
||||
|
||||
```lua
|
||||
local t = frame:CreateTexture(nil, "ARTWORK")
|
||||
t:SetAllPoints(frame)
|
||||
t:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
t:SetRotation(math.rad(45)) -- 45 degrees counter-clockwise
|
||||
t:SetRotation(math.pi, 0, 1) -- half turn around the top-left corner
|
||||
```
|
||||
|
||||
### `texture:SetVertexOffset(vertexIndex, offsetX, offsetY)`
|
||||
|
||||
Moves one corner of a texture by `offsetX, offsetY` pixels. `vertexIndex` picks
|
||||
the corner: `UPPER_LEFT_VERTEX`, `LOWER_LEFT_VERTEX`, `UPPER_RIGHT_VERTEX`, or
|
||||
`LOWER_RIGHT_VERTEX` (values 1 to 4). `+x` is right and `+y` is up. Vanilla has
|
||||
no way to move a texture's corners. This backport matches retail.
|
||||
|
||||
Moving the corners warps the whole quad, so the texture and its background move
|
||||
together. It suits skew, trapezoid, and fake-perspective effects, and waving
|
||||
animations. It composes with `SetRotation`: the texture rotates first, then the
|
||||
offsets apply. The offset holds across moves, resizes, and `SetTexture`, and
|
||||
costs nothing per frame while it stays still.
|
||||
|
||||
`GetVertexOffset(vertexIndex)` returns the current `offsetX, offsetY` for a
|
||||
corner (`0, 0` if unset).
|
||||
|
||||
Note: an offset that turns the quad inside-out — mirroring it past its opposite
|
||||
edge — is not drawn. The engine skips a back-facing quad, so keep the warp
|
||||
within a non-mirrored shape.
|
||||
|
||||
```lua
|
||||
local t = frame:CreateTexture(nil, "ARTWORK")
|
||||
t:SetAllPoints(frame)
|
||||
t:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
-- pull the top two corners inward: a trapezoid (fake perspective)
|
||||
t:SetVertexOffset(UPPER_LEFT_VERTEX, 20, 0)
|
||||
t:SetVertexOffset(UPPER_RIGHT_VERTEX, -20, 0)
|
||||
```
|
||||
|
||||
### `texture:SetColorTexture(colorR, colorG, colorB [, a])`
|
||||
|
||||
Fills the texture with a solid color. Each channel is `0` to `1`. The alpha `a`
|
||||
is optional and defaults to `1` (opaque). This is the 7.0 name for a fill that
|
||||
vanilla already does — `SetTexture(r, g, b [, a])` with numbers instead of a
|
||||
path. The backport is the same engine call under a second name, so the clamping
|
||||
and the opaque default match the engine.
|
||||
|
||||
```lua
|
||||
local t = frame:CreateTexture(nil, "BACKGROUND")
|
||||
t:SetAllPoints(frame)
|
||||
t:SetColorTexture(0.1, 0.6, 1.0, 0.8) -- semi-transparent blue fill
|
||||
```
|
||||
|
||||
### `fontstring:SetRotation(angle [, cx, cy])`
|
||||
|
||||
Rotates a FontString's text by `angle` radians. A positive angle turns the text
|
||||
counter-clockwise. Later clients added this method. Vanilla has no text rotation
|
||||
of its own.
|
||||
|
||||
The optional `cx, cy` set the pivot point, as a normalized position inside the
|
||||
text from `0` to `1`. The default pivot is the center, `(0.5, 0.5)`. An angle of
|
||||
`0` clears the rotation and returns the text to upright.
|
||||
|
||||
The rotation is visual only. `GetStringWidth`, `GetStringHeight`, `GetRect`, and
|
||||
`SetPoint` all stay axis-aligned, so a rotated label measures and anchors as if
|
||||
it were upright. This matches retail. `GetRotation()` returns the current angle
|
||||
in radians.
|
||||
|
||||
The method turns the glyph vertices, so the whole text stays visible. It works on
|
||||
any FontString, and the rotation holds across text changes, moves, and resizes.
|
||||
It costs nothing per frame while the text stays still.
|
||||
|
||||
Inline `|T…|t` icons inside a rotated FontString do not turn with the text. They
|
||||
draw as separate anchored regions. Plain text rotates correctly.
|
||||
|
||||
```lua
|
||||
local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
||||
fs:SetPoint("CENTER")
|
||||
fs:SetText("Rotate me")
|
||||
fs:SetRotation(math.rad(45)) -- 45 degrees counter-clockwise
|
||||
fs:SetRotation(0) -- back to upright
|
||||
```
|
||||
|
||||
### `frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`
|
||||
|
||||
The modern rename of vanilla's `SetMinResize` / `SetMaxResize` pair.
|
||||
@@ -4034,9 +4417,9 @@ modifier/button-qualified, same precedence as `type`):
|
||||
| `target` | — | Targets the `unit` (or clears the target if `unit` is `"none"`). Respects the engine's default-interaction precedence: with a spell on the cursor it casts on the unit, with an item on the cursor it drops it on the unit, instead of switching target. |
|
||||
| `assist` | — | Targets the `unit`'s target. |
|
||||
| `focus` | — | Sets the ClassicAPI focus to the `unit`. |
|
||||
| `spell` | `spell` | Casts the `spell` on the `unit` via [`C_Spell.CastAtUnit`](#c_spellcastatunitspellidorname-unit) — the unit's GUID goes straight to the cast dispatcher (no target juggling), and ground-target spells land at the unit's feet. |
|
||||
| `spell` | `spell` | Casts the `spell`. With a `unit`, it casts on that unit via [`C_Spell.CastAtUnit`](#c_spellcastatunitspellidorname-unit) — the unit's GUID goes straight to the cast dispatcher (no target juggling), and a ground-target spell lands at the unit's feet. With no `unit`, it casts on the current target through `CastSpellByName`, like a plain `/cast`. |
|
||||
| `item` | `item`, or `bag`+`slot` | Uses an item on the `unit`. `item` may be a name / itemID / link (used via `C_Item.UseItemByName`, unit as the target) or a `"bag slot"` string like `"0 1"` (used via `UseContainerItem`). The deprecated `bag`+`slot` attributes are used when `item` is unset. |
|
||||
| `macro` | `macrotext` or `macro` | Runs the macro text. Prefers an addon `RunMacro` (e.g. SuperCleveRoidMacros — named macros + extended conditionals); otherwise runs the text natively, line by line, through the stock `ChatEdit_ParseText`. |
|
||||
| `macro` | `macrotext` or `macro` | Runs the macro text. To give the macro a unit, the handler selects the clicked `unit`, runs the macro, then restores the target from before the click. It prefers an addon `RunMacro` (for example, SuperCleveRoidMacros — named macros and extended conditionals). If no addon `RunMacro` exists, it runs the text natively, line by line, through the stock `ChatEdit_ParseText`. |
|
||||
| `stopcasting` | — | Stops the current cast. |
|
||||
| `menu` / `togglemenu` | — | Pops the standard unit dropdown at the cursor (whisper / inspect / trade / invite / …, the same menu `PlayerFrame` / `TargetFrame` / `PartyMemberFrame` show). |
|
||||
|
||||
@@ -5333,6 +5716,32 @@ not modern's `ComboPoints` reuse of the same number.
|
||||
local rage = UnitPower("player", Enum.PowerType.Rage)
|
||||
```
|
||||
|
||||
### `Enum.SpellBookSpellBank`
|
||||
|
||||
Selects which spellbook a `C_SpellBook.*` slot query reads. Passed as the
|
||||
`spellBank` argument to
|
||||
[`C_SpellBook.GetSpellBookItemInfo`](#c_spellbookgetspellbookiteminfoslotindex-spellbank).
|
||||
|
||||
| Value | Field |
|
||||
|------:|----------|
|
||||
| `0` | `Player` |
|
||||
| `1` | `Pet` |
|
||||
|
||||
### `Enum.SpellBookItemType`
|
||||
|
||||
Categorizes a spellbook slot. Vanilla's spellbook only ever holds real
|
||||
spells, so `C_SpellBook.GetSpellBookItemInfo` returns `Spell` for a
|
||||
player-book slot and `PetAction` for a pet-book slot. The other values
|
||||
exist for signature parity with retail but never occur in 1.12.
|
||||
|
||||
| Value | Field | Notes |
|
||||
|------:|---------------|-------|
|
||||
| `0` | `None` | Empty slot. Never returned — an empty slot yields `nil` instead. |
|
||||
| `1` | `Spell` | A player-book spell. |
|
||||
| `2` | `FutureSpell` | A not-yet-learned trainer spell. Never occurs in 1.12. |
|
||||
| `3` | `PetAction` | A pet-book spell. |
|
||||
| `4` | `Flyout` | A flyout group — a later-expansion concept. Never occurs. |
|
||||
|
||||
## Glue
|
||||
|
||||
Backports of two `C_Glue` session helpers. `C_Glue` is registered on the glue
|
||||
@@ -9102,6 +9511,79 @@ No retail equivalent (retail ships only the explored getter; its inverse,
|
||||
`C_Map.GetMapOverlays`, returns *everything*). Same table shape and
|
||||
`areaID` semantics as the explored getter.
|
||||
|
||||
## Model
|
||||
|
||||
### `model:SetDisplayInfo(creatureDisplayID)`
|
||||
|
||||
Points a Model frame at a creature by its display ID. This is the modern
|
||||
alternative to `Model:SetModel(path)`, which needs a raw model file path.
|
||||
|
||||
The engine resolves the display ID to a model file through two DBC tables:
|
||||
`CreatureDisplayInfo`, then `CreatureModelData`. It loads the model the same
|
||||
way `SetModel` does, then applies the creature's skin textures. So the model
|
||||
shows the correct look for that display, not an untextured base.
|
||||
|
||||
A Model frame provides no light of its own. Call `SetLight` after
|
||||
`SetDisplayInfo`, or the model renders as a black silhouette. Call
|
||||
`SetCamera(0)` to frame it.
|
||||
|
||||
An unknown or missing display ID falls back to the engine's placeholder
|
||||
model.
|
||||
|
||||
A character-based display (an NPC on the shared `Character\<Race>\<Sex>`
|
||||
base model) is dressed the same way a spawned NPC is: baked body texture,
|
||||
hair, face, facial hair, and equipment, including the attached helm and
|
||||
shoulder models. The dressing runs when the model file finishes loading,
|
||||
a moment after the call. Weapons do not show: the server sends NPC
|
||||
weapons only for spawned units, and no client table carries them.
|
||||
|
||||
This is the single-creature form, the same one MoP shipped. Later retail
|
||||
clients added an optional second argument `mountDisplayID` that shows the
|
||||
creature on a mount. That form is not supported here: a mount needs a
|
||||
second, attached model, and vanilla's Model frame holds one model.
|
||||
|
||||
```lua
|
||||
local m = CreateFrame("Model", "PreviewModel", UIParent)
|
||||
m:SetSize(200, 260)
|
||||
m:SetPoint("CENTER")
|
||||
m:SetDisplayInfo(330) -- a human NPC
|
||||
m:SetCamera(0)
|
||||
m:SetLight(1, 0, 0, -1, -1, 1, 1, 1, 1) -- else it is a black silhouette
|
||||
```
|
||||
|
||||
### `model:SetCreature(creatureID)`
|
||||
|
||||
Points a Model frame at a creature by its **entry ID** — the creature's
|
||||
database ID, not a display ID. This is the older companion to `SetDisplayInfo`:
|
||||
WoW added `SetCreature` in WotLK and `SetDisplayInfo` later, in Cataclysm.
|
||||
|
||||
The engine looks the entry ID up in the client creature cache, reads the display
|
||||
ID, then loads the model exactly like `SetDisplayInfo`. The same light and camera
|
||||
notes apply.
|
||||
|
||||
**Limitation.** The lookup only works for a creature the client has cached. A
|
||||
creature caches after you see it, target it, or mouse over it, or from
|
||||
`creaturecache.wdb` at login. Vanilla has no client-side entry-to-display table,
|
||||
so an uncached creature ID is a no-op — the frame keeps its previous model.
|
||||
|
||||
For an uncached creature, request its data first, then set it when the data
|
||||
arrives:
|
||||
|
||||
```lua
|
||||
local creatureID = 448 -- the creature's entry (database) ID
|
||||
C_CreatureInfo.RequestLoadCreatureByID(creatureID)
|
||||
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("CREATURE_DATA_LOAD_RESULT")
|
||||
f:SetScript("OnEvent", function()
|
||||
if arg1 == creatureID and arg2 then -- (creatureID, success)
|
||||
model:SetCreature(creatureID)
|
||||
model:SetCamera(0)
|
||||
model:SetLight(1, 0, 0, -1, -1, 1, 1, 1, 1)
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
## ChatBubbles
|
||||
|
||||
### `C_ChatBubbles.GetAllChatBubbles([includeForbidden])`
|
||||
@@ -11305,7 +11787,7 @@ resistance-aware aura libraries, and damage-meter school tagging.
|
||||
Previously addons either maintained hardcoded `spellID → school`
|
||||
tables or scanned tooltips for the first-line color tag.
|
||||
|
||||
### `CastSpellNoToggle(name | spellID)`
|
||||
### `CastSpellNoToggle(name | spellID [, unit])`
|
||||
|
||||
Spam-safe variant of `CastSpellByName` that won't toggle off an
|
||||
already-active spell. Covers both kinds of vanilla-toggle abilities:
|
||||
@@ -11357,6 +11839,27 @@ CastSpellNoToggle("Aspect of the Hawk")
|
||||
CastSpellNoToggle("Battle Stance")
|
||||
```
|
||||
|
||||
#### Optional unit target
|
||||
|
||||
A second argument casts the spell at a unit, without changing your
|
||||
current target:
|
||||
|
||||
```lua
|
||||
CastSpellNoToggle("Auto Shot", "focus") -- Auto Shot on your focus
|
||||
CastSpellNoToggle("Shoot", "targettarget") -- wand your target's target
|
||||
```
|
||||
|
||||
The unit is any standard token — `"focus"`, `"targettarget"`,
|
||||
`"party1"`, `"mouseover"`, a player name, and so on. A unit-target or
|
||||
auto-repeat spell fires straight at that unit. A ground-target spell
|
||||
lands at the unit's feet. This is the same cast-at-unit path as
|
||||
[`C_Spell.CastAtUnit`](#c_spellcastatunitspellidorname-unit).
|
||||
|
||||
The toggle gates run first, so the unit only matters when the spell
|
||||
actually casts. If the spell is already toggled on, the call stays a
|
||||
no-op and ignores the unit. An unknown unit token raises the engine's
|
||||
standard "Unknown unit" error, the same as `UnitHealth`.
|
||||
|
||||
String input matches case-insensitively and tolerates a trailing
|
||||
`(Rank N)` suffix the same way `CastSpellByName` itself does —
|
||||
`"Shoot"` and `"Shoot(Rank 1)"` both compare equal to a Shoot that's
|
||||
@@ -11366,8 +11869,9 @@ Reads `[VAR_ACTIVE_AUTO_REPEAT_SPELL]` (`0x00CEAC30`) for the auto-
|
||||
repeat check, and the engine's `FUN_SPELL_IS_TOGGLE_AURA_ACTIVE`
|
||||
(`0x004B36F0`) for the aura-active check. Delegates to
|
||||
`Script_CastSpellByName` (`0x004B4AB0`) for the actual cast — same
|
||||
resolution semantics (rank picking, target rules, etc.) as the
|
||||
engine's own global.
|
||||
resolution semantics (rank picking, target rules, and more) as the
|
||||
engine's own global. With a unit argument, it dispatches through the
|
||||
same cast-at-unit path as `C_Spell.CastAtUnit` instead.
|
||||
|
||||
Using this from inside a macro action slot? See
|
||||
[`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line) below for the additional
|
||||
@@ -11697,6 +12201,40 @@ end
|
||||
Equivalent to the legacy function of the same name introduced in 3.0
|
||||
(later renamed to `FindSpellBookSlotBySpellID` in 5.x).
|
||||
|
||||
### `C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)`
|
||||
|
||||
Returns a table describing the spell in a spellbook slot. `slotIndex` is
|
||||
1-based across the whole book. `spellBank` picks the book —
|
||||
[`Enum.SpellBookSpellBank.Player`](#enumspellbookspellbank) (`0`) or
|
||||
`Enum.SpellBookSpellBank.Pet` (`1`).
|
||||
|
||||
```lua
|
||||
local info = C_SpellBook.GetSpellBookItemInfo(1, Enum.SpellBookSpellBank.Player)
|
||||
-- info.name, info.spellID, info.itemType, info.isPassive, info.iconID, ...
|
||||
```
|
||||
|
||||
Table fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `itemType` | [`Enum.SpellBookItemType`](#enumspellbookitemtype) | `Spell` (1) for the player book, `PetAction` (3) for the pet book. |
|
||||
| `actionID` | number | The spellID. Equals `spellID` — vanilla has no spell-override system. |
|
||||
| `spellID` | number | The spellID in the slot. |
|
||||
| `name` | string | The localized spell name. |
|
||||
| `subName` | string | The rank text (e.g. `"Rank 3"`), or `""` when the spell has no rank. |
|
||||
| `iconID` | string | The icon **path** — feed it straight to `texture:SetTexture(...)`. |
|
||||
| `isPassive` | boolean | `true` for a passive spell. |
|
||||
| `isOffSpec` | boolean | Always `false`. |
|
||||
|
||||
Returns `nil` for an empty or out-of-range slot, matching the modern API.
|
||||
|
||||
> **Deviations from retail, forced by 1.12's data.** `iconID` is a
|
||||
> texture path, not a `fileID` (vanilla has no fileID system) — the same
|
||||
> deviation as [`C_Spell.GetSpellInfo`](#c_spellgetspellinfospellid).
|
||||
> `skillLineIndex` is not returned (`nil`); spellbook tabs aren't
|
||||
> SkillLines. `isOffSpec` is always `false` — vanilla has no
|
||||
> specializations.
|
||||
|
||||
### `C_SpellBook.GetSpellLevelLearned(spellID)`
|
||||
|
||||
Returns the level at which a spell becomes available — the
|
||||
@@ -13206,6 +13744,31 @@ resolver the engine's `UnitCreatureType` uses, which handles every unit kind
|
||||
[`C_CreatureInfo.GetCreatureTypeInfo`](#c_creatureinfogetcreaturetypeinfocreaturetypeid)
|
||||
to get the localized name back from the id.
|
||||
|
||||
### `UnitCreatureID(unit)`
|
||||
|
||||
Returns the numeric **creature id** — the NPC / creature-template entry — for a
|
||||
unit, or `nil`. This is the unit-token twin of
|
||||
[`C_CreatureInfo.GetCreatureID`](#c_creatureinfogetcreatureidguid): it does
|
||||
`C_CreatureInfo.GetCreatureID(UnitGUID(unit))` for you.
|
||||
|
||||
```
|
||||
creatureID = UnitCreatureID(unit)
|
||||
```
|
||||
|
||||
```lua
|
||||
/dump UnitCreatureID("target") -- 1842 for Hogger, nil for a player
|
||||
/dump UnitCreatureID("pet") -- your pet's creature-template id
|
||||
```
|
||||
|
||||
Vanilla packs the entry id into the unit's GUID (bits 24-47), so this reads it
|
||||
straight from the resolved GUID — no creature cache lookup. Returns `nil` for a
|
||||
player (a player GUID carries no template), an unresolvable-but-valid token
|
||||
(`"target"` with nothing targeted, an empty `"partyN"` slot), and any unit whose
|
||||
GUID isn't a creature or pet. Raises a Lua error on a garbage token — the
|
||||
standard `UnitX` behavior. Pair with
|
||||
[`C_CreatureInfo.GetCreatureInfoByID`](#c_creatureinfogetcreatureinfobyidcreatureid)
|
||||
to get the name from the id.
|
||||
|
||||
### `GetUnitSpeed(unit)`
|
||||
|
||||
Returns `currentSpeed, runSpeed, flightSpeed, swimSpeed` — all four
|
||||
@@ -13855,6 +14418,40 @@ This is the Blizzard-shaped surface over the field nampower exposes raw as
|
||||
Consumers that need the raw multiplier back can recover it as
|
||||
`1 / (1 + UnitSpellHaste(unit) / 100)`. Returns `0` for invalid units.
|
||||
|
||||
### `UnitSpellTargetName(unit)`
|
||||
|
||||
Returns the name of the unit that `unit` is casting or channeling a spell
|
||||
**at**, or `nil`.
|
||||
|
||||
```
|
||||
targetName = UnitSpellTargetName(unit)
|
||||
```
|
||||
|
||||
```lua
|
||||
UnitSpellTargetName("target") -- "Playername" while the mob casts at you
|
||||
UnitSpellTargetName("player") -- your current cast's target, or nil
|
||||
```
|
||||
|
||||
Returns `nil` when:
|
||||
|
||||
- `unit` is not casting or channeling.
|
||||
- The spell has no unit target. This covers a self-cast, a ground-target
|
||||
spell (Blizzard, Rain of Fire), and an item or lock cast.
|
||||
- The target's name can't be resolved (an off-screen stranger).
|
||||
|
||||
The target comes from the cast's `SMSG_SPELL_START` packet. ClassicAPI
|
||||
captures it for the player and for any remote unit whose cast you observed
|
||||
since it began. So the same limit as
|
||||
[`C_Spell.UnitCastingInfo`](#c_spellunitcastinginfounit--c_spellcastinginfo)
|
||||
applies: a remote unit's target is known only while you saw its cast. The
|
||||
name is resolved through the object manager (players and creatures), then the
|
||||
friends list, then the persistent name cache — the same chain as
|
||||
[`UnitNameFromGUID`](#unitnamefromguidguid).
|
||||
|
||||
The player's target lands with the confirming packet, about one round-trip
|
||||
after the cast starts. So `UnitSpellTargetName("player")` can read `nil` for
|
||||
the first part of your own cast, then return the name.
|
||||
|
||||
## UnitAuras
|
||||
|
||||
Backport of the modern `C_UnitAuras` namespace. Returns
|
||||
|
||||
@@ -0,0 +1,761 @@
|
||||
# Backporting inline `|T…|t` texture escape sequences to 1.12
|
||||
|
||||
The 1.12 text engine shows `|Tpath:height:width:…|t` markup as literal text.
|
||||
This project makes it draw the icon inline in FontStrings, chat, and tooltips —
|
||||
the way 4.3.4 and later clients do. The feature is complete and shipped
|
||||
([src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp),
|
||||
[src/text/InlineTexturePool.cpp](../src/text/InlineTexturePool.cpp)).
|
||||
|
||||
This file is the reverse-engineering map. **Current design** below states what
|
||||
ships today. Everything from **Goal & spec** down is the historical RE trail —
|
||||
the 4.3.4 study, the 1.12 injection map, and the bring-up plan. It is kept for
|
||||
the addresses and the reasoning, not as a description of the current code.
|
||||
|
||||
## Current design (shipped)
|
||||
|
||||
Each icon renders as an **engine-managed region** (a pooled `CSimpleTexture`)
|
||||
anchored to its owning FontString. The engine draws the region every frame and
|
||||
moves it with its line, so texture residency and scroll tracking are correct by
|
||||
construction. (A raw-GxU-quad renderer existed during bring-up and was removed
|
||||
in `56c2670`; its RE is the "REMOVED" section below.)
|
||||
|
||||
The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
|
||||
- **Positioning** — a co-hook on the glyph emitter `FUN_005ccbe0` records each
|
||||
icon at its pen position and delegates the plain text runs to the engine
|
||||
(segmented delegation, below). The pen read-back needed a fix: the engine's
|
||||
pen is lazy and omits the last glyph's own advance, so a coin sat on its
|
||||
digits; the emitter now adds the true terminal advance from the engine's own
|
||||
glyph-advance helpers (`550cf5e`).
|
||||
- **Rendering** — the paint-pass co-hook `FUN_005c8fe0` walks the icon records
|
||||
and queues each to `Text::InlineTexturePool`, which places the region on the
|
||||
next frame tick (never mid-render).
|
||||
- **Placement coordinates** — text pen space IS render-target pixels. The
|
||||
engine stores node positions NORMALIZED (fs-local anchor ÷
|
||||
`[0x832A44]`/`[0x832A48]` per axis, `FUN_0041ade0`), and the origin finalize
|
||||
`FUN_005cdf70` multiplies by the integer raster dimensions
|
||||
`[0xC2B9A4]`/`[0xC2B9A0]` (rounded, with the justify shift and vertical
|
||||
align folded in). So the flush converts pen → fs-relative anchor with
|
||||
`K = raster ÷ anchorExtent`, **per axis**, from those four live globals — no
|
||||
calibration, no cache, correct at every resolution and UI scale (`968335d`).
|
||||
Two earlier forms both failed: an empirical K (`origin ÷ rect edge`) that
|
||||
chat could never self-derive (its rect edge sits near screen x = 0 and
|
||||
failed the divide guard) and so rode a global cache seeded by whichever
|
||||
fontstring derived first — right only at the layout it was calibrated on;
|
||||
then a closed form built from the SetPoint px factor, which used the wrong
|
||||
global pair. Full derivation chain at `VAR_TEXT_RASTER_X/Y` in `Offsets.h`.
|
||||
- **Fade / parent alpha** — icons fade with their line (the pool mirrors the
|
||||
fs's folded colour alpha per frame, `3ca3b7e`), and segmented GLYPHS do too:
|
||||
a bit-3 (standalone FontString) node's baked per-glyph colours never re-bake
|
||||
on an alpha change (the engine's colour-change invalidate `FUN_005ccb40`
|
||||
skips bit-3 nodes), so the flush mirrors the node's live alpha byte
|
||||
(node+0x2c — SetTextColor alpha × frame-chain alpha, kept folded by the
|
||||
engine) into every baked colour, change-detected per page (`bd14a93`). An
|
||||
earlier `0xFF` alpha force at emit was a misdiagnosis of a bubble that built
|
||||
while faded out; it froze icon-line text fully opaque while everything
|
||||
around it faded.
|
||||
- **Outlined fonts** — outline ink extends past the glyph advances; the
|
||||
engine itself grows line height by exactly 4.0 / 2.0 pen px for thick / thin
|
||||
outline (`face+0x180` bits 3 / 0, `.rdata` constants). `OutlineInkPen` reads
|
||||
the same flags and constants, and half the ink leads each icon, half trails
|
||||
— threaded through the shared advance helper so the emitter advance, the
|
||||
justify pre-shift, and the wrap-stepper probes all agree. Without it, coins
|
||||
clipped into outlined digits the moment the exact K removed the old
|
||||
miscalibration's accidental slack (`968335d`).
|
||||
- **Measure** — three cold FontString-level co-hooks make measure match the
|
||||
drawn result: width (`FUN_00772890`), height (`FUN_007729B0`), and wrap
|
||||
(`FUN_005C7260`). `GetStringWidth`, `fontstring:GetStringHeight`, wrap
|
||||
breaks, and line height all count the icon.
|
||||
- **Tall icons** — an icon taller than the font grows its line height, so it
|
||||
does not overlap its neighbors, and it centers in the grown line. A hyperlink
|
||||
that contains a tall icon is hoverable across the whole icon
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
- **Editboxes** — an editbox keeps the raw markup, so the caret stays aligned
|
||||
(two complementary signals, below).
|
||||
- **Payload** — the full
|
||||
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
|
||||
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
|
||||
TGA.
|
||||
- **Anti-spoof** — chat strips player-injected `|T` icons.
|
||||
|
||||
Every fs-level measure path now counts icons: width (`FUN_00772890`), height
|
||||
(`FUN_007729B0`), wrap (`FUN_005C7260`), and the substring measure
|
||||
(`FUN_00772AE0` — an earlier note claimed nothing consumed it; its xrefs
|
||||
refute that: the GameTooltip auto-size measures each WRAPPED SEGMENT of a
|
||||
wrap-enabled line through it, so icon-bearing wrapped tooltip lines
|
||||
undersized their tooltip until it was hooked too). The Lua control surface is
|
||||
one kill switch, `_classicapi_InlineTexEnable` (the SEH latch trips it on a
|
||||
flush fault); the bring-up tune / stat / probe functions were removed
|
||||
(`5e5677b`, `6800a9a`).
|
||||
|
||||
## REMOVED (bring-up record) — the raw-GxU-quad rendering primitive
|
||||
|
||||
This was the FIRST renderer: each icon drawn as a raw textured quad through the
|
||||
text VB path. It shipped, then was removed in `56c2670` for the engine-region
|
||||
model (regions are resident by construction; the quad path needed the texture-
|
||||
residency machinery below and still flickered on VRAM pressure — see
|
||||
`docs/InlineTextureResidency.md`). This section is the address record for the
|
||||
removed code, not a description of the current renderer. `LoadTextureByPath`,
|
||||
`DrawTexturedQuad`, and the GxU device offsets it names are gone from
|
||||
`Offsets.h`; the values live here and in git history.
|
||||
|
||||
An arbitrary texture draws through the text VB path as a coloured, alpha-blended
|
||||
quad. The end-to-end recipe:
|
||||
|
||||
1. **Load by path** — `LoadTextureByPath` replicates `FUN_00770200`'s load: build
|
||||
flags via `FUN_0058a980(&flags, DAT_00878cf0, 0,0, 0,0,0, 1, 0)`, then
|
||||
`FUN_00449d90(path, &desc, flags, 0, 1)` with a byte-identical 5-dword desc
|
||||
(`{PTR_FUN_007ffa10, 8, &self8, (&self8)|1, 0}`). Returns the **HTEXTURE**
|
||||
(0x15c-byte struct; path@+0xc, flags@+0x110, refcount@+4).
|
||||
2. **THE KEY — residency + correct bind via `FUN_0044acf0(HTEXTURE, 1, 0)`.**
|
||||
This is the engine's own "get renderable texture" call (what the widget
|
||||
render uses every frame). It returns the bindable **CGxTexture** at
|
||||
`[HTEXTURE+0x140]` and, with force=1, drives the streaming load
|
||||
(`FUN_0044ad50` submits/prioritises it via `[HTEXTURE+0x138]`). **Calling it
|
||||
every frame is the live reference that makes the texture resident** — no
|
||||
hidden widget, no manual BLP decode. This was the entire multi-session
|
||||
blocker: WoW streams textures and only loads ones a live frame references; a
|
||||
standalone `FUN_00449d90` texture never loads on its own. Binding the raw
|
||||
HTEXTURE is `SetTexture([HTEXTURE+0x48]=0)` = a **white** quad; the D3D
|
||||
handle lives on the CGxTexture (`[CGxTexture+0x48]`), not the HTEXTURE.
|
||||
Dead ends that wasted time: manually invoking the decode callback
|
||||
`FUN_0044a260` (fills `[+0x120]` but not the bindable `+0x140`, and faults);
|
||||
binding `[HTEXTURE+0x48]` or `[HTEXTURE+0x120]` (both wrong offsets); forcing
|
||||
the streamable flag `tex[0x3c]|=0x80` (crashes in d3d9 — the bind's stream-in
|
||||
vfunc corrupts the resource).
|
||||
3. **Bind** — `FUN_00589e80(0x17, cgxTex)` (selector 0x17 = text texture stage).
|
||||
4. **Colour** — the font stage's default stage-0 COLOROP is already MODULATE, so
|
||||
`texture.rgb × diffuse(white)` = the icon's true colours; texture alpha works
|
||||
too. No colorop override is needed for a plain white vertex. (If a tint is
|
||||
wanted later: COLOROP is D3DTSS internal state `0x42` via `FUN_005a2570`; the
|
||||
engine leaves COLORARG1 at the D3D default = TEXTURE. Do NOT poke it directly
|
||||
mid-draw — it leaks `MODULATE` to the whole UI when a fault skips the restore.
|
||||
Use the engine's selector `0x1f` (value→COLOROP/ALPHAOP preset table at
|
||||
`DAT_0080a25c`/`DAT_0080a274`; value 1 = MODULATE/MODULATE).)
|
||||
5. **Quad** — lock the shared dynamic VB `FUN_0058a140(0, 0x18, 0x800)`, get the
|
||||
write ptr `FUN_0058a080`, write **4 verts** of the format
|
||||
`{float x,y,z; u32 colorBGRA; float u,v}` (stride 0x18) in corner order
|
||||
**TL, TR, BL, BR** (matches the shared quad index buffer `{0,1,2, 2,1,3}` and
|
||||
the device cull winding), submit `FUN_005c8f40(&handle, 4)`, unlock
|
||||
`FUN_0058a0a0(handle, 0)`.
|
||||
6. **Where** — co-hook the text paint pass `FUN_005c8fe0` and draw after the
|
||||
original glyph flush (device is in the correct textured-UI state there).
|
||||
Coordinate space is **per-layout pixels, y-down**; glyph verts live at small
|
||||
local coords (a probed glyph was at ~(9,33)–(17,50)). Draw coords must be
|
||||
within the layout's rendered area (a fixed (100,100) test lands in chat).
|
||||
|
||||
The build-side gotcha: the D3D device (default backend, `FUN_0058dd70`) is
|
||||
actually **OpenGL** (`glMaterialfv`/`glFogi`) — the D3D backend is `FUN_0058ba70`
|
||||
(`FUN_00598ce0`, vtable `PTR_FUN_00809ef8`); its state applier is `vtable[1]` =
|
||||
`FUN_005a2f00`, which maps engine selectors to D3D via `FUN_005a2570`
|
||||
(SetRenderState `+0xe4`, SetTextureStageState `+0x10c`, SetSamplerState `+0x114`,
|
||||
SetTexture `+0x104`). Selector map (shared across GL/D3D backends): 7=blend,
|
||||
0x10=depthtest, 0x14=cullface, 0x17-0x1e=texture bind (8 units),
|
||||
0x1f-0x26=colorop/alphaop preset (unit 0-7).
|
||||
|
||||
## SOLVED — positioning, measure/wrap, editbox exclusion, texcoords
|
||||
|
||||
The primitive draws at arbitrary coords; the rest is knowing WHERE and WHEN.
|
||||
Everything is in [src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp);
|
||||
offsets under the "Inline texture escape" + positioning blocks in
|
||||
[Offsets.h](../src/Offsets.h).
|
||||
|
||||
### The text render hierarchy (verified)
|
||||
|
||||
```
|
||||
frame → renderNode [frame+0x118] → group [renderNode+0x18]
|
||||
→ node [group+0x18] → line [node+0x24] ← the emitter's `this`
|
||||
```
|
||||
|
||||
- Main UI render `FUN_007657d0(DAT_00cf0bd8)` walks strata → frames, calls
|
||||
`FUN_0076fb00(renderNode)` per frame's render node.
|
||||
- `FUN_0076fb00` draws the frame's Texture regions then `FUN_005c1ef0(group)` →
|
||||
`FUN_005c8b70(group)` → `FUN_005c8fe0(node)` per node.
|
||||
- `FUN_005c8fe0(node)` = the **paint pass** we co-hook: ensures each line built
|
||||
(`FUN_005cd6a0` → `FUN_005cdc20` → the emitter), then binds each font page and
|
||||
copies the line's per-page glyph batches into the dynamic VB, translated by the
|
||||
line origin `[line+0x70]/[+0x74]`.
|
||||
- The **emitter** `FUN_005ccbe0(line, text, len, colorState, penXYZ, pageMask,
|
||||
linkState)` builds one wrapped line's glyph quads. `this` (the "line"/node the
|
||||
icon list keys on) owns batches `[+0xa0+page*4]` and origin `[+0x70]`. **Note
|
||||
the render node the emitter sees has NO back-pointer up to the frame** — the
|
||||
editbox discriminator had to be found another way (see below).
|
||||
|
||||
### Positioning — segmented delegation (co-hook the emitter)
|
||||
|
||||
We do NOT reimplement the emitter's intricate glyph vertex math. Instead, in the
|
||||
`FUN_005ccbe0` co-hook, for a line containing an inline texture:
|
||||
|
||||
1. Split the line at icon boundaries. Render each **plain run** by calling the
|
||||
ORIGINAL emitter on just that substring — the engine still draws every glyph.
|
||||
2. Thread the pen across runs: on return the emitter leaves the final node-local
|
||||
pen x in **`linkState[4]`**, stored with `FSTP dword` — a **FLOAT bit
|
||||
pattern**, not an int. Read it as a float (`*(float*)&linkState[4]`); an
|
||||
int-cast yields garbage (this bug hid the icon off-screen and dropped the
|
||||
trailing text).
|
||||
3. Record an `IconRecord` at the pen (node-local `x`,`y`) into `g_nodeIcons[line]`
|
||||
and advance the pen by the icon width. No draw runs during the build — icons
|
||||
are recorded and **flushed** in the `FUN_005c8fe0` paint co-hook, which
|
||||
computes each icon's pen-space position (line origin `[line+0x70]/[+0x74]`
|
||||
maps node-local → pen, the same translate the glyph copy `FUN_005c8710`
|
||||
applies), converts pen → anchor with the per-axis K (see **Placement
|
||||
coordinates** in Current design), and queues it to `Text::InlineTexturePool`
|
||||
as a placement RELATIVE TO THE OWNING FONTSTRING. The pool applies
|
||||
placements on the next frame tick.
|
||||
4. Restore `penXYZ[0]` on exit (the original never writes it; the draw builder
|
||||
does NOT reset it between left-justified lines, so leaving it mutated
|
||||
cascade-shifts every following line).
|
||||
|
||||
### FontString pipe-doubling — the escape arrives as `||T…||t`
|
||||
|
||||
The FontString text sanitizer **doubles any pipe that doesn't begin a recognized
|
||||
escape** so it renders as a literal `|`. 1.12 doesn't know `|T`, so a caller's
|
||||
`|Tpath:h|t` reaches the emitter as `||Tpath:h||t` (verified: emitter text length
|
||||
+2, pipes doubled). The detector therefore matches BOTH the doubled form (the
|
||||
real-world case) and a clean `|T` (should one ever arrive undoubled). Normal
|
||||
doubled pipes (`|| `) never match because the next char must be `T`.
|
||||
|
||||
### bit-3 batch-clear mode
|
||||
|
||||
The emitter clears the line's page batches at its top only when node flag
|
||||
`[line+0x5c] & 0x08` is set (the FontString/static case). Calling the original
|
||||
per segment would then wipe all but the last run. Handled by doing the clear
|
||||
ONCE per build (a `len==0` original call with bit 3 still set), then clearing
|
||||
bit 3 so the per-segment calls APPEND, restoring flags on exit. Bit 5 (shadow)
|
||||
isn't set for these nodes, so the only added work is a harmless colour append.
|
||||
|
||||
Clearing bit 3 has one more consequence: it switches the engine from the
|
||||
UNIFORM colour path (node+0x2c, consulted live at every paint) to baked
|
||||
PER-GLYPH colours the paint uses verbatim. The build-time alpha in colorState
|
||||
is already correct (the engine folds SetTextColor alpha × frame-chain alpha
|
||||
into node+0x2c on every alpha change), but the baked copies never refresh —
|
||||
the flush's alpha mirror keeps them tracking later fades. See **Fade / parent
|
||||
alpha** in Current design.
|
||||
|
||||
### Multi-line / multi-icon accumulation
|
||||
|
||||
The draw builder walks a node's wrapped lines by calling the emitter repeatedly
|
||||
on the SAME node, advancing the text pointer. So the icon list is cleared ONCE
|
||||
per build — on the first wrapped line, detected by `text == [node+0x48]` (the
|
||||
node's text start) — and icons ACCUMULATE across lines. Multiple icons per line
|
||||
fall out of the segmentation loop naturally.
|
||||
|
||||
### Vertical centring — font-relative
|
||||
|
||||
`penXYZ[1]` sits near the text TOP, so the icon centre = `penY + fontHeight *
|
||||
g_centerFrac` (0.6, calibrated in-game across chat + pfUI's bubble). Font height
|
||||
comes from the engine's own `FUN_005c6fa0(flag, [node+0x1c])` (the emitter's
|
||||
glyph-sizing call), so centring holds across font sizes without a fixed pixel
|
||||
nudge. A TALL icon (height > font) centres in its GROWN line (see the tall-icon
|
||||
sections in Current design) — the string-height co-hook adds the overflow, so
|
||||
the extra room sits half above and half below the text.
|
||||
|
||||
### Measure / wrap — co-hook the tokenizer
|
||||
|
||||
Line wrap is decided by the measure/fit functions (`FUN_005c6940`, `FUN_005c7470`
|
||||
via `FUN_005c7260`), which all share the `|`-tokenizer `FUN_005c2810`. Left
|
||||
alone, they count the hidden path text and wrap early. Fix: co-hook the
|
||||
tokenizer — when it's at an inline-texture span, consume the whole span as ONE
|
||||
near-zero-width token so every measure/fit caller ignores the path characters.
|
||||
The emitter detects icons independently (it scans the raw bytes), so the draw
|
||||
path is unaffected.
|
||||
|
||||
### Measure width: FIXED at the fs-level choke (`GetStringWidth` counts icons)
|
||||
|
||||
The tokenizer reports an icon span as a **near-zero-width** token, so at the
|
||||
gxu loop level every measure caller counts the icon as ~0 pixels wide. (The
|
||||
emitter still reserves the render width, so text after an icon positions
|
||||
correctly.) An earlier version of this doc concluded that the gap could not
|
||||
be closed cleanly, because the gxu engine has **no single cold choke**:
|
||||
|
||||
- `FUN_005c6940` (width), `FUN_005c7470`/`FUN_005c7260` (wrap), and the
|
||||
hit-test loop each tokenize and accrue width independently.
|
||||
- The token contract has no width field.
|
||||
- The per-glyph helpers (`FUN_005cabd0`/`FUN_005c6b70`) are the hottest text
|
||||
functions in the engine.
|
||||
|
||||
That analysis was right about the gxu level. The choke sits ONE LEVEL UP:
|
||||
**`FUN_00772890` = `CSimpleFontString::GetStringWidthInternal`**
|
||||
(`Offsets::FUN_FONTSTRING_STRING_WIDTH`) is cold, MinHook-safe, and the single
|
||||
funnel for exactly 4 callers:
|
||||
|
||||
- `Script_GetStringWidth` (0x0079E510)
|
||||
- the GameTooltip auto-size (0x00530640)
|
||||
- the editbox caret positioner (0x0077DE70)
|
||||
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
|
||||
which feeds auto-width fontstring layout)
|
||||
|
||||
It lazily computes into the `fs+0xFC` cache (anchor units, 0.0 sentinel) via
|
||||
the measure core `FUN_0044D670`, then divides the pen-space result by
|
||||
`fs+0x7C`.
|
||||
|
||||
The co-hook lives in `src/text/InlineTexture.cpp`. It calls the original,
|
||||
then adds `SumIconAdvances(text) / K` when two gates pass:
|
||||
|
||||
- `InlineInterceptActive(text)` is true — the **same predicate** the tokenizer
|
||||
hook stands down on, factored into one function so measure and render can
|
||||
never disagree.
|
||||
- The fs is not an editbox: `fs+0x120 & 0x1000` is clear (the fs-level bit
|
||||
`FUN_0044D670` translates into the gxu editbox flag 0x40).
|
||||
|
||||
The icon sum is computed in TRUE PEN PX — the exact numbers the emitter
|
||||
reserves — and bridged into the getter's return space by the font height
|
||||
known in both spaces: `sumPen × (fontHInt / fontHPen)`. `ResolveFsPenFont`
|
||||
replicates the render chain: `fontHPen = round((fontHInt × fs+0x7C, floored
|
||||
at [0x801628]/rasterY) × rasterY)` (the rebuild's node fontSize + the node
|
||||
ctor's min clamp + the emitter's own height helper), and the outline ink
|
||||
resolves through `face = [[fs+0xE0]+0x20]` — the same object the node stores
|
||||
at +0x44. fs-built nodes are always in snap mode (no fs-flag maps to node
|
||||
bit 7), so the snap-truncated advances are exact. The per-icon advance comes
|
||||
from `IconAdvancePen`, the **same helper** the emitter reserves with — so the
|
||||
measured width matches the rendered width by construction, outline ink and
|
||||
pixel snapping included. (An earlier bridge used the SetPoint push factor
|
||||
`div×1024/mul ≈ 1468` as the intermediate — the units cancelled for `:0`
|
||||
icons, but it couldn't express the truncation or the ink.)
|
||||
|
||||
Unit trap (hit on first flight): escape sizes are UI PIXELS. Do NOT divide
|
||||
the icon sum by `fs+0x7C` — that field is the layout UI *scale* (~0.68–1.0).
|
||||
The original's own `out / fs+0x7C` applies to a value the measure core
|
||||
already ran through the `FUN_0041AD80` gxu→internal converter, not to raw
|
||||
pixels. The `/0x7C` version measured a 16px icon as +44k px.
|
||||
|
||||
The hook NEVER writes the `fs+0xFC` cache: the original can serve the cached
|
||||
value, so the icon sum is re-derived and re-added per call (idempotent).
|
||||
|
||||
The same hook also fixes two layout consumers for free: tooltips auto-size
|
||||
wide enough for icon-bearing lines, and auto-width fontstrings lay out at
|
||||
their true rendered width.
|
||||
|
||||
### Wrap: FIXED at the shared stepper (`FUN_TEXT_WRAP_STEPPER` = 0x005C7260)
|
||||
|
||||
Wrap decisions also measured icons as ~0, so a chat line with prefix icons
|
||||
broke as if the icons were not there and the emitter's real advances ran past
|
||||
the right edge. The choke is the wrap-stepper dispatcher `FUN_005C7260`: it
|
||||
lays out one wrapped line per call, and its exactly 4 callers are EVERY wrap
|
||||
consumer — the draw builder `FUN_005CDC20` (render breaks), the height
|
||||
measure `FUN_005C2070`, the chars-that-fit counter `FUN_005C21C0` (ellipsis),
|
||||
and the break-array computer `FUN_005C2430` (behind `FUN_00772B60`).
|
||||
|
||||
The co-hook shrinks the `wrapWidth` argument by a **minimal feasible
|
||||
shrink**, gated on the same `InlineInterceptActive` predicate plus the flags
|
||||
editbox bit 0x40 the dispatcher already carries. The stepper reports each
|
||||
probe's measured (icons-at-zero) line width in `outWidth`, which gives a
|
||||
direct feasibility check: the line renders inside the frame iff its icons
|
||||
fit in the shrink plus the spare the break left (`iconSum ≤ s + (probeW −
|
||||
outWidth)`). The hook probes sandboxed (local out-params plus a COPY of the
|
||||
`p10` in/out state byte, so the caller's first-line state is not consumed):
|
||||
s = 0 first (feasible → no shrink at all — the common short-message case),
|
||||
escalate by the measured deficit when infeasible, bisect down after the
|
||||
first feasible hit; ≤8 probes, and with no feasible hit the last escalation
|
||||
target wins (wraps early rather than overflowing). The feasibility check
|
||||
carries a HALF-GLYPH tolerance: an auto-width fontstring (single anchor, no
|
||||
width — e.g. pfUI's addon-list labels) wraps at the icon-inclusive string
|
||||
width our GetStringWidth hook feeds the effective-width vmethod, so its line
|
||||
fits with exactly zero slack, and cross-computation drift between the two
|
||||
hooks' unit chains (plus OUTLINE extras and the trailing-glyph quirk) read
|
||||
"fits exactly" as "1px over" — shrinking a zero-slack line forces a wrap
|
||||
that had no business existing. Genuine icon overflow is ≥ a full icon, so
|
||||
half a glyph separates drift from real overflow. `outBreak` and
|
||||
`outNext − text` are byte counts; `outBreak == len` means the whole text
|
||||
fit. Two rejected earlier schemes, for the record: a naive whole-remaining-
|
||||
text sum shredded an 8-icon line to one word per line, and a fixed-point
|
||||
iteration on the icon count oscillated (a ~55-byte zero-width escape makes
|
||||
the break↔icon-count coupling discontinuous — no fixed point exists).
|
||||
`text` always points at the REMAINING text, so a continuation line whose
|
||||
icons are behind it gets sum = 0. Because all four consumers share the hook,
|
||||
render breaks, `GetStringHeight`, truncation, and break arrays shift
|
||||
together. (A `_classicapi_InlineTexWrap(n)` ring dumped the last 8
|
||||
icon-bearing calls during bring-up; removed with the other diagnostics in
|
||||
`5e5677b`.)
|
||||
|
||||
Unit trap #2 (hit TWICE): the stepper's inputs are gxu-normalized, PER AXIS —
|
||||
fontH is normalized-Y (× rasterY = pen px) and wrapWidth/outWidth are
|
||||
normalized-X (× rasterX = pen px). Both caller flavors land there: the draw
|
||||
builder passes node+0x1C/+0x3C (normalized at block creation), and the
|
||||
fs-level callers (the path chat wraps through) x-normalize their widths
|
||||
inside `FUN_0044d670` before gxu. First flight subtracted raw pixels and
|
||||
annihilated the small normalized widths (2-glyph chat fragments). The second
|
||||
converted with `fontH / fontHPx` — a Y-AXIS factor applied to an X-axis
|
||||
width — overshrinking the budget by the render aspect (×1.33 at 4:3, ×2.37
|
||||
at 2560×1080): icon-bearing chat lines wrapped a word or two early, worse per
|
||||
icon and per aspect. The correct pen-x → width-units factor is `1/rasterX`,
|
||||
and the glyph-relative tolerances use `fontHPx/rasterX` so "half a glyph"
|
||||
means an actual glyph width.
|
||||
|
||||
**Chat hyperlink hover over inline icons works (verified).** An emote wrapped in a
|
||||
hyperlink — `|Htel:name|h|T…|t|h`, the TwitchEmotes pattern — pops its tooltip when
|
||||
you hover the *icon*, not just adjacent text. The hover region is NOT re-measured
|
||||
on hover: the engine records each hyperlink's screen rect at layout time into a
|
||||
`GXUFONTHYPERLINKINFO` array (32-byte entries: rect + link data) via `FUN_005cd310`,
|
||||
called from the draw builder `FUN_005cdc20` — the same layout pass that runs the
|
||||
line through our co-hooked tokenizer (`FUN_005c2810`) and wrap-stepper
|
||||
(`FUN_005c7260`). So the recorded rect rides the same icon-aware position
|
||||
accounting the wrap fix installs; the icon lands inside the hover region for free,
|
||||
and hovering it fires the frame's `OnHyperlinkEnter`. This is a consequence of the
|
||||
wrap fix, not a separate feature. (Ghidra caveat: the decompile of the
|
||||
`FUN_005cd310` call args is mangled — a pointer-as-float + an uninitialized temp —
|
||||
so this is confirmed by the call path plus in-game test, not the byte-level
|
||||
formula.)
|
||||
|
||||
**Still icon-blind (accepted residuals):**
|
||||
|
||||
- Sub-pixel only for a string-final icon: the width hooks report its INK edge
|
||||
(advance minus the trail half-pad/ink — `TrailingIconTrimPen`), matching the
|
||||
engine's own last-glyph convention (the gxu width loop ends on ink, not
|
||||
advance — the source of the old −0.8px measurement, whose previous-glyph
|
||||
advance treatment is semantically CORRECT once an icon renders after it).
|
||||
What remains is the icon region's ±0.5px placement rounding and the base
|
||||
loop's ~0 contribution for the payload-0 icon token.
|
||||
- An fs whose `fs+0xFC` cache was filled while the feature was toggled
|
||||
differently (`_classicapi_InlineTexEnable`) keeps its old base width until
|
||||
the next `SetText`/font change re-dirties it. Debug-toggle-only.
|
||||
|
||||
**Editbox measure stays raw (by design).** The caret/measure path reads the
|
||||
focused editbox's INPUT buffer in place, and the tokenizer stands down on it
|
||||
per-editbox (`TextInFocusedEditbox` pointer-range test — not a global focus
|
||||
check), so caret math measures the raw `|T…|t` literal the editbox renders.
|
||||
The width hook mirrors that with the `fs+0x120 & 0x1000` editable gate. One
|
||||
marginal wobble: the focused single-line chat editbox's display fontstring
|
||||
carries editable=0 (only multi-line editors set the bit), so the caret
|
||||
positioner's line-boundary branch (multi-line-only in practice) can see an
|
||||
icon-adjusted `FUN_00772890` on it. Closing that costs a per-token content
|
||||
compare, which is not worth it.
|
||||
|
||||
### Editbox exclusion — two COMPLEMENTARY signals (bit 6 + focus global)
|
||||
|
||||
Editable text must show raw, editable `|T…|t` markup, not rendered icons — you
|
||||
edit macros, tweak icon fields, copy/paste texture strings. **This is
|
||||
retail-correct, verified in-game on a modern client:** typing
|
||||
`|TInterface\Icons\INV_Misc_Coin_01:16|t` into the chat entry box OR a macro body
|
||||
shows it **raw** (no icon) — Blizzard neutralizes player-*typed* escape sequences
|
||||
as anti-abuse — while addon/system text (`AddMessage`, `GetCoinTextureString`)
|
||||
renders. Retail's discriminator is the text's SOURCE (trusted vs player-typed),
|
||||
not editbox-vs-display; a shift-clicked item link renders in the chat box because
|
||||
it's inserted through the trusted path, but a typed `|T`/`|c` does not. Our
|
||||
editbox-raw + display-renders split reproduces the same observable outcome. (One
|
||||
mechanism-level divergence we accept: our editbox-raw uses the GLOBAL focus signal
|
||||
below, so while an editbox is focused OTHER display text that rebuilds also
|
||||
briefly shows raw; retail suppresses per-source. Cosmetic, and a per-editbox fix
|
||||
would need hot-path text-buffer matching — not worth it.) 1.12's tokenizer,
|
||||
unlike 4.3.4's, has no per-render texture-disable bit (4.3.4 uses tokenizer flag
|
||||
**0x1000** to kill only `|T`, **0x800** to kill all escapes), and editbox text
|
||||
nodes are NOT distinguishable by class from the render node — the emitter can't
|
||||
reach the owning frame. It takes **two signals together**, not one (an earlier
|
||||
version of this note wrongly called the focus global a redundant "belt-and-
|
||||
suspenders fallback" — removing it renders the chat/copy boxes' `|T` as icons):
|
||||
|
||||
- **Node flag `[node+0x5c]` bit 6 (0x40) = EDITABLE** — catches the **macro
|
||||
editor** (`MacroFrameText`, flags `0x4D`), which builds its layout once,
|
||||
**un-focused**, so the focus global never sees it. But single-line inputs
|
||||
(chat entry box, `pfChatCopyBox`) have bit 6 **CLEAR** — their nodes are
|
||||
`0x0D`/`0x20D`, identical to display FontStrings — so bit 6 can't catch them.
|
||||
- **Focused-editbox global `DAT_00cf4dc8`** (`InputFocused()`; holds the editbox
|
||||
with keyboard focus, cleared by `ClearFocus` = `FUN_0077e410`) — catches those
|
||||
single-line inputs. It's GLOBAL (true UI-wide, not per-node), which is why,
|
||||
while an editbox is focused, other display text that *rebuilds* also briefly
|
||||
renders raw. That's an accepted tradeoff: there is no per-node signal for
|
||||
single-line inputs, and when you're in an editbox seeing raw markup is what you
|
||||
want anyway.
|
||||
|
||||
**Why the tokenizer's bit-6 gate is load-bearing for RENDER, not just measure:**
|
||||
when the emitter suppresses an editbox it delegates to the ORIGINAL emitter,
|
||||
which **re-enters the tokenizer** to lay out glyphs (`FUN_005ccbe0` →
|
||||
`FUN_005c2810`). On that render path `flags` is the node's own flags, so bit
|
||||
0x40 makes the tokenizer stand down and the `|T` draws as literal text. Drop the
|
||||
bit-6 gate and the (un-focused) macro editor draws **BLANK** — the tokenizer eats
|
||||
the span as a zero-width token and the original emitter renders nothing (verified
|
||||
in-game). Focused single-line inputs get the same "don't intercept" via
|
||||
`Suppressed()` → `InputFocused()`. So both the emitter and the tokenizer consult
|
||||
`Suppressed() || bit-6`; the two must agree or you get blank-vs-raw mismatches.
|
||||
|
||||
(Dead end: **monochrome render flag `DAT_00c2b9dc`** (`FUN_005c8b70` sets it per
|
||||
group) — un-set at MacroFrameText's build time and flickered as a flush gate.
|
||||
Dropped. The editbox class vtable is `0x0081c8c0`, its script-slot resolver
|
||||
`FUN_0077a310` with focus slots at `+0x438`/`+0x440` — useful if a frame-level
|
||||
hook is ever needed.)
|
||||
|
||||
### Descriptor fields + the vertical flip
|
||||
|
||||
`ParseIcon` handles the full positional payload
|
||||
`path:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b`.
|
||||
Texcoords crop a sprite sheet to one cell: `u = left/texW, right/texW`,
|
||||
`v = top/texH, bottom/texH` — e.g. `UI-RaidTargetingIcons` is a 256×256 sheet,
|
||||
4×2 grid of 64×64, so marker N's cell is `256:256:<col*64>:<+64>:<row*64>:<+64>`.
|
||||
|
||||
**Vertex colour (`r:g:b`, 0-255):** the last three positional fields tint the
|
||||
icon. They pack to `0xFFrrggbb` — the same `0xAARRGGBB` order the tokenizer's
|
||||
`|cAARRGGBB` colour path builds (verified from `FUN_005c2810`'s colour branch),
|
||||
so the value flows straight into the quad's vertex colour and the font stage's
|
||||
default MODULATE COLOROP multiplies `texture.rgb × tint`. White (`255:255:255`,
|
||||
the default) = untinted. Because parsing is positional, a tint needs all the
|
||||
preceding fields present; the `TintIcon` helper passes `0`s for width..bottom
|
||||
(texW/texH `0` disables the texcoord crop → full texture) so the colour lands in
|
||||
the trailing slots.
|
||||
|
||||
**Texcoord order:** the region's 4-float texcoord setter
|
||||
(`FUN_SIMPLETEXTURE_SET_TEXCOORD`) takes the rect **Y-FIRST INTERLEAVED**
|
||||
(`{v0, u0, v1, u1}`), matching the engine's `{yA, left, yB, right}` rect
|
||||
convention — diagnosed in-game when `{u0,v0,u1,v1}` rendered a raid-mark crop as
|
||||
the wrong cell. v is top-down, same as the `|T` payload's top/bottom fields.
|
||||
(The removed quad path had the inverse concern: the OpenGL backend put v=0 at
|
||||
the bottom, so the quad mapped the TOP screen corners to v1 — kept here as the
|
||||
backend note.)
|
||||
|
||||
### Build / deploy / test cadence
|
||||
|
||||
- `set -o pipefail; cmake --build build --config Release 2>&1 | tail -5 && cp build/Release/ClassicAPI.dll "C:/WoW/Octo/dll/ClassicAPI.dll"`
|
||||
— `pipefail` stops a failed build from deploying a stale DLL.
|
||||
- **The client must be fully EXITED to relink** (it loads a symlink to the
|
||||
linker's output). DLL changes need a full restart, not `/reload`.
|
||||
- **The embedded `!!!ClassicAPI` addon is symlinked**, so Lua-only changes (the
|
||||
`TextureTest()` harness in `Util/AddOnCompat.lua`) need only `/reload`.
|
||||
|
||||
### Remaining / optional
|
||||
|
||||
- **Measure width, height, wrap** — all DONE (the three fs-level co-hooks in
|
||||
Current design). Vertex-colour tint (`:r:g:b`) — DONE. The bring-up
|
||||
diagnostics (`_classicapi_InlineTexSuppress/Tune/Stats/ProbeFS/RegionCal/Wrap`
|
||||
and the capture globals) were REMOVED (`5e5677b`); only
|
||||
`_classicapi_InlineTexEnable` remains.
|
||||
- **Animated emotes** — no DLL work is needed: a TwitchEmotes-style animator
|
||||
re-`SetText`s each line ~30fps and rewrites the `|T` texcoords to crop the
|
||||
current film-strip frame. That is addon Lua. RLE-compressed TGAs do not
|
||||
decode in 1.12 (uncompressed only) — convert with
|
||||
`magick in.tga -compress none in.tga` (no `-orient`/`-flip`: the region
|
||||
renderer's texcoord order shows the frame right-side-up).
|
||||
- **Still icon-blind** (accepted): the hyperlink hit-test past a tall icon
|
||||
OUTSIDE the link on the same line. The substring measure `FUN_00772AE0` was
|
||||
hooked once its real consumer surfaced (the tooltip auto-size measures
|
||||
wrapped SEGMENTS with it), and the trailing-icon width residual is trimmed
|
||||
to sub-pixel (`TrailingIconTrimPen` — a string-final icon reports its ink
|
||||
edge, not its advance, per the engine's own last-glyph convention).
|
||||
|
||||
## Goal & spec
|
||||
|
||||
Render the modern inline-texture escape (warcraft.wiki.gg/wiki/UI_escape_sequences#Textures):
|
||||
|
||||
```
|
||||
|Tpath:height[:width[:offsetX:offsetY[:texWidth:texHeight:left:right:top:bottom[:rV:gV:bV]]]]|t
|
||||
```
|
||||
|
||||
Atlas markup (`|A:atlas:…|a`) is Legion+ (needs an atlas DB 1.12 lacks) — out
|
||||
of scope. FileDataID paths are Legion+ — 1.12 resolves texture PATH strings
|
||||
only.
|
||||
|
||||
## Confirmed: 1.12 has ZERO inline-texture support
|
||||
|
||||
In-game test on the target client — all of these render as literal text, no
|
||||
icon:
|
||||
|
||||
```
|
||||
|TInterface\Icons\INV_Misc_Coin_01:16|t
|
||||
|TInterface\Icons\INV_Misc_Coin_01:16:16|t
|
||||
|TInterface\Icons\INV_Misc_Coin_01:16:16:0:0|t
|
||||
```
|
||||
|
||||
Root cause (see 1.12 map below): the shared escape tokenizer has no `T`/`t`
|
||||
case, so `|T` falls through to "literal `|` + ordinary glyphs".
|
||||
|
||||
## Reference: 4.3.4 (`C:\WoW\Proudmoore\apwow.exe`, build 15595)
|
||||
|
||||
4.3.4's text pipeline is a shared `|`-tokenizer feeding per-purpose loops that
|
||||
`switch` on the returned token type. It DOES support `|T`.
|
||||
|
||||
- **Tokenizer `FUN_00613980`** — reads one token, returns a type code, bytes
|
||||
consumed (`*param2`), payload char (`*param5`). `|`-switch token types:
|
||||
`1`=color(`|c`+8 hex), `2`=reset(`|r`), `3`=break(`|n`/newline), `4`=`||`,
|
||||
`5`=`|H`, `6`=`|h`, **`7`=texture(`|T`…`|t`)**, `8`=`|t`, `9`/`10`=`|K`/`|k`
|
||||
callback, `0xb`/`0xc`=`|W`/`|w`, `0`=ordinary glyph. The `T` case scans
|
||||
forward to the closing `|t` and returns 7 with the whole span's byte length.
|
||||
- **Field parser `FUN_00617ba0(tokenStart, &desc, size)`** — splits the `|T`
|
||||
payload on `:` (helpers: `FUN_0040fa50`=strchr, `FUN_004b3d00`=atof,
|
||||
`FUN_004b3480`=atoi) into a **0x40-byte descriptor**:
|
||||
|
||||
| off | field |
|
||||
|----|-------|
|
||||
| `+0x10`/`+0x14` | path ptr / length (up to first `:`) |
|
||||
| `+0x18` | height (font-scaled) — required |
|
||||
| `+0x1C` | width (defaults to height) |
|
||||
| `+0x20`/`+0x24` | offsetX / offsetY |
|
||||
| `+0x28`–`+0x34` | 4 normalized texcoords (texel ÷ texW/texH) |
|
||||
| `+0x3C` | hasTexCoords flag |
|
||||
| `+0x38`–`+0x3B` | vertex B/G/R/A |
|
||||
| `+0x3D` | hasColor flag |
|
||||
|
||||
- **Measure loop `FUN_00618430`** — walks tokens; `case 7` calls
|
||||
`FUN_00617ba0` and adds the texture width to the pen.
|
||||
- **Draw emitter `FUN_0061ea10`** (analog of 1.12 `FUN_005ccbe0`) — its
|
||||
`case 7` is the texture draw:
|
||||
1. `FUN_00617ba0(tokenStart, &desc)` — parse fields into the descriptor
|
||||
(stored inline on the line-state struct at `param7+0xb`, 0x10 dwords).
|
||||
2. Compute pen x, width (`desc.width[+0x18 dword = param7[0x12]] * scale`),
|
||||
and y from the justify mode `[obj+0x58]` (0=baseline, 1=center, 2=top).
|
||||
3. Copy the descriptor to a stack struct and call **`FUN_0061e820`** — the
|
||||
inline-texture quad emit. (`FUN_0061dd50` handles a region-state 1→2
|
||||
transition when a texture interrupts a hyperlink capture.)
|
||||
- **`FUN_0061e820`** — normalizes the 4 quad corners (÷ screen w/h from
|
||||
`DAT_00dd9ea0`/`DAT_00dd9ea4`) and calls `FUN_0061e410(1, &verts)`.
|
||||
- **`FUN_0061e410(count=1, &verts)`** (`__thiscall`, ECX = a vertex-buffer
|
||||
object) — appends `count` quads of **0x40 bytes** (one quad = pos + texcoords)
|
||||
to a **dedicated inline-texture vertex buffer**, growing it as needed
|
||||
(`FUN_0061db40`). So inline textures accumulate in their OWN batch, separate
|
||||
from the per-font-page glyph batches, each quad carrying its own texture
|
||||
(bound at flush). This is the concrete confirmation of the "separate quad
|
||||
path" — the backport must add such a batch + flush on the 1.12 side.
|
||||
|
||||
## Target: 1.12 (`C:\WoW\Octo\WoW.exe`, build 5875) injection map
|
||||
|
||||
Same architecture as 4.3.4, different addresses and token numbering. Reached
|
||||
from `Script_GetStringWidth` (`FUN_0079e510`) → `FUN_00772890` →
|
||||
`FUN_0044d670` → `FUN_005c2050` → the loops.
|
||||
|
||||
- **Shared tokenizer `FUN_005c2810`** — `~11 callers`. `|`-switch token types:
|
||||
`0`=color(`|c`), `1`=reset(`|r`), `2`=break(`|n`/newline), `3`=`||`,
|
||||
`4`=`|H`, `5`=`|h`, `6`=ordinary glyph. **No `T`/`t` case** → `|T` hits the
|
||||
fall-through (`*param5=0x7c; return 6`) = literal `|` then glyphs. Adding a
|
||||
texture token means picking an unused code (e.g. `7`).
|
||||
- **Measure loop `FUN_005c6940`** — `switch`: `0/1/4/5` zero-width, `2` break,
|
||||
default glyph (advance via `FUN_005c6b70`; kerning `FUN_005ca2d0`/
|
||||
`FUN_005ca4b0`). No texture case.
|
||||
- **Fit/width loops** `FUN_005c6c50` (chars-into-width + per-char x, for
|
||||
cursor/wrap) and `FUN_005c7470` (truncation/ellipsis). Same switch shape.
|
||||
- **Draw builder `FUN_005cdc20`** — reads the layout object's text at `[+0x48]`,
|
||||
handles line-wrap / justify (`[+0x54]`) / color state, per line calls the
|
||||
glyph emitter and `FUN_005cd310` (hyperlink rect registration).
|
||||
- **Glyph emitter `FUN_005ccbe0`** — the real quad emitter. Per glyph:
|
||||
`FUN_005cabd0(fontFace,char)` → glyph record (atlas **page** at `rec[10]`,
|
||||
advance `rec[0x12]`, texcoords `rec[0x18..0x1b]`), then appends 4 verts to a
|
||||
**per-atlas-page** batch at `[obj + page*4 + 0xa0]`. `case 0/1` set/reset
|
||||
color, `4/5` open/close hyperlink region, default = glyph.
|
||||
|
||||
### The architectural wrinkle
|
||||
|
||||
Glyph quads batch **per font-atlas page**. An inline `|T` texture is an
|
||||
arbitrary texture, not a font page, so it cannot join that batch — it needs a
|
||||
**separate textured-quad draw** (its own draw call, correctly ordered against
|
||||
the glyph batches so it appears inline). This is the substantive work, not the
|
||||
tokenizing. 4.3.4 solves it exactly this way — captured below: its draw `case 7`
|
||||
appends the quad to a dedicated inline-texture vertex buffer at
|
||||
`renderObject+0x9c`, separate from the per-font-page glyph batches (see the
|
||||
`FUN_0061ea10` / `FUN_0061e820` items under Open items).
|
||||
|
||||
## Strategy
|
||||
|
||||
The tokenizer is shared by ~11 callers, so DON'T just make it return a new
|
||||
type — the other callers' `switch`es would hit `default` and mis-handle it.
|
||||
Instead:
|
||||
|
||||
1. **Reimplement (co-hook) the measure loop `FUN_005c6940` and the draw
|
||||
builder `FUN_005cdc20`/emitter `FUN_005ccbe0`** — copy existing logic
|
||||
(fully decompiled), delegate every non-texture token to the original
|
||||
handling, and add an inline `|T…|t` case: measure → advance pen by texture
|
||||
width; draw → emit the arbitrary-texture quad.
|
||||
2. **Port `FUN_00617ba0`** (field parse → 0x40 descriptor). Standalone, safe to
|
||||
write now from the 4.3.4 decompilation; needs 1.12 equivalents of strchr/
|
||||
atof/atoi (trivial or already in `Offsets.h`).
|
||||
3. **Leave the shared tokenizer `FUN_005c2810` and the other ~9 callers
|
||||
untouched** — they keep rendering `|T` literally (wrap/cursor math slightly
|
||||
off inside a texture in v1 — cosmetic, extend later).
|
||||
4. **Texture load by path** — reuse the engine's `Texture:SetTexture` loader;
|
||||
**quad emit** — the primitive 4.3.4's draw case uses (to pin).
|
||||
|
||||
Risk: the hot text path (every string every frame). But these are
|
||||
deterministic C functions, NOT the contended nampower/SuperWoW targets, so the
|
||||
MinHook-collision concern is low. Reimplementing two complex loops faithfully
|
||||
is the real cost.
|
||||
|
||||
## Incremental slices
|
||||
|
||||
1. Fixed-size `|Tpath:height|t` — render + width only (ignore width/offset/
|
||||
texcoord/color). Proves the end-to-end quad path.
|
||||
2. width, offsetX/Y.
|
||||
3. texcoords (`texW:texH:l:r:t:b`).
|
||||
4. vertex color.
|
||||
5. correct wrap/cursor (extend `FUN_005c6c50`/`FUN_005c7470`).
|
||||
|
||||
## Open items / next steps
|
||||
|
||||
- [x] **4.3.4 draw-side `case 7`** — captured: `FUN_0061ea10` case 7 →
|
||||
`FUN_00617ba0` (parse) + `FUN_0061e820`→`FUN_0061e410` (append quad to a
|
||||
dedicated inline-texture vertex buffer). Separate batch, per-quad texture.
|
||||
- [x] **4.3.4 inline-texture buffer located** — `FUN_0061e820` (`RET 0x40`,
|
||||
i.e. a 0x40-byte quad struct by value) does `ADD ECX,0x9c` before
|
||||
`FUN_0061e410`, so the dedicated inline-texture vertex buffer is at
|
||||
**renderObject+0x9c** (glyph batches are the `+0xa0 + page*4` array — the
|
||||
two are adjacent but distinct). Quad struct: positions at `+0x8..+0x14`,
|
||||
texcoords at `+0x20..+0x2c` (rest = z / texture handle / color). Appended
|
||||
one quad at a time via `FUN_0061e410(this=buf@+0x9c, count, &quad)`.
|
||||
- [x] **Flush + path→texture load** — NOT traced on the 4.3.4 side by design;
|
||||
its purpose (the flush + per-quad texture bind + path→load mechanism) is
|
||||
fully answered on the **1.12 side** below: paint/flush `FUN_005c8fe0`,
|
||||
bind `FUN_00589e80(0x17,tex)` + draw `FUN_005c8f40`, load `FUN_00449d90`.
|
||||
4.3.4's own `+0x9c`-buffer flush is not needed — we implement against 1.12.
|
||||
- [x] **1.12 path→texture loader** — `FUN_00770200(textureObj, path, flag,
|
||||
blend=DAT_00878cf0, flag)`. It's the string-path branch of
|
||||
`Script_Texture_SetTexture` (`FUN_0079bb40`, method-table entry at
|
||||
`0x0087c1a0`). This is how a `|T` path becomes a bound texture.
|
||||
- [x] **1.12 batch render + textured-quad GPU primitive — FOUND.** The paint
|
||||
pass is `FUN_005c8fe0(layoutObj)`: it ensures each line built
|
||||
(`FUN_005cd6a0`), then per font page (stride 0x18, indices 0..7 over
|
||||
`[obj+0x18c+page*0x18]` = the page's texture object) does:
|
||||
- **bind texture:** `FUN_00589e80(0x17, texturePage)` (0x17 = the texture
|
||||
render-state selector),
|
||||
- **lock dynamic VB:** `FUN_0058a140(0, stride=0x18, 0x800)` → handle;
|
||||
`FUN_0058a080(handle)` → vertex data ptr,
|
||||
- copy the page's built verts from the per-page batch at
|
||||
`[lineObj + 0xa0 + page*4]` (`FUN_005ce090`=count, `FUN_005ce0c0`=copy),
|
||||
- **draw:** `FUN_005c8f40(&handle, vertCount)` (flush/submit),
|
||||
- **unlock:** `FUN_0058a0a0(handle, 0)`.
|
||||
|
||||
**Vertex stride = 0x18** (24 bytes = pos + uv + packed color). So an inline
|
||||
`|T` quad = 4 (or 6) such verts, bound to its own texture. Reuse path for
|
||||
the `|T` draw: `FUN_00589e80(0x17, inlineTex)` → write quad verts →
|
||||
`FUN_005c8f40`. **`FUN_005c8fe0` is the co-hook site for strategy B** —
|
||||
after the glyph batches are drawn, draw the frame's collected `|T` quads.
|
||||
- [x] **Path → bindable texture handle — RESOLVED.** `FUN_00770200`
|
||||
(SetTexture-by-path) internally calls **`FUN_00449d90(path, &desc, flags,
|
||||
0, 1)`** — the general path→CGxTexture loader (normalizes path, by-name
|
||||
cache, creates on miss via `FUN_0044a140`/`FUN_0044a310`, returns a
|
||||
**fallback texture on failure so never null**) — and stores the handle at
|
||||
`textureObj+0xcc`. That handle is the SAME CGxTexture type
|
||||
`FUN_00589e80(0x17, tex)` binds (the `tex[0x3c]` streamable flag +
|
||||
non-null `*tex` that `FUN_00593840` checks are CGxTexture fields; font
|
||||
pages and Texture widgets bind identically). So a `|T` draws as:
|
||||
`tex = FUN_00449d90(path,&desc,flags,0,1)` → `FUN_00589e80(0x17,tex)` →
|
||||
write 4 verts (stride 0x18) → `FUN_005c8f40(&vb,n)`. No scratch widget
|
||||
needed if we replicate `FUN_00770200`'s descriptor setup
|
||||
(`FUN_0058a980(&desc, blend, …)` then `FUN_00449d90`); or, simplest, keep
|
||||
one scratch Texture object, `FUN_00770200(scratch,path,…)`, read
|
||||
`[scratch+0xcc]`. The internal by-name cache makes repeats cheap.
|
||||
|
||||
Two implementation strategies once the primitive is pinned:
|
||||
- **A (mirror 4.3.4):** own a separate inline-texture vertex buffer, flush it
|
||||
in the paint pass alongside the glyph batches. Faithful, more integration.
|
||||
- **B (immediate):** at paint time, draw each collected `|T` quad immediately
|
||||
via the GxU primitive (bind `FUN_00770200`'d texture, draw one quad). Simpler;
|
||||
inline textures are few, so per-quad draw calls are fine. Likely the better
|
||||
first cut.
|
||||
- [ ] Confirm 1.12's tokenizer flag bits (`param4 & 0x100/0x200/0x400/0x800`)
|
||||
that gate color/escape handling, so the reimplemented loops pass them
|
||||
through unchanged.
|
||||
|
||||
## Reality check on scope — how it actually landed
|
||||
|
||||
The plan above feared reimplementing 1.12's glyph emitter `FUN_005ccbe0` (a
|
||||
long, intricate function) and the measure loop. It did not come to that. The
|
||||
shipped design avoids both:
|
||||
|
||||
- **Positioning** does NOT reimplement the emitter — it SEGMENTS the line at
|
||||
icon boundaries and delegates each plain run to the ORIGINAL emitter, so the
|
||||
engine still lays out every glyph. We only track where the icons go.
|
||||
- **Rendering** does NOT stand up a separate quad batch — icons are
|
||||
engine-managed regions anchored to their FontString (the removed quad batch
|
||||
was strategy B; regions replaced it).
|
||||
- **Measure** does NOT reimplement the loops — three cold co-hooks on the
|
||||
fs-level width / height / wrap functions add the icons' contribution and call
|
||||
the originals.
|
||||
|
||||
The result is the largest feature in the project, but built from co-hooks that
|
||||
delegate to the engine rather than from reimplemented layout code. It was
|
||||
verified by in-game iteration over many build/test rounds, as expected.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Why inline icons render as engine regions
|
||||
|
||||
Inline `|T…|t` icons render as pooled `CSimpleTexture` regions
|
||||
([src/text/InlineTexturePool.cpp](../src/text/InlineTexturePool.cpp)) — one per
|
||||
visible icon, anchored to the icon's owning FontString. The engine draws each
|
||||
region every frame like any UI texture, so the icon is resident by construction
|
||||
and moves with its line for free. This is 4.3.4's `CSimpleEmbeddedTexture`
|
||||
model, built from the parts 1.12 ships.
|
||||
|
||||
This file records why that design was chosen and the RE behind it. A raw-GxU-
|
||||
quad renderer came first and was removed (`56c2670`).
|
||||
|
||||
## History — a residency misdiagnosis, corrected
|
||||
|
||||
The quad renderer flickered: a random subset of chat icons blanked on scroll,
|
||||
and a second client showed no icons at all. The first diagnosis was **texture
|
||||
residency** — the theory that a raw quad bound a texture the VRAM manager had
|
||||
evicted, because a raw quad is owned by no frame object.
|
||||
|
||||
That diagnosis was wrong. The real cause was the Large-Address-Aware pointer
|
||||
bound (`dc61f77`): a 2GB cap in the flush's `LooksReadable` check rejected valid
|
||||
heap pointers above `0x80000000`, and the flush's node walk BREAKS on a rejected
|
||||
node. So one high node truncated the walk, and every node after it lost its
|
||||
icon. That is the "random subset blanks" pattern, and a second client shows none
|
||||
because it grows the heap past 2GB sooner. With the bound fixed, the raw quads
|
||||
did not flicker either.
|
||||
|
||||
So engine regions are kept for **4.3.4 parity**, not necessity. They are
|
||||
resident by construction, which closes the residency question for good — but the
|
||||
question was never the real bug. Do not re-open the residency investigation; the
|
||||
D3D notes at the end are the record of what was ruled out.
|
||||
|
||||
## The region the pool builds
|
||||
|
||||
1.12 ships `CSimpleTexture` — the ordinary `Texture` widget
|
||||
(`frame:CreateTexture()`). It is a managed, reference-counted, `CGxTexCache`-
|
||||
backed, engine-drawn texture. The only thing 3.3.5's `CSimpleEmbeddedTexture`
|
||||
adds is automatic inline layout inside the FontString, and the emitter already
|
||||
computes the icon positions — so the pool supplies them. (`CSimpleEmbeddedTexture`
|
||||
is compiled into 3.3.5 only; RTTI confirms it is absent from 1.12.)
|
||||
|
||||
Per icon, the pool takes a pooled region and sets the texture, texcoords,
|
||||
colour, and two anchor points relative to the owning FontString's bottom-left.
|
||||
The engine then owns load, residency, managed-pool reload, clipping, and draw.
|
||||
|
||||
### 1.12 entry points (used by the pool)
|
||||
|
||||
RTTI name strings: `.?AVCSimpleTexture@@` @ `0x00846588`,
|
||||
`.?AVCSimpleFontString@@` @ `0x00846544`.
|
||||
|
||||
Create a `CSimpleTexture` from C (mirrors `Tooltip::LinePool`'s FontString path):
|
||||
|
||||
```
|
||||
void* mem = FUN_00760450(&DAT_00cf4ce0, 0, /*rtti*/0x00846588, 0xFFFFFFFE); // pool alloc
|
||||
void* tex = FUN_0076fc40(mem, parentFrame, /*drawLayer*/2, /*sublayer*/1); // ctor
|
||||
```
|
||||
|
||||
Set the texture path (loads through the engine's own loader, owns the handle):
|
||||
|
||||
```
|
||||
FUN_00770200(tex, path, 0, DAT_00878cf0, 0); // CSimpleTexture::SetTexture(path)
|
||||
```
|
||||
|
||||
`FUN_00770200` builds GxTexFlags (`FUN_0058a980`), loads via `FUN_00449d90`,
|
||||
stores the HTEXTURE at `tex+0xCC` (owned ref), releases the old one
|
||||
(`FUN_0041aed0`), and marks the region dirty. This ownership is the whole
|
||||
residency story — the engine holds the texture and reloads it on eviction.
|
||||
|
||||
Region methods the pool calls directly:
|
||||
|
||||
| method | C++ target |
|
||||
|---|---|
|
||||
| SetTexCoord | `FUN_00770410(tex, &{v0,u0,v1,u1})` — Y-FIRST interleaved |
|
||||
| SetColor | `FUN_0077F750(tex, &BGRA)` — also the chat-fade alpha mirror |
|
||||
| SetPoint | `FUN_00767C70` — internal units (see LinePool `PixelToInternal`) |
|
||||
| Show / Hide | `FUN_0077FCB0` / `FUN_0077FC60` (desired-shown `+0xC4`, realized `+0xC8`) |
|
||||
|
||||
`CSimpleTexture` layout (from ctor `FUN_0076fc40`): vtable `0x0081c718`;
|
||||
region-render vtable at `+0x24`; HTEXTURE `+0xCC`; blend `+0xD0`; per-corner
|
||||
quad position/texcoord arrays `+0xD4..+0x100` (`OFF_SIMPLETEXTURE_CORNERS`);
|
||||
`+0x128` a flag. The region-render vtable slot 0 (`0x00770670`) computes the
|
||||
screen rect and calls `FUN_007705b0` to STORE the quad corners; the GPU draw is
|
||||
deferred to the engine's batched region paint.
|
||||
|
||||
## The pen↔anchor bridge
|
||||
|
||||
The emitter computes icon positions in PEN units; the pool stores region
|
||||
`SetPoint` offsets in ANCHOR units, relative to the FontString's rect. The
|
||||
bridge is one scale `K` (pen units per anchor unit) plus the FontString's own
|
||||
rect: the flush reads the FontString rect and derives `K` per node, converts
|
||||
each icon's screen rect to FontString-relative anchor offsets, and queues that.
|
||||
The pool applies the queued placement on the next frame tick — never mid-render.
|
||||
`K` derivation and the FontString-relative store are in `FlushLayout`
|
||||
([src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp)); the
|
||||
convergence pass in `Maintain`
|
||||
([src/text/InlineTexturePool.cpp](../src/text/InlineTexturePool.cpp)) self-heals
|
||||
any residual scale from the region's own frame-scale chain.
|
||||
|
||||
## What was ruled out during the residency chase (do not re-investigate)
|
||||
|
||||
The blank icons had zero CPU-side signature — records present, node chain
|
||||
contiguous, geometry sane, index buffer ready at submit, batched draw, z-buffer
|
||||
excluded twice. The full D3D9 render state at the true draw point was benign:
|
||||
`zFunc=8` always, `aBlend=1`, `colorW=0xF`, scissor and stencil off, texture
|
||||
stage MODULATE with a valid bound texture every draw. All of this was consistent
|
||||
with the LAA-bound truncation (the node never reached the draw), not with a lost
|
||||
texture surface.
|
||||
|
||||
D3D9 device access, if it is ever needed again:
|
||||
|
||||
- `IDirect3DDevice9* = *(void**)(*(void**)0x00C0ED38 + 0x38a8)` (CGxDeviceD3d).
|
||||
- `GetRenderState` = device vtable `+0xE8`; `SetRenderState` `+0xE4`.
|
||||
- The GxU re-applies its cached render states before every draw (`FUN_005a1b20`),
|
||||
so a raw `SetRenderState` for a GxU-managed state is overwritten; `ZENABLE` is
|
||||
not in that path, so a direct set survives.
|
||||
@@ -0,0 +1,9 @@
|
||||
; Exports for ClassicAPI.dll.
|
||||
;
|
||||
; `Load` is VanillaFixes' post-injection entry point: after the launcher
|
||||
; injects each dlls.txt DLL, the VanillaFixes runtime calls the DLL's `Load`
|
||||
; export on the game's MAIN thread (GetProcAddress(module, "Load")), outside
|
||||
; the loader lock and with no timeout. Listing it here exports it undecorated
|
||||
; as "Load" so GetProcAddress finds it regardless of calling convention.
|
||||
EXPORTS
|
||||
Load
|
||||
@@ -13,26 +13,6 @@
|
||||
|
||||
namespace Common {
|
||||
|
||||
#define HOOK_FUNCTION(offset, hook, original) \
|
||||
{ \
|
||||
auto *target = reinterpret_cast<LPVOID>(offset); \
|
||||
if (MH_CreateHook(target, reinterpret_cast<LPVOID>(hook), \
|
||||
reinterpret_cast<LPVOID *>(&original)) != MH_OK) \
|
||||
return FALSE; \
|
||||
if (MH_EnableHook(target) != MH_OK) \
|
||||
return FALSE; \
|
||||
}
|
||||
|
||||
#define UNHOOK_FUNCTION(offset, original) \
|
||||
{ \
|
||||
auto *target = reinterpret_cast<LPVOID>(offset); \
|
||||
if (MH_DisableHook(target) != MH_OK) \
|
||||
return FALSE; \
|
||||
if (MH_RemoveHook(target) != MH_OK) \
|
||||
return FALSE; \
|
||||
(original) = nullptr; \
|
||||
}
|
||||
|
||||
bool PatchBytes(void *dst, const void *src, size_t len);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
+148
-36
@@ -16,8 +16,12 @@
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "nameplate/Walk.h"
|
||||
#include "player/NameCache.h"
|
||||
#include "text/InlineTexture.h"
|
||||
#include "text/InlineTexturePool.h"
|
||||
#include "texture/Transform.h"
|
||||
|
||||
static Game::FrameScript_Initialize_t FrameScript_Initialize_o = nullptr;
|
||||
static Game::LoadScriptFunctions_t LoadScriptFunctions_o = nullptr;
|
||||
@@ -47,6 +51,20 @@ static bool __fastcall FrameScript_Initialize_h() {
|
||||
// cache to clear here.
|
||||
NamePlate::Events::PrepareForReload();
|
||||
|
||||
// The reload teardown destroys every icon pool texture, fontstring,
|
||||
// and text node — forget all inline-icon pointers now so nothing
|
||||
// touches them (see InlineTexture.h / InlineTexturePool.h).
|
||||
Text::InlineTexture::PrepareForReload();
|
||||
Text::InlineTexturePool::PrepareForReload();
|
||||
|
||||
// Drop per-region corner transforms (SetRotation / SetVertexOffset) — the
|
||||
// reload destroys every addon texture and the region pool reuses their pointers.
|
||||
Texture::Transform::PrepareForReload();
|
||||
|
||||
// Drop pending character-dress jobs and their engine compositors — the
|
||||
// reload destroys every Model frame the jobs point at.
|
||||
Model::DisplayInfo::PrepareForReload();
|
||||
|
||||
// Persist the name cache before the engine starts tearing down.
|
||||
// This hook fires on both `/reload` and `/logout` (the engine
|
||||
// re-initializes Lua state in both cases), giving us a clean
|
||||
@@ -71,6 +89,13 @@ static void __fastcall LoadScriptFunctions_h() {
|
||||
static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
LoadGlueScriptFunctions_o();
|
||||
Game::RunGlueModuleRegistrations();
|
||||
// World→glue teardown destroyed every world fontstring and text node the
|
||||
// inline-icon maps reference; forget them now that the glue UI is booting.
|
||||
// (Glue→world is covered by FrameScript_Initialize_h; the node-free hook
|
||||
// handles individual deaths, but bulk teardown paths may bypass it.)
|
||||
Text::InlineTexture::PrepareForReload();
|
||||
Text::InlineTexturePool::PrepareForReload();
|
||||
Texture::Transform::PrepareForReload();
|
||||
}
|
||||
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
@@ -86,47 +111,134 @@ static void __fastcall FrameRegisterEvent_h(void *frame, void *edx,
|
||||
FrameRegisterEvent_o(frame, edx, eventName);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook install — kept OFF the Windows loader lock.
|
||||
//
|
||||
// Every MH_EnableHook freezes all process threads (CreateToolhelp32Snapshot +
|
||||
// SuspendThread/GetThreadContext each). Doing ~90 of those from DllMain, under
|
||||
// the loader lock, is the pattern MinHook documents as unsafe: on machines
|
||||
// whose security stack intercepts thread suspension it stalled the remote
|
||||
// LoadLibrary thread past VanillaFixes' 10-second injection deadline, so VF
|
||||
// read the still-running thread as STILL_ACTIVE (259) and showed its generic
|
||||
// "compatible client" error — a false diagnosis of a slow load.
|
||||
//
|
||||
// So DllMain installs nothing. The install runs later, on a thread that does
|
||||
// NOT hold the loader lock, via one of two triggers that both funnel through
|
||||
// the latched EnsureInitialized():
|
||||
// * VanillaFixes' `Load` export, called on the game's main thread after
|
||||
// injection (the sanctioned VF extension point), or
|
||||
// * a fallback worker thread we spawn from DllMain, for any other injector
|
||||
// that never calls `Load`.
|
||||
// Whichever arrives first installs; the other observes the cached result.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static volatile LONG g_initClaimed = 0; // 0 until a thread takes the installer role
|
||||
static volatile LONG g_initDone = 0; // 0 until the install has finished
|
||||
static volatile LONG g_initResult = 1; // 0 = success, 1 = failure (until proven)
|
||||
static volatile LONG g_mhInitialized = 0; // MH_Initialize succeeded (gates detach teardown)
|
||||
|
||||
static bool CreateAndQueue(uintptr_t offset, void *hook, void **original) {
|
||||
auto *target = reinterpret_cast<LPVOID>(offset);
|
||||
if (MH_CreateHook(target, hook, original) != MH_OK)
|
||||
return false;
|
||||
// Queue only — a single MH_ApplyQueued below applies the whole batch in
|
||||
// one thread-freeze.
|
||||
if (MH_QueueEnableHook(target) != MH_OK)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool InstallHooks() {
|
||||
if (MH_Initialize() != MH_OK)
|
||||
return false;
|
||||
InterlockedExchange(&g_mhInitialized, 1);
|
||||
|
||||
if (!CreateAndQueue(Offsets::FUN_INVALID_FUNCTION_PTR_CHECK,
|
||||
reinterpret_cast<void *>(InvalidFunctionPtrCheck_h), nullptr))
|
||||
return false;
|
||||
|
||||
// Four core init hooks — each runs glue logic (PrepareForReload,
|
||||
// RunModuleRegistrations, RunGlueModuleRegistrations, EnableWrites,
|
||||
// RetryClaims) tightly coupled to DllMain state, so a declarative
|
||||
// HookAutoRegister wouldn't simplify them.
|
||||
if (!CreateAndQueue(Offsets::FUN_FRAME_SCRIPT_INITIALIZE,
|
||||
reinterpret_cast<void *>(FrameScript_Initialize_h),
|
||||
reinterpret_cast<void **>(&FrameScript_Initialize_o)))
|
||||
return false;
|
||||
if (!CreateAndQueue(Offsets::FUN_LOAD_SCRIPT_FUNCTIONS,
|
||||
reinterpret_cast<void *>(LoadScriptFunctions_h),
|
||||
reinterpret_cast<void **>(&LoadScriptFunctions_o)))
|
||||
return false;
|
||||
if (!CreateAndQueue(Offsets::FUN_LOAD_GLUE_SCRIPT_FUNCTIONS,
|
||||
reinterpret_cast<void *>(LoadGlueScriptFunctions_h),
|
||||
reinterpret_cast<void **>(&LoadGlueScriptFunctions_o)))
|
||||
return false;
|
||||
if (!CreateAndQueue(Offsets::FUN_FRAME_REGISTER_EVENT,
|
||||
reinterpret_cast<void *>(FrameRegisterEvent_h),
|
||||
reinterpret_cast<void **>(&FrameRegisterEvent_o)))
|
||||
return false;
|
||||
|
||||
// All feature hooks declared via `Game::HookAutoRegister` at file scope
|
||||
// in their respective modules (create + queue-enable, no apply yet).
|
||||
if (!Game::RunHookRegistrations())
|
||||
return false;
|
||||
|
||||
// One thread-freeze that activates every queued hook at once.
|
||||
return MH_ApplyQueued() == MH_OK;
|
||||
}
|
||||
|
||||
// Runs InstallHooks exactly once. Returns 0 on success, 1 on failure. If a
|
||||
// second caller arrives while the install is in flight, it blocks briefly so
|
||||
// both callers observe the real result.
|
||||
static DWORD EnsureInitialized() {
|
||||
if (InterlockedCompareExchange(&g_initClaimed, 1, 0) == 0) {
|
||||
g_initResult = InstallHooks() ? 0 : 1;
|
||||
InterlockedExchange(&g_initDone, 1);
|
||||
} else {
|
||||
while (InterlockedCompareExchange(&g_initDone, 0, 0) == 0)
|
||||
Sleep(1);
|
||||
}
|
||||
return static_cast<DWORD>(g_initResult);
|
||||
}
|
||||
|
||||
static DWORD WINAPI InitWorker(LPVOID) {
|
||||
EnsureInitialized();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// VanillaFixes calls this on the game's MAIN thread after injection
|
||||
// (InitAdditionalDLLs -> GetProcAddress(module, "Load")), outside the loader
|
||||
// lock and with no timeout — the sanctioned VF extension point. Returns 0 on
|
||||
// success; VF reports any non-zero result to the user. Exported undecorated
|
||||
// as "Load" via src/ClassicAPI.def.
|
||||
extern "C" DWORD Load() { return EnsureInitialized(); }
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
DisableThreadLibraryCalls(hModule);
|
||||
|
||||
if (MH_Initialize() != MH_OK)
|
||||
return FALSE;
|
||||
|
||||
auto *target = reinterpret_cast<LPVOID>(Offsets::FUN_INVALID_FUNCTION_PTR_CHECK);
|
||||
if (MH_CreateHook(target, reinterpret_cast<LPVOID>(InvalidFunctionPtrCheck_h), nullptr) != MH_OK)
|
||||
return FALSE;
|
||||
if (MH_EnableHook(target) != MH_OK)
|
||||
return FALSE;
|
||||
|
||||
// Four core init hooks stay here — each runs glue logic
|
||||
// (PrepareForReload, RunModuleRegistrations,
|
||||
// RunGlueModuleRegistrations, EnableWrites, RetryClaims)
|
||||
// that's tightly coupled to DllMain state, so a declarative
|
||||
// HookAutoRegister wouldn't simplify them.
|
||||
HOOK_FUNCTION(Offsets::FUN_FRAME_SCRIPT_INITIALIZE, FrameScript_Initialize_h,
|
||||
FrameScript_Initialize_o);
|
||||
HOOK_FUNCTION(Offsets::FUN_LOAD_SCRIPT_FUNCTIONS, LoadScriptFunctions_h,
|
||||
LoadScriptFunctions_o);
|
||||
HOOK_FUNCTION(Offsets::FUN_LOAD_GLUE_SCRIPT_FUNCTIONS,
|
||||
LoadGlueScriptFunctions_h, LoadGlueScriptFunctions_o);
|
||||
HOOK_FUNCTION(Offsets::FUN_FRAME_REGISTER_EVENT, FrameRegisterEvent_h,
|
||||
FrameRegisterEvent_o);
|
||||
|
||||
// All feature hooks declared via `Game::HookAutoRegister` at
|
||||
// file scope in their respective modules.
|
||||
if (!Game::RunHookRegistrations())
|
||||
return FALSE;
|
||||
// Install nothing here (loader lock — see the block comment above).
|
||||
// Spawn a worker that installs off the lock; the new thread cannot
|
||||
// run its body until DllMain returns and the loader lock releases,
|
||||
// and we never wait on it. Under VanillaFixes the `Load` export
|
||||
// usually wins the latch first; this worker covers every other
|
||||
// injector. DisableThreadLibraryCalls suppressed its THREAD_ATTACH.
|
||||
HANDLE worker = CreateThread(nullptr, 0, InitWorker, nullptr, 0, nullptr);
|
||||
if (worker != nullptr)
|
||||
CloseHandle(worker);
|
||||
} else if (reason == DLL_PROCESS_DETACH) {
|
||||
// Clean /quit path. Flush before MH_Uninitialize — the cache's
|
||||
// file I/O uses Win32 directly (no MinHook involvement), so
|
||||
// either order works in practice, but flushing first lets us
|
||||
// bail early if the hook teardown ever grows side effects.
|
||||
// Hard process termination (task manager kill) bypasses this
|
||||
// path; that's an inherent OS limitation, and the 5-minute
|
||||
// backstop flush in Remember() covers the worst case there.
|
||||
Player::NameCache::Flush();
|
||||
MH_Uninitialize();
|
||||
// Clean /quit path — only tear down if we actually initialized.
|
||||
// Flush before MH_Uninitialize — the cache's file I/O uses Win32
|
||||
// directly (no MinHook involvement), so either order works in
|
||||
// practice, but flushing first lets us bail early if the hook
|
||||
// teardown ever grows side effects. Hard process termination (task
|
||||
// manager kill) bypasses this path; that's an inherent OS
|
||||
// limitation, and the 5-minute backstop flush in Remember() covers
|
||||
// the worst case there.
|
||||
if (g_mhInitialized) {
|
||||
Player::NameCache::Flush();
|
||||
MH_Uninitialize();
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
+7
-2
@@ -73,7 +73,7 @@ using RegisterFrameMethods_t = void(__fastcall *)(const FrameMethodEntry *table,
|
||||
} // namespace
|
||||
|
||||
void *State() {
|
||||
return *reinterpret_cast<void **>(static_cast<uintptr_t>(Offsets::VAR_LUA_STATE));
|
||||
return Game::Read<void *>(Offsets::VAR_LUA_STATE);
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -320,7 +320,12 @@ bool RunHookRegistrations() {
|
||||
if (MH_CreateHook(targetPtr, node->hook,
|
||||
reinterpret_cast<LPVOID *>(node->original)) != MH_OK)
|
||||
return false;
|
||||
if (MH_EnableHook(targetPtr) != MH_OK)
|
||||
// Queue, don't enable. The caller (InstallHooks) applies every
|
||||
// queued hook with a single MH_ApplyQueued, so all ~90 hooks share
|
||||
// ONE thread-freeze instead of one freeze per hook. This is the
|
||||
// difference that keeps install cheap on machines whose security
|
||||
// stack intercepts SuspendThread / VirtualProtect per call.
|
||||
if (MH_QueueEnableHook(targetPtr) != MH_OK)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
+54
-8
@@ -17,6 +17,46 @@
|
||||
|
||||
namespace Game {
|
||||
|
||||
// --- Typed engine-memory access ---------------------------------------------
|
||||
//
|
||||
// The canonical way to read/write an engine object field or a fixed-VA global,
|
||||
// replacing the `*reinterpret_cast<const T *>(base + Offsets::OFF_X)` idiom
|
||||
// (and its double-cast variant when `base` isn't a byte pointer). Offsets stay
|
||||
// in Offsets.h per the single-source-of-truth rule — these only centralize the
|
||||
// cast noise, they add no checking.
|
||||
//
|
||||
// Game::Read<int>(obj, Offsets::OFF_FIELD) field read
|
||||
// Game::Ref<uint32_t>(obj, Offsets::OFF_FIELD) writable field lvalue
|
||||
// Game::Ptr<float>(obj, Offsets::OFF_FIELD) pointer TO a field (arrays,
|
||||
// out-params for engine calls)
|
||||
// Game::Read<float>(Offsets::VAR_GLOBAL) fixed-VA global read
|
||||
// Game::Ref<int>(Offsets::VAR_GLOBAL) fixed-VA global lvalue
|
||||
|
||||
template <typename T>
|
||||
inline const T *Ptr(const void *base, uintptr_t offset) {
|
||||
return reinterpret_cast<const T *>(reinterpret_cast<const uint8_t *>(base) + offset);
|
||||
}
|
||||
template <typename T>
|
||||
inline T *Ptr(void *base, uintptr_t offset) {
|
||||
return reinterpret_cast<T *>(reinterpret_cast<uint8_t *>(base) + offset);
|
||||
}
|
||||
template <typename T>
|
||||
inline T Read(const void *base, uintptr_t offset) {
|
||||
return *Ptr<T>(base, offset);
|
||||
}
|
||||
template <typename T>
|
||||
inline T &Ref(void *base, uintptr_t offset) {
|
||||
return *Ptr<T>(base, offset);
|
||||
}
|
||||
template <typename T>
|
||||
inline T Read(uintptr_t address) {
|
||||
return *reinterpret_cast<const T *>(address);
|
||||
}
|
||||
template <typename T>
|
||||
inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
using FrameScript_Initialize_t = bool(__fastcall *)();
|
||||
using LoadScriptFunctions_t = void(__fastcall *)();
|
||||
|
||||
@@ -341,19 +381,24 @@ void RunGlueModuleRegistrations();
|
||||
|
||||
// Declarative MinHook registration. Each feature module declares a
|
||||
// file-scope `static const Game::HookAutoRegister _hookreg{target,
|
||||
// &hook_fn, reinterpret_cast<void**>(&original_fn)};` and DllMain's
|
||||
// `RunHookRegistrations` walks the list once after `MH_Initialize`,
|
||||
// installing each hook with `MH_CreateHook` + `MH_EnableHook`.
|
||||
// &hook_fn, reinterpret_cast<void**>(&original_fn)};` and the installer
|
||||
// (`InstallHooks` in DllMain.cpp) walks the list once after
|
||||
// `MH_Initialize`, creating each hook and QUEUE-enabling it; a single
|
||||
// `MH_ApplyQueued` then activates them all in one thread-freeze.
|
||||
//
|
||||
// The install runs OFF the loader lock (via the `Load` export or the
|
||||
// fallback worker thread — see DllMain.cpp), never inside DllMain, so
|
||||
// the thread-freeze can't deadlock or stall the loader.
|
||||
//
|
||||
// Same lifetime rules as ModuleAutoRegister: constructors chain onto
|
||||
// a static-init list before DllMain runs, the linker keeps the OBJ
|
||||
// because the constructor has side effects, and order across TUs is
|
||||
// undefined but doesn't matter here (hooks are independent).
|
||||
//
|
||||
// Use only for feature hooks. The three core engine-init hooks in
|
||||
// Use only for feature hooks. The four core engine-init hooks in
|
||||
// DllMain (FrameScript_Initialize / LoadScriptFunctions /
|
||||
// Frame::RegisterEvent) have inline logic that interleaves with the
|
||||
// hook chain and stays in DllMain.
|
||||
// LoadGlueScriptFunctions / Frame::RegisterEvent) have inline logic that
|
||||
// interleaves with the hook chain and stays with the installer.
|
||||
struct HookAutoRegister {
|
||||
HookAutoRegister(uintptr_t target, void *hook, void **original);
|
||||
uintptr_t target;
|
||||
@@ -362,8 +407,9 @@ struct HookAutoRegister {
|
||||
HookAutoRegister *next;
|
||||
};
|
||||
|
||||
// Installs every registered hook. Returns `false` and stops on first
|
||||
// failure — caller (DllMain) should propagate by returning FALSE.
|
||||
// Creates and QUEUE-enables every registered hook (no `MH_ApplyQueued`
|
||||
// here — the caller applies the whole batch at once). Returns `false`
|
||||
// and stops on first failure.
|
||||
bool RunHookRegistrations();
|
||||
|
||||
} // namespace Game
|
||||
|
||||
+850
-6
@@ -40,6 +40,47 @@ enum Offsets {
|
||||
// state, so `FrameScript_RegisterFunction` writes land on it.
|
||||
FUN_LOAD_GLUE_SCRIPT_FUNCTIONS = 0x0046ABB0,
|
||||
|
||||
// Binding manager internals used by the direct-action/override binding backport.
|
||||
//
|
||||
// Stock Lua `SetBinding` wrapper (`int __fastcall(lua_State *)`).
|
||||
// The SetBindingSpell/Item/Macro/Click wrappers delegate to it after
|
||||
// constructing their action command, preserving punctuation-key
|
||||
// normalization, current-set selection, the Boolean return value, and the
|
||||
// native UPDATE_BINDINGS notification.
|
||||
FUN_SCRIPT_SET_BINDING = 0x004B8000,
|
||||
|
||||
// Hardware-key dispatch, before the normal binding table is resolved:
|
||||
// int __thiscall(manager, key, isDown)
|
||||
// Hooking here lets owner-scoped override bindings remain a separate
|
||||
// layer rather than temporarily mutating the character/account binding
|
||||
// table. The normal dispatcher is called unchanged when no override wins.
|
||||
FUN_BINDING_KEY_DISPATCH = 0x004B7990,
|
||||
|
||||
// Executes a resolved binding command:
|
||||
// int __thiscall(manager, command, isDown)
|
||||
// Vanilla has no SPELL/ITEM/MACRO/CLICK direct-action command handlers. The
|
||||
// hook recognizes those four families and delegates every native command
|
||||
// to the client.
|
||||
FUN_BINDING_COMMAND_EXECUTE = 0x004B7B50,
|
||||
|
||||
// Binding-manager `this`-relative flag, set to 1 while a resolved binding
|
||||
// command runs. FUN_BINDING_KEY_DISPATCH writes 1 immediately before its
|
||||
// call to FUN_BINDING_COMMAND_EXECUTE and 0 immediately after. Override
|
||||
// commands dispatched outside that engine path must mirror it so native
|
||||
// command handlers observe the same manager state a real keypress produces.
|
||||
// Verified in the FUN_BINDING_KEY_DISPATCH disassembly.
|
||||
OFF_BINDING_MANAGER_EXECUTING = 0xD8,
|
||||
|
||||
// Frame-script execution context and its nesting depth (the `DAT_00ceeac0`
|
||||
// dance referenced below). Shared by the binding dispatcher and the
|
||||
// tooltip/frame script invokers. FUN_BINDING_KEY_DISPATCH saves the context
|
||||
// pointer, zeroes it for the nested command, restores it afterward, and
|
||||
// floors the depth back to 0. `Bindings::Api` mirrors this when it runs an
|
||||
// override command outside the engine's own dispatch. Verified in that
|
||||
// disassembly.
|
||||
VAR_FRAMESCRIPT_EXEC_CONTEXT = 0x00CEEAC0,
|
||||
VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH = 0x00CEEAC4,
|
||||
|
||||
// GameTooltip script-method prologue helpers (used to resolve self → CFrameScriptObject*).
|
||||
// Underlying Lua C API names per [[docs/LuaCAPI.md]] are `lua_rawgeti`
|
||||
// (0x6F3BC0) and `lua_touserdata` (0x6F3740). The Set* method prologue's
|
||||
@@ -264,18 +305,301 @@ enum Offsets {
|
||||
OFF_REGION_ANCHOR = 0x24, // LayoutFrame anchor sub-object (SetPoint `this` / relativeTo base)
|
||||
DRAWLAYER_ARTWORK = 2,
|
||||
FRAMEPOINT_TOPLEFT = 0,
|
||||
FRAMEPOINT_TOPRIGHT = 2,
|
||||
FRAMEPOINT_LEFT = 3,
|
||||
FRAMEPOINT_RIGHT = 5,
|
||||
FRAMEPOINT_BOTTOMLEFT = 6,
|
||||
FRAMEPOINT_BOTTOMRIGHT = 8,
|
||||
|
||||
// FUN_REGION_SET_POINT stores offsets in *internal* coordinates, not
|
||||
// pixels. Script_SetPoint converts: `internal = pixel * [0x00832A44] /
|
||||
// ([0x00832A4C] * 1024)` (FUN_0041ae60's factor × input ÷
|
||||
// (FUN_0041ad70's return × DAT_007ffd68)). Both globals are runtime
|
||||
// UI-scale floats; passing raw pixels makes offsets ~1000× too large.
|
||||
// The REVERSE conversion (internal → UI pixels) is what the Script_*
|
||||
// measure getters push: Script_GetStringWidth (0x0079E510) — and our
|
||||
// GetStringHeight backport — compute `px = internal * [0x00832A4C] *
|
||||
// 1024 / [0x00832A44]` (FUN_0041AE40(FUN_0041AD70() × DAT_007FFD68 × v)).
|
||||
VAR_UI_COORD_SCALE_MUL = 0x00832A44, // float numerator
|
||||
VAR_UI_COORD_SCALE_DIV = 0x00832A4C, // float denominator base
|
||||
UI_COORD_SCALE_UNIT = 1024, // DAT_007ffd68
|
||||
// Semantically, [0x832A44] is the SCREEN WIDTH IN ANCHOR UNITS (the
|
||||
// full-screen x extent of the internal layout space) — that's why it's
|
||||
// the px→internal numerator above — and [0x832A48] is its Y sibling
|
||||
// (screen HEIGHT in anchor units). Verified via the gxu text position
|
||||
// converter FUN_0041ade0: block/node positions are stored NORMALIZED,
|
||||
// x = anchor/[0x832A44], y = anchor/[0x832A48] (the y global was
|
||||
// previously unmapped — the x/y pair sits at +0x00/+0x04, the SetPoint
|
||||
// denominator base at +0x08).
|
||||
VAR_UI_ANCHOR_SCREEN_H = 0x00832A48,
|
||||
// gxu text-raster dimensions — INT dwords holding the live render-target
|
||||
// size in PIXELS ({0,0,640,480} fallback), written by FUN_005c2b50 from
|
||||
// the render-target rect (FUN_0058a240) whenever it changes; every font
|
||||
// page re-rasterizes on change (the FUN_005ca6f0 walk). These are the
|
||||
// normalized→pen multipliers: the origin finalize FUN_005cdf70 computes
|
||||
// node origin (+0x70/+0x74) = round(normalizedPos × [raster]) after
|
||||
// folding the justify shift (right: +widthLimit, centre: +half) and
|
||||
// vertical align; the emitter's font-height helper FUN_005C6FA0 is
|
||||
// round(fontSize × [VAR_TEXT_RASTER_Y]) the same way (x sibling
|
||||
// FUN_005C7010 uses _X). So TEXT PEN UNITS ARE RENDER-TARGET PIXELS, and
|
||||
// pen-per-anchor = [VAR_TEXT_RASTER_*] / [VAR_UI_ANCHOR_SCREEN_*] per
|
||||
// axis — the exact conversion Text::InlineTexture's flush uses to place
|
||||
// icon regions (verified against a live probe: rasterX/anchorW matched
|
||||
// the measured origin÷rect-edge quotient to four digits).
|
||||
VAR_TEXT_RASTER_Y = 0x00C2B9A0,
|
||||
VAR_TEXT_RASTER_X = 0x00C2B9A4,
|
||||
// gxu text-node invalidate/reset — `__fastcall(node)`. Zeroes the
|
||||
// built-line count (+0x9C, the draw builder's early-out gate), resets the
|
||||
// link-rect state (+0x80/+0x90) and wrap cache (+0x68/+0x6C), and frees
|
||||
// all 8 page buffers — the next paint's pre-pass re-runs the builder, so
|
||||
// the emitter re-bakes the node from scratch. This is what the node
|
||||
// colour setter FUN_005CCB40 calls on a colour change for accumulation
|
||||
// (bit-3-clear) nodes; InlineTexture's flush calls it for a SEGMENTED
|
||||
// bit-3 node whose BASE colour RGB changed (glue AddonList toggle), since
|
||||
// baked per-glyph RGB can't be patched in place (|c runs own theirs).
|
||||
FUN_TEXT_NODE_INVALIDATE = 0x005CDEF0,
|
||||
// gxu font-face flags word. Bit 0 (0x1) = thin outline, bit 3 (0x8) =
|
||||
// thick outline: the emitter's prologue (FUN_005CCBE0) reads
|
||||
// *(fontFace+0x180) and grows the line height by [FLOAT_OUTLINE_EXTRA_*]
|
||||
// pen px for outlined faces, because outline INK extends past the glyph
|
||||
// metrics; the rebuild/color-sync shadow paths test the same bits. The
|
||||
// inline-texture lead/trail pads read the same flags so an icon clears an
|
||||
// outlined neighbour's ink (the coin-into-digits clip).
|
||||
OFF_FONTFACE_FLAGS = 0x180,
|
||||
// The engine's outline ink allowance, .rdata float constants (4.0 / 2.0
|
||||
// pen px total, i.e. ~2 / ~1 px per side) — the exact values the emitter
|
||||
// adds to line height for bit 3 / bit 0 outlined faces. Read live so we
|
||||
// stay bit-identical with the engine's own compensation. The 2.0 constant
|
||||
// double-duties as the MINIMUM font size numerator in the node ctor
|
||||
// (FUN_005cd6d0 / FUN_005ccb80: fontSize floored at [0x801628]/rasterY =
|
||||
// 2 pen px).
|
||||
FLOAT_OUTLINE_EXTRA_THICK = 0x0080306C,
|
||||
FLOAT_OUTLINE_EXTRA_THIN = 0x00801628,
|
||||
// The node's BUILT line count — incremented once per emitted wrapped line
|
||||
// by the draw builder (FUN_005CDC20, including the \n break case) and the
|
||||
// builder's own early-out gate (`if (node+0x9C != 0) return`); zeroed by
|
||||
// the node invalidate (FUN_TEXT_NODE_INVALIDATE). The render truth for
|
||||
// FontString:GetNumLines once the node has painted.
|
||||
OFF_TEXT_NODE_BUILT_LINES = 0x9C,
|
||||
// Wrap break-array computer: `__thiscall(fs, const char *text, float
|
||||
// wrapWidth /*fs-internal units*/, int *outBreaks, int cap)` → segment
|
||||
// count (byte offsets into text, [0]=0). Routes through FUN_005C2430 →
|
||||
// the wrap stepper, so it is icon-aware via the stepper co-hook. Verified
|
||||
// from the GameTooltip auto-size (FUN_00530640), which fills a 20-entry
|
||||
// array and measures each segment via FUN_FONTSTRING_MEASURE_SUBSTRING.
|
||||
FUN_FONTSTRING_BREAK_ARRAY = 0x00772B60,
|
||||
// Substring width measure: `__thiscall(fs, const char *text, int len)` →
|
||||
// ST0 (len 0 = strlen). Same measure-core call + `out / fs+0x7C` shape as
|
||||
// GetStringWidthInternal, but for an ARBITRARY string in the fs's font —
|
||||
// no cache. Callers (xrefs): the GameTooltip auto-size FUN_00530640, which
|
||||
// measures each WRAPPED SEGMENT of a wrap-enabled line between the
|
||||
// FUN_00772B60 break positions and takes the max as the tooltip width —
|
||||
// the icon-relevant consumer (an earlier note claimed nothing consumed
|
||||
// this function; the xref list refutes it) — plus the editbox
|
||||
// caret/selection cluster (FUN_0077DA80, FUN_0077DE70, FUN_0077D0D0),
|
||||
// which must stay raw and is excluded by the editable/focused-buffer
|
||||
// gates in the co-hook.
|
||||
FUN_FONTSTRING_MEASURE_SUBSTRING = 0x00772AE0,
|
||||
// FontString → gxu face resolution, for fs-level (measure-hook) callers
|
||||
// that need the face the RENDER will use. The rebuild (FUN_007724a0)
|
||||
// passes [fs+0xE0] (the font HANDLE) to the block creator FUN_0044d420,
|
||||
// which hands [handle+0x20] to FUN_005c1c30 → FUN_005cd6d0, which stores
|
||||
// it at node+0x44 — the emitter's face (`this` for every glyph call, and
|
||||
// the +0x180 outline-flags carrier). So face = [[fs+0xE0]+0x20].
|
||||
OFF_FONTSTRING_FONT_HANDLE = 0xE0,
|
||||
OFF_FONT_HANDLE_FACE = 0x20,
|
||||
|
||||
// CSimpleTexture (`Texture` widget) creation + operate primitives, used by
|
||||
// Text::InlineTexturePool to render inline |T icons as engine-owned,
|
||||
// managed-pool, resident-kept textures (the residency fix — see
|
||||
// docs/InlineTextureResidency.md). Mirrors Script_CreateTexture
|
||||
// (FUN_00773A20) and the Texture frame-method handlers; the sibling of the
|
||||
// CSimpleFontString path above. Same FUN_REGION_POOL_ALLOC / SetPoint /
|
||||
// Show / Hide / SetColor (FUN_FONTSTRING_SET_COLOR) as the FontString pool.
|
||||
VAR_SIMPLETEXTURE_POOL = 0x00CF4CE0, // &DAT_00cf4ce0 CSimpleTexture free-list pool (`this`)
|
||||
VAR_SIMPLETEXTURE_CLASS_TAG = 0x00846588, // ".?AVCSimpleTexture@@" (alloc debug tag)
|
||||
FUN_SIMPLETEXTURE_CTOR = 0x0076FC40, // __thiscall(mem, parent, layer, sublayer) -> tex
|
||||
// CSimpleTexture::SetTexture(path). Loads via FUN_00449D90, stores the owned
|
||||
// HTEXTURE at +0xCC (releases the old via DecRef FUN_0041AED0), marks dirty.
|
||||
// `__thiscall(tex, path, 0, *VAR_TEXTURE_BLEND_DEFAULT, 0) -> u32`. The
|
||||
// ownership + engine batched region-draw is the residency win. Same-path
|
||||
// early-out makes pooled reuse cheap.
|
||||
FUN_SIMPLETEXTURE_SET_TEXTURE = 0x00770200,
|
||||
// Script_Texture_SetTexture — the Lua `texture:SetTexture(...)` handler. Its
|
||||
// NUMERIC branch (arg 2 a number) clamps r/g/b/a to [0,1] and fills the
|
||||
// texture with a solid colour via FUN_00770360; its string branch loads a
|
||||
// path through FUN_00770200 above. texture/ColorTexture.cpp aliases this whole
|
||||
// handler as the 7.0 `SetColorTexture` (which IS exactly the numeric form), so
|
||||
// the clamp and opaque-alpha default come straight from the engine.
|
||||
FUN_SCRIPT_TEXTURE_SET_TEXTURE = 0x0079BB40,
|
||||
// CSimpleTexture::SetTexCoord — `__thiscall(tex, float[4])`. Struct field
|
||||
// order is {top, left, bottom, right} = {v0, u0, v1, u1} (verified from the
|
||||
// SetTexCoord handler 0x0079BEB0's Lua-arg → struct mapping). Natural
|
||||
// texcoords (no v-flip — the engine region path is top-left origin).
|
||||
FUN_SIMPLETEXTURE_SET_TEXCOORD = 0x00770410,
|
||||
OFF_SIMPLETEXTURE_HTEXTURE = 0xCC, // owned HTEXTURE ref (diagnostic)
|
||||
// The four drawn-quad corner POSITIONS: 4 vertices of {x, y, z} floats (0xC
|
||||
// stride), order BL, TL, BR, TR (from FUN_REGION_STORE_CORNERS's writes). The
|
||||
// region draw enqueue FUN_00772fd0 (0x00772fd0) hands region+0xD4 straight to
|
||||
// the vertex batch as FOUR INDEPENDENT vertices — and region+0x104 as the
|
||||
// matching 4 per-corner texcoords — never re-deriving an axis-aligned rect.
|
||||
// So writing ROTATED x,y here draws a rotated quad with NO corner clipping
|
||||
// (unlike 4.3.4's SetRotation, which rotates the texcoords instead). Used by
|
||||
// texture/Rotation.cpp for Texture:SetRotation.
|
||||
OFF_SIMPLETEXTURE_CORNERS = 0xD4,
|
||||
// Region corner-store: `__thiscall(region, const float rect[4])` writes the
|
||||
// drawn quad corners into region+0xD4..+0x100 from the rect ({yA,left,yB,right}
|
||||
// screen px). The renderer only draws a region whose +0xD4 corners are
|
||||
// populated (verified: the draw gate tests region+0xD4 != 0), and normally
|
||||
// SetPoint→layout-resolve calls this. We call it directly to place an icon by
|
||||
// its screen rect with NO anchors (anchoring 100+ textures/frame corrupts the
|
||||
// UI-manager pending-layout list — the FUN_00765650 crash). Co-hooked by
|
||||
// texture/Rotation.cpp, which re-applies rotation right after the engine
|
||||
// restores the axis-aligned corners (layout resolve / SetTexCoord).
|
||||
FUN_REGION_STORE_CORNERS = 0x007705B0,
|
||||
// Texcoord crop applied to a {top,left,bottom,right} screen rect before the
|
||||
// corner-store: shrinks right→left and bottom→top by the region's per-corner
|
||||
// texcoord span (ABS of the +0x104 U/V differences). No-op for full 0..1
|
||||
// texcoords. `__thiscall(region, float rect[4])`, rect in/out. The crop
|
||||
// itself is unconditional; every engine call site gates it on
|
||||
// OFF_REGION_TEXCOORD_MODIFIES_RECT below — with the flag clear (the
|
||||
// default) a partial SetTexCoord draws at the region's FULL size, so
|
||||
// callers replicating an engine store must apply the same gate.
|
||||
FUN_REGION_TEXCOORD_CROP = 0x00770570,
|
||||
// The SetTexCoordModifiesRect flag (int). Written from the Lua boolean by
|
||||
// Script_SetTexCoordModifiesRect (0x0079C080, `piVar4[0x49]`), read back
|
||||
// 1/nil by Script_GetTexCoordModifiesRect (0x0079C120). All three
|
||||
// FUN_REGION_TEXCOORD_CROP call sites test it: the SetTexCoord writers
|
||||
// (FUN_00770410 4-arg, FUN_007704C0 8-arg corner form) crop+store only
|
||||
// when set, and the layout-resolve store path (FUN_00770670) crops when
|
||||
// set / stores the raw anchor rect (+0x40..+0x4C) when clear. Zero by
|
||||
// default (region ctor), so the crop never runs unless an addon opts in.
|
||||
OFF_REGION_TEXCOORD_MODIFIES_RECT = 0x124,
|
||||
// Get the region's resolved screen rect: `__thiscall(region+OFF_REGION_ANCHOR,
|
||||
// float out[4]) -> int` (1 = valid, 0 = not laid out yet). Reads the anchor
|
||||
// sub-object's +0x40..+0x4C = {top, left, bottom, right}, gated on the
|
||||
// rect-valid bit [+0x3c]&1. This is the engine's own source for the corner
|
||||
// rect (see FUN_00770410's SetTexCoord path); texture/Rotation.cpp reads it to
|
||||
// rebuild axis-aligned corners before rotating.
|
||||
FUN_REGION_GET_RECT = 0x00768320,
|
||||
|
||||
// __thiscall(region+OFF_REGION_ANCHOR, int flag /*stack; pass 0*/) — force a
|
||||
// synchronous layout resolve: recomputes the region's rect (+0x64) from its
|
||||
// anchors right now, instead of waiting for the render's pending-layout pass.
|
||||
// Verified from Show (FUN_0077FCB0 at 0x0077FCE1: PUSH 0; LEA ECX,[ESI+0x24];
|
||||
// CALL). Load-bearing for tick-time icon placement: SetTexCoord
|
||||
// (FUN_00770410) stores the DRAW CORNERS (+0xD4) from the rect as a side
|
||||
// effect, so without a realize between SetPoint and SetTexCoord the corners
|
||||
// are stored from the STALE pre-anchor rect — the region's rect then resolves
|
||||
// correctly (diagnostics look perfect) but the renderer draws the corners,
|
||||
// which stay zero/stale → invisible icons (and the old build's frozen
|
||||
// top-left icons: corners stored from the zero rect at first apply).
|
||||
FUN_REGION_LAYOUT_REALIZE = 0x00768060,
|
||||
|
||||
// CSimpleFontString::RebuildString — destroys the old gxu text node and
|
||||
// creates the fresh one (FUN_0044d420), storing it at fs+0xF8. Gated on the
|
||||
// fs dirty bit (+0x60 & 1), so it fires on TEXT CHANGES, not per frame.
|
||||
// Co-hooked by Text::InlineTexture to map text node → owning fontstring —
|
||||
// the key that lets inline-icon regions anchor to their owning line
|
||||
// (1.12 chat lines ARE CSimpleFontStrings: the ScrollingMessageFrame's
|
||||
// display refresh FUN_00788750 SetTexts/anchors/shows one fs per visible
|
||||
// line — verified against the 4.3.4 CSimpleEmbeddedTexture model, which
|
||||
// anchors its icon regions to the owning fontstring the same way).
|
||||
FUN_FONTSTRING_REBUILD_STRING = 0x007724A0,
|
||||
// fs+0xF8 holds an HTEXTBLOCK handle, NOT the node itself: FUN_0044d420
|
||||
// allocates the 12-byte handle {vtbl, refcount, node}, FUN_005c1c30 writes
|
||||
// the created text node into handle+8, and FUN_0041af10 AddRefs and returns
|
||||
// the handle. The layout's node (what the emitter/paint hooks see) is
|
||||
// therefore *( *(fs+0xF8) + 8 ).
|
||||
OFF_FONTSTRING_TEXT_BLOCK = 0xF8, // HTEXTBLOCK handle (0 when dirty/empty)
|
||||
// CSimpleFontString live text color: count at +0xB4, array pointer at +0xB8
|
||||
// (uint32 BGRA per slot; slot 0 = the base color, alpha = byte 3; a parallel
|
||||
// per-glyph alpha-byte array hangs off +0xA8/+0xA4). Written by the fs
|
||||
// SetColor (FUN_FONTSTRING_SET_COLOR 0x0077F750: stores the dword at
|
||||
// (*(u32**)(fs+0xB8))[0] + the alpha byte at (*(u8**)(fs+0xA8))[0], then
|
||||
// fires the color-update vmethod +0x20). Count 0 = never colored = default
|
||||
// opaque white. THE CHAT-FADE SIGNAL: the ScrollingMessageFrame's fader
|
||||
// (FUN_00788460, gated on the SetFading flag at smf+0x360; per-line state
|
||||
// {rgba@+4, shown@+8, timeVisibleLeft@+0xC, fadeLeft@+0x10} in the
|
||||
// stride-0x10 line array at smf+0x3A4) animates each visible line
|
||||
// fontstring's alpha byte through this same SetColor every frame, then
|
||||
// hides the fs at fade end. Text::InlineTexturePool mirrors this byte onto
|
||||
// the line's icon regions each tick so inline icons fade with their line
|
||||
// (regions parent to the CHAT FRAME, so the frame's own alpha byte —
|
||||
// frame+0xC8, per Script_GetAlpha 0x00774DC0 — already modulates them via
|
||||
// the engine's parent×region product; only the fs's own component needs
|
||||
// mirroring).
|
||||
OFF_FONTSTRING_COLOR_COUNT = 0xB4,
|
||||
OFF_FONTSTRING_COLOR_ARRAY = 0xB8,
|
||||
OFF_TEXTBLOCK_NODE = 0x8, // the gxu text node inside the handle
|
||||
// Byte of fontstring state flags; bit 1 = "text block needs rebuild".
|
||||
// RebuildString (0x7724A0) gates on it at entry, releases the old block
|
||||
// (+0xF8 = 0), and only CREATES a new one if the fs rect is resolved —
|
||||
// a SetText during an unresolved rect leaves the fs blockless with the
|
||||
// refcounted zombie node still painting. If the bit is also clear at that
|
||||
// point, nothing ever rebuilds (the stuck-blockless state InlineTexture's
|
||||
// flush nudges by re-setting this bit).
|
||||
OFF_FONTSTRING_DIRTY_FLAGS = 0x60,
|
||||
// The per-node draw builder: walks a node's wrapped lines and calls the
|
||||
// glyph emitter (FUN_TEXT_EMITTER) once per line. Called from the paint's
|
||||
// per-node pre-pass FUN_005cd6a0 (`__thiscall(node)`, no stack args). We
|
||||
// co-hook it to stamp exact BUILD BOUNDARIES for the emitter's first-line
|
||||
// detection — the old `text == node+text-ptr` heuristic silently failed on
|
||||
// pfUI-processed chat lines (stale/preprocessed pointer), leaving inherited
|
||||
// records on reused node addresses (ghost icons) or never clearing them.
|
||||
FUN_TEXT_DRAW_BUILDER = 0x005CDC20,
|
||||
// The gxu text-node FREE: unlinks the node from its layout lists and pushes
|
||||
// it onto the node free list (DAT_00c2b98c) for reuse. Single caller —
|
||||
// FUN_005c1d00, the HTEXTBLOCK handle release (handle vtbl 0x008026e4 dtor
|
||||
// 0x0044D5C0 → 0x005c1d00 → here). This is the choke point where every
|
||||
// text node dies and its address becomes reusable: InlineTexture hooks it
|
||||
// to erase ALL per-node state exactly at death, making stale-record and
|
||||
// stale-owner bugs (ghost icons, orphaned records) structurally impossible
|
||||
// instead of heuristically guarded.
|
||||
FUN_TEXT_NODE_FREE = 0x005CD950,
|
||||
// THE pen↔anchor unit bridge. RebuildString (0x7724A0) multiplies every
|
||||
// text-unit quantity by region+0x7C when crossing into node creation:
|
||||
// nodePos = inset×s + rect corner, nodeFontH = fontPx×s, spacing = +0xF4×s,
|
||||
// and divides rect extents by s for the text-unit width/height. SetParent
|
||||
// (FUN_0076AB10) propagates the parent's +0x7C down the frame tree — it's
|
||||
// the per-object layout/UI-scale chain. This single scalar is what every
|
||||
// earlier "derive the scale" attempt (13/16, ownerH/fontH) was guessing at.
|
||||
OFF_LAYOUT_SCALE = 0x7C, // float: anchor units per text/pen unit
|
||||
OFF_FONTSTRING_INSET_X = 0x110, // float, text units (RebuildString posX term)
|
||||
OFF_FONTSTRING_INSET_Y = 0x114, // float, text units (RebuildString posY term)
|
||||
// CSimpleRegion::SetParentAndLayer — __thiscall(region, parentFrame, layer,
|
||||
// show). Handles old-parent unlink + new-parent region-registry insert +
|
||||
// conditional Show. Verified from the region base ctor FUN_0077F640 and the
|
||||
// message frame's per-line setup (FUN_00788750 calls it with (frame, 2, 1)).
|
||||
FUN_REGION_SET_PARENT_AND_LAYER = 0x0077FD10,
|
||||
OFF_REGION_PARENT = 0x9C, // region's parent frame ptr (read by Show's gates)
|
||||
OFF_REGION_DESIRED_SHOWN = 0xC4, // Show (FUN_0077FCB0) NO-OPS unless this is set;
|
||||
// engine callers always write it before show/hide
|
||||
OFF_REGION_ACTUALLY_SHOWN = 0xC8, // 1 after Show completed (the realize latch)
|
||||
|
||||
// Owning-frame recovery for the inline-icon pool (the 3.3.5 ownership model,
|
||||
// no overlay). Chat renders through the strata walker FUN_007657d0 →
|
||||
// per-frame draw-list rebuild FUN_00765920 → per-layer render FUN_0076FB00 →
|
||||
// text paint (NOT through the child-frame render FUN_0076B3F0). So
|
||||
// Text::InlineTexturePool co-hooks the two below:
|
||||
// • FUN_FRAME_DRAWLIST_REBUILD — `__fastcall(frame)`, receives the frame
|
||||
// cleanly and owns its 5 inline draw layers at frame + OFF_FRAME_LAYER_BASE
|
||||
// + i*FRAME_LAYER_STRIDE. We record layer→frame so the layer-render hook
|
||||
// can recover the owning frame with no offset-scan / vtable guessing.
|
||||
// • FUN_FRAME_LAYER_RENDER — `__fastcall(layer)`, the per-layer paint that
|
||||
// directly wraps the chat text paint. We bracket the icon pool's pass here
|
||||
// (the flush runs nested) and look the frame up in the layer→frame map.
|
||||
FUN_FRAME_DRAWLIST_REBUILD = 0x00765920,
|
||||
FUN_FRAME_LAYER_RENDER = 0x0076FB00,
|
||||
OFF_FRAME_LAYER_BASE = 0x1C, // first inline draw layer, region-relative
|
||||
FRAME_LAYER_STRIDE = 0x30, // per-layer stride
|
||||
FRAME_LAYER_COUNT = 5, // BACKGROUND..OVERLAY
|
||||
// Resolved on-screen rect of a region: 4 floats at regionBase+0x64 (=
|
||||
// LayoutFrame+0x40), in GxU SCREEN PIXELS — verified: FUN_00770670 reads
|
||||
// them and FUN_007705b0 stores them straight as GxU vertex corners. Layout is
|
||||
// {yA, left, yB, right} (x at [1]/[3], y at [0]/[2]); use min/max to get
|
||||
// left/top without pinning which y index is top.
|
||||
OFF_REGION_RECT = 0x64,
|
||||
|
||||
// "Currently displayed thing" state fields on a GameTooltip frame
|
||||
// instance. Each Set* path writes one of these (and zero or two
|
||||
@@ -737,9 +1061,13 @@ enum Offsets {
|
||||
// (opcode 0x96 → FUN_0049D560) parses the wire data. Called with
|
||||
// the sender GUID as stack args 9 and 10 (lo, hi).
|
||||
//
|
||||
// Calling convention: `__fastcall` with 10 args — ECX = sender
|
||||
// name string, EDX = chat type, then 8 stack args ending in the
|
||||
// GUID pair. Called from:
|
||||
// Calling convention: `__fastcall`, `RET 0x28` — ECX = the MESSAGE
|
||||
// text, EDX = chat type, then 10 stack args ending in the sender
|
||||
// GUID pair (lo, hi). (ECX was previously mislabeled "sender name";
|
||||
// verified from the call-site disassembly in FUN_0049D560 —
|
||||
// `MOV ECX,[EBP-0x10]` is the raw/processed message. The message is
|
||||
// copied into one buffer that feeds BOTH the chat frame and the
|
||||
// say/yell bubble spawn FUN_00608AC0.) Called from:
|
||||
// - FUN_0049D560 directly for live (non-throttled) chat
|
||||
// - The pending-chat queue processor (`__AUPENDINGCHAT`) for
|
||||
// messages buffered via FUN_0049CAE0 when the engine flag at
|
||||
@@ -748,9 +1076,11 @@ enum Offsets {
|
||||
// (system notifications, arena team membership changes, etc.)
|
||||
// which pass 0 / NULL for the GUID args
|
||||
//
|
||||
// Hooked by `Chat::CurrentGUID::ChatDispatch_h` to capture the
|
||||
// GUID into a global for `GetCurrentChatGUID()` to read during an
|
||||
// addon's CHAT_MSG_* OnEvent.
|
||||
// Hooked (once) by `Chat::Dispatch`, which orchestrates the concerns
|
||||
// sharing this choke point: `Chat::CurrentGUID` (publish the sender
|
||||
// GUID for `GetCurrentChatGUID()`), `Chat::IconFilter` (strip
|
||||
// player-injected `|T` icon spoofs from chat + speech bubbles), and
|
||||
// `Chat::RaidMarkers` (expand `{rtN}`/`{skull}`/… into inline markers).
|
||||
FUN_CHAT_DISPATCH = 0x0049A870,
|
||||
// Per-player inventory manager lives at this offset on the player object.
|
||||
// +0x00 = u32 slot count (OFF_INVMGR_SLOT_COUNT), +0x04 = u64* GUID
|
||||
@@ -1474,6 +1804,81 @@ enum Offsets {
|
||||
VAR_TEXTURE_METHOD_REGISTRY = 0x00CF5434,
|
||||
VAR_FONTSTRING_METHOD_REGISTRY = 0x00CF5400,
|
||||
|
||||
// Model frame — backs `Model:SetDisplayInfo(creatureDisplayID)`
|
||||
// (`Model::DisplayInfo`). Registry ctx per docs/raw_methods.txt (table
|
||||
// 0x00878948, 23 methods: SetModel/SetSequence/ReplaceIconTexture/…).
|
||||
VAR_MODEL_METHOD_REGISTRY = 0x00CF0C8C,
|
||||
// Model frame-script type id, lazily assigned by the engine on the first
|
||||
// Model method call (`Script_SetModel` FUN_0076d950 prologue:
|
||||
// `if (id == 0) id = ++counter`). Mirror the lazy-assign so a Model
|
||||
// type-check works even before any stock Model method has run; the IsA
|
||||
// check itself is the CFrameScriptObject vmethod at vtable+0x10, shared by
|
||||
// every frame type.
|
||||
VAR_MODEL_LUA_TYPE_ID = 0x00CF0C5C,
|
||||
VAR_FRAMESCRIPT_TYPE_ID_COUNTER = 0x00CEEF6C,
|
||||
// FUN_0076cfe0(model /*ecx*/, int replaceableType, const char *path) —
|
||||
// loads `path` as a texture and binds it to the model's replaceable-texture
|
||||
// slot `replaceableType`. Worker behind `Model:ReplaceIconTexture` (which
|
||||
// passes type 14); creature skins use MONSTER_1/2/3 = 11/12/13.
|
||||
FUN_MODEL_SET_REPLACEABLE_TEXTURE = 0x0076CFE0,
|
||||
// The loaded model instance a CSimpleModelFFX keeps at +0x318. It is the
|
||||
// SAME CModel class CGUnits keep at unit+0xD8 — verified via
|
||||
// FUN_007110d0 (the geoset-range setter), whose submesh walk and +0x98
|
||||
// visibility-array writes match the instance layout probed in-game.
|
||||
OFF_SIMPLEMODELFFX_MODEL_INSTANCE = 0x318,
|
||||
|
||||
// --- Character-model compositor ("CharComponent", `Model::DisplayInfo`) --
|
||||
// The engine object that dresses a character-based model: resolves the
|
||||
// hair / facial-hair geosets (CharHairGeosets, CharacterFacialHairStyles —
|
||||
// beards, earrings, teeth), loads the baked NPC body texture or composites
|
||||
// the player one, and applies equipment geosets (sleeves / robe→trousers /
|
||||
// boots / tabard / cape, FUN_00477520). Units keep one at unit+0xD30,
|
||||
// driven by CGUnit_C::UpdateCharacterCustomization (FUN_005fb200) — the
|
||||
// single caller-of-record this whole API was derived from — behind the
|
||||
// poll gate FUN_00607da0 (dress only once FUN_MODEL_INSTANCE_LOADED says
|
||||
// the model is in). The portrait renderer (Script_SetPortraitTexture →
|
||||
// FUN_00524f60) waits on FUN_CHARCOMP_PUMP before rendering, which is why
|
||||
// portraits always show the finished appearance.
|
||||
FUN_CHARCOMP_CREATE = 0x00475FB0, // () -> builder from the engine pool at 0x00B42720; 0 on exhaustion
|
||||
FUN_CHARCOMP_DESTROY = 0x00476000, // (builder /*ecx*/) — releases refs (FUN_00476ac0), returns node to pool
|
||||
// __thiscall(builder, CharCompInfo*) -> bool. Copies 0x5B dwords
|
||||
// (0x16C bytes) to builder+0x18, validates race against ChrRaces, resolves
|
||||
// skin/hair/facial sections + geosets. Caller must AddRef the model first
|
||||
// (FUN_005fb200 does); the builder owns that ref from then on.
|
||||
FUN_CHARCOMP_SET_INFO = 0x00476B90,
|
||||
// __thiscall(builder, slot 0..9, itemDisplayInfoID) — feed one NPC
|
||||
// equipment piece (validated against ItemDisplayInfo.dbc, no-op for 0 /
|
||||
// out-of-range). Units pass CreatureDisplayInfoExtra +0x20..+0x44.
|
||||
FUN_CHARCOMP_SET_ITEM = 0x00478AA0,
|
||||
// __thiscall(builder, int *unused /*engine passes 0*/) -> bool done.
|
||||
// The per-frame pump: composites pending texture regions; the baked-NPC
|
||||
// path short-circuits to the geoset apply (FUN_00477520) and returns 1 on
|
||||
// the first call. Keep calling until it returns 1.
|
||||
FUN_CHARCOMP_PUMP = 0x00477860,
|
||||
// Model-instance API used around the compositor:
|
||||
FUN_MODEL_INSTANCE_ADDREF = 0x00710390, // (model /*ecx*/)
|
||||
// __thiscall(model, tryLoad, recurseChildren) -> bool loaded. The engine's
|
||||
// dress gate (FUN_00607da0) passes (0, 0) — a passive check.
|
||||
FUN_MODEL_INSTANCE_LOADED = 0x007103D0,
|
||||
// Attached-child list on a model instance (helm/shoulder item models the
|
||||
// compositor attaches via FUN_00712f70). Head + sibling-next verified from
|
||||
// FUN_MODEL_INSTANCE_LOADED's recurseChildren walk.
|
||||
OFF_MODEL_INSTANCE_CHILD_HEAD = 0x1DC,
|
||||
OFF_MODEL_INSTANCE_CHILD_NEXT = 0x1E4,
|
||||
// __thiscall(model, callback, userData) — stores the instance's pre-render
|
||||
// callback pair (instance+0x3BC/+0x3C0). CSimpleModelFFX::SetModelInstance
|
||||
// (FUN_0076cd30) registers FUN_SIMPLEMODELFFX_LIGHT_FOG_CB with the frame
|
||||
// as userData on ITS instance only; attached children have an empty slot
|
||||
// and render UNLIT (pure black — verified in-game: a light tint colored
|
||||
// the body but not the equipment). `Model::DisplayInfo` fills each child's
|
||||
// slot with the same pair so equipment lights like the body.
|
||||
FUN_MODEL_INSTANCE_SET_RENDER_CB = 0x007134B0,
|
||||
// __fastcall-shaped engine callback (never called by us directly): applies
|
||||
// the owning frame's fog (+0x3A4/+0x3A8) and light struct (+0x324, written
|
||||
// by Script_SetLight via FUN_0076cf30) to the render context each time the
|
||||
// instance is drawn.
|
||||
FUN_SIMPLEMODELFFX_LIGHT_FOG_CB = 0x0076D680,
|
||||
|
||||
// Engine Script_* method implementations `Frame::Modern` delegates to
|
||||
// (each `int __fastcall(void *L)`, reading self at stack index 1).
|
||||
// Addresses from the docs/raw_methods.txt catalogue. Show/Hide are
|
||||
@@ -2640,6 +3045,66 @@ enum Offsets {
|
||||
VAR_CREATURETYPE_COUNT = 0x00C0DE30,
|
||||
OFF_CREATURETYPE_NAMES = 0x04,
|
||||
|
||||
// CreatureDisplayInfo.dbc / CreatureModelData.dbc — the
|
||||
// creatureDisplayID → model-file chain behind `Model:SetDisplayInfo`.
|
||||
// Both standard pointer-array DBCs (records[id], sparse). Column offsets
|
||||
// verified two independent ways: parsing the extracted 1.12 DBCs
|
||||
// (C:\WoW\Octo\DBFilesClient) AND the engine's own unit-display resolver
|
||||
// FUN_0060afb0, which reads displayID from `[descriptor+0x1F4]` →
|
||||
// CreatureDisplayInfo[id] +0x04 ModelID → CreatureModelData[ModelID].
|
||||
// CreatureDisplayInfo: +0x04 ModelID (col 1 → CreatureModelData id);
|
||||
// +0x18/+0x1C/+0x20 TextureVariation[3] (cols 6/7/8 — creature skin,
|
||||
// bare filenames applied as replaceable-texture MONSTER_1/2/3).
|
||||
// CreatureModelData: +0x08 ModelPath (col 2, e.g. "Creature\Rat\Rat.mdx").
|
||||
VAR_CREATUREDISPLAYINFO_RECORDS = 0x00C0DE90,
|
||||
VAR_CREATUREDISPLAYINFO_COUNT = 0x00C0DE94,
|
||||
OFF_CREATUREDISPLAYINFO_MODEL_ID = 0x04,
|
||||
OFF_CREATUREDISPLAYINFO_TEXTURE_VARIATION = 0x18, // char*[3] @ +0x18/+0x1C/+0x20
|
||||
VAR_CREATUREMODELDATA_RECORDS = 0x00C0DE68,
|
||||
VAR_CREATUREMODELDATA_COUNT = 0x00C0DE6C,
|
||||
OFF_CREATUREMODELDATA_MODEL_PATH = 0x08,
|
||||
|
||||
// CreatureDisplayInfo.extendedDisplayInfoID (col 3) — nonzero for a
|
||||
// CHARACTER-based display (shared Character\Race\Sex base model). It indexes
|
||||
// CreatureDisplayInfoExtra.dbc, whose LAST field (col 18 in the 19-col
|
||||
// vanilla layout, +0x48) is the pre-composited body-skin filename. That
|
||||
// texture lives under Textures\BakedNpcTextures\ and applies to the M2
|
||||
// body-skin replaceable types (1/8) — NOT the monster slots, which a
|
||||
// character model has no texture units for (hence "renders white" without
|
||||
// this). The baked skin already carries the equipped body armor. Verified by
|
||||
// parsing the extracted DBC; C:\Git\Inklab resolves the same chain.
|
||||
OFF_CREATUREDISPLAYINFO_EXTENDED_ID = 0x0C,
|
||||
VAR_CREATUREDISPLAYINFOEXTRA_RECORDS = 0x00C0DEA4,
|
||||
VAR_CREATUREDISPLAYINFOEXTRA_COUNT = 0x00C0DEA8,
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME = 0x48, // col 18 (last), string
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_RACE = 0x04, // col 1 (ChrRaces id)
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_SEX = 0x08, // col 2 (0=male, 1=female)
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_HAIR_COLOR = 0x18, // col 6 (CharSections color)
|
||||
// Cols 3/4/5/7 + the NPC equipment block — field order verified from
|
||||
// CGUnit_C::UpdateCharacterCustomization (FUN_005fb200), which reads
|
||||
// +0x04..+0x1C into the compositor info struct and walks +0x20..+0x44
|
||||
// as 10 ItemDisplayInfo ids for FUN_CHARCOMP_SET_ITEM.
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_SKIN = 0x0C, // col 3
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_FACE = 0x10, // col 4
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_HAIR_STYLE = 0x14, // col 5
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_FACIAL_HAIR = 0x1C, // col 7
|
||||
OFF_CREATUREDISPLAYINFOEXTRA_EQUIP = 0x20, // cols 8..17: u32[10] ItemDisplayInfo ids
|
||||
|
||||
// CharSections.dbc — per-race/sex character texture layers. Scanned by
|
||||
// Model:SetDisplayInfo to resolve a character display's HAIR texture (applied
|
||||
// to the M2 hair replaceable type 6, else the hair geoset renders white).
|
||||
// Standard pointer-array DBC; match rows on race/sex/baseSection (3 = Hair) +
|
||||
// color, then read the first texture column. Verified against the extracted
|
||||
// DBC: Human Female (race 1, sex 1) hairColor 0 -> "Character\Human\Hair00_00.blp".
|
||||
VAR_CHARSECTIONS_RECORDS = 0x00C0DF6C,
|
||||
VAR_CHARSECTIONS_COUNT = 0x00C0DF70,
|
||||
OFF_CHARSECTIONS_RACE = 0x04, // col 1
|
||||
OFF_CHARSECTIONS_SEX = 0x08, // col 2
|
||||
OFF_CHARSECTIONS_BASE_SECTION = 0x0C, // col 3 (3 = Hair)
|
||||
OFF_CHARSECTIONS_COLOR = 0x14, // col 5
|
||||
OFF_CHARSECTIONS_TEXTURE = 0x18, // col 6 (texture[0] path)
|
||||
CHARSECTIONS_BASE_SECTION_HAIR = 3,
|
||||
|
||||
// Race → faction group, mirroring the player branch of
|
||||
// `Script_UnitFactionGroup` (`0x00516630`) — backs
|
||||
// `C_CreatureInfo.GetFactionInfo(raceID)`. Chain:
|
||||
@@ -4358,6 +4823,15 @@ enum Offsets {
|
||||
// `BaseLib::StringLib` registers `string.gmatch` as a direct alias of
|
||||
// this function pointer — no wrapper.
|
||||
FUN_LUA_STR_GFIND = 0x007FCFA0,
|
||||
// `str_format` — the Lua 5.0 `string.format` C function (`int
|
||||
// __fastcall(void *L)`), strlib entry at `0x00822dc0`. Verified by decode:
|
||||
// reads the format at arg 1, handles `%%`, `%d/i/c`, `%e/E/f/g/G`,
|
||||
// `%o/u/x/X`, `%s`, `%q`, with the "invalid option to 'format'" / "invalid
|
||||
// format (width or precision too long)" errors. FontString:SetFormattedText
|
||||
// pushes THIS directly and pcalls it, rather than resolving `_G.string.format`
|
||||
// — so an addon replacing `string.format` cannot hijack or break it (matches
|
||||
// retail, whose SetFormattedText formats in C).
|
||||
FUN_LUA_STR_FORMAT = 0x007FD3D0,
|
||||
// `math_fmod` — the Lua 5.0 `math.mod` C function (mathlib entry at
|
||||
// `0x00822cb0`): `luaL_checknumber(1/2)` → `fmod(x, y)` → `lua_pushnumber`.
|
||||
// 5.1 renamed the Lua-facing name to `math.fmod` with the identical C
|
||||
@@ -5029,6 +5503,13 @@ enum Offsets {
|
||||
MOVEFLAG_FALLING = 0x2000,
|
||||
MOVEFLAG_FALLING_FAR = 0x4000,
|
||||
MOVEFLAG_SWIMMING = 0x200000,
|
||||
// The player-movement bits that drop an in-progress cast — the exact mask
|
||||
// the engine's CheckCast (FUN_006094f0) tests (0x200f). Spell::Cast uses it
|
||||
// to tell a spurious movement drop of a movement-immune cast (grenades)
|
||||
// from a real cancel/interrupt, which happens with the player stationary.
|
||||
MOVEFLAG_MASK_CAST_DROP = MOVEFLAG_FORWARD | MOVEFLAG_BACKWARD |
|
||||
MOVEFLAG_STRAFE_LEFT | MOVEFLAG_STRAFE_RIGHT |
|
||||
MOVEFLAG_FALLING, // = 0x200f
|
||||
|
||||
// CGPlayer-local pointer to the GameObject the current spell cast
|
||||
// is targeting. Holds a heap pointer (high bits in the user-mode
|
||||
@@ -5180,6 +5661,22 @@ enum Offsets {
|
||||
// `RET 0x4` (callee cleans the one stack arg).
|
||||
FUN_WORLD_TICK = 0x0066FD50,
|
||||
|
||||
// UI render root — the CSimpleTop singleton's (`DAT_00cf0bd8`) per-frame
|
||||
// render callback. Registered at priority 1.0 into the global HLAYER list
|
||||
// (`FUN_00442800`) and drained every frame by the master scene-render pump
|
||||
// `FUN_00442350`; body is just `FUN_00765650(root, ...)` (UI layout pass)
|
||||
// then `FUN_007657d0(root)` (the strata walk that draws every UI frame).
|
||||
// Unlike FUN_WORLD_TICK this fires in BOTH glue and world: the root ctor
|
||||
// `FUN_00764180` runs from the world-init path (`FUN_0048FBF0`) AND the
|
||||
// glue-boot path (`FUN_0046A7B0`), so the UI renders — and this callback
|
||||
// fires — on the login/character-select screens too. This is the tick to
|
||||
// drive UI-object upkeep that must also run on glue (inline-texture icon
|
||||
// regions), where FUN_WORLD_TICK is silent. `__stdcall(void *bounds,
|
||||
// int flag)`, `RET 0x8`; clean PUSH EBP / MOV EBP,ESP prologue (MinHook-
|
||||
// safe). Single caller (the HLAYER drain), quiet render region — not a
|
||||
// known collision target for the other Octo DLLs.
|
||||
FUN_UI_RENDER_ROOT = 0x00764330,
|
||||
|
||||
// SMSG_BINDPOINTUPDATE handler. The server sends this packet on
|
||||
// initial login (to sync the player's current bind) and again
|
||||
// every time the player binds at a new innkeeper. The handler
|
||||
@@ -5819,6 +6316,12 @@ enum Offsets {
|
||||
SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40,
|
||||
SPELL_ATTR_EX2_AUTOREPEAT_FLAG = 0x20,
|
||||
OFF_SPELL_RECORD_INTERRUPT_FLAGS = 0x54, // u32 (column 21)
|
||||
// Bit 0 of InterruptFlags: the cast is interrupted when the caster moves.
|
||||
// Both the server (HandleMovementOpcodes → InterruptSpellsWithInterruptFlags,
|
||||
// tortoise-wow) and the client's own CheckCast (FUN_006094f0 gates its MOVING
|
||||
// failure on this bit) use it; thrown items (grenades — e.g. spell 4068)
|
||||
// lack it, so movement must not end their cast. See Spell::Cast (issue #23).
|
||||
SPELL_INTERRUPT_FLAG_MOVEMENT = 0x1,
|
||||
|
||||
// Remaining Spell.dbc record fields — the single source of truth for the
|
||||
// record layout (byte = column * 4). Modules must use these rather than
|
||||
@@ -6207,4 +6710,345 @@ enum Offsets {
|
||||
// `__fastcall(const char *name) -> definition node`, 0 if unregistered
|
||||
// (case-insensitive, hashed). See FUN_006ee6f0 in Templates.cpp notes.
|
||||
FUN_XML_TEMPLATE_LOOKUP = 0x006EE6F0,
|
||||
|
||||
// --- Inline texture escape (`|Tpath:h:w:...|t`) backport -------------------
|
||||
// See src/text/InlineTexture.cpp and docs/InlineTextureEscapes.md. The
|
||||
// 1.12 text pipeline is a shared `|`-tokenizer feeding per-purpose loops.
|
||||
// Slice 1 injects an inline-texture quad into the text PAINT pass.
|
||||
|
||||
// Text paint pass — flushes each text line's per-font-page glyph vertex
|
||||
// batches to the GPU every frame. `__fastcall(layoutObj)`; gated on
|
||||
// DAT_00c2b9d4. Co-hooked post-original: the flush walks the layout's node
|
||||
// list and queues each recorded inline icon as a fontstring-anchored region
|
||||
// placement (Text::InlineTexturePool). See FUN_005c8fe0.
|
||||
// (The GxU raw-quad primitives this site once drew with — dynamic-VB
|
||||
// lock/write/submit, texture bind/load/force-decode, colorop state — were
|
||||
// removed with the quad render mode; VAs are preserved in
|
||||
// docs/InlineTextureEscapes.md and git history if ever needed again.)
|
||||
FUN_TEXT_PAINT = 0x005C8FE0,
|
||||
|
||||
// Default texture-load blend arg (DAT_00878cf0) — passed to the engine's
|
||||
// SetTexture-by-path (FUN_SIMPLETEXTURE_SET_TEXTURE) exactly as
|
||||
// Script_Texture_SetTexture's own call site does.
|
||||
VAR_TEXTURE_BLEND_DEFAULT = 0x00878CF0,
|
||||
|
||||
// --- Inline-texture positioning (slice 1: measure/emit integration) --------
|
||||
// The 1.12 text pipeline builds a layout as a list of render "nodes" (one
|
||||
// per wrapped line); each node owns per-font-page glyph vertex batches and a
|
||||
// screen origin, and the paint pass (FUN_005c8fe0) walks the layout's node
|
||||
// list drawing each node's batches translated by that origin. See
|
||||
// docs/InlineTextureEscapes.md and the decompiled map in InlineTexture.cpp.
|
||||
|
||||
// Per-line glyph emitter. `__thiscall(node, byte *text, int len, uint
|
||||
// *colorState, float *penXYZ, uint *pageMask, int *linkState)`. Reads the
|
||||
// pen start from penXYZ[0..2] (node-local), appends glyph quads to the
|
||||
// node's page batches, and on return writes the final pen x (node-local,
|
||||
// as a FLOAT via `FSTP dword` — read linkState[4] as float, not int) into
|
||||
// linkState[4]. We co-hook it and, for a line that
|
||||
// contains an inline `|T…|t`, render the plain runs by delegating to the
|
||||
// original per segment (threading the pen via linkState[4]) and record an
|
||||
// icon quad at the pen between segments. Safe to call repeatedly per line
|
||||
// because the batch-clear at its top is gated on OFF_TEXT_NODE_FLAGS bit 3,
|
||||
// which is clear during normal accumulation draw. See FUN_005ccbe0.
|
||||
FUN_TEXT_EMITTER = 0x005CCBE0,
|
||||
// Node flags. Bit 3 (0x08) gates the emitter's per-call batch-clear (set
|
||||
// only in a rebuild mode we don't take); we segment only when it's clear.
|
||||
// Bit 6 (0x40) = EDITABLE text (set on editbox content; verified in-game:
|
||||
// the macro editbox is 0x4D, chat display 0x20D, FontStrings 0x0D). It rides
|
||||
// in the tokenizer's flags argument, so we suppress inline rendering per node
|
||||
// for editboxes — they show raw, editable `|T…|t` markup. This is the 1.12
|
||||
// analog of 4.3.4's per-render texture-disable flag (tokenizer bit 0x1000).
|
||||
OFF_TEXT_NODE_FLAGS = 0x5C,
|
||||
// Node horizontal justify (int): 0 = left, 1 = centre, 2 = right — the draw
|
||||
// builder's encoding. The emitter pre-shifts an inline icon's pen by the
|
||||
// line's justify offset for centre/right nodes (left needs no shift).
|
||||
// Verified in-game against centred/right-aligned text layout.
|
||||
OFF_TEXT_NODE_JUSTIFY = 0x54,
|
||||
// Node screen origin (float x, float y). The paint pass translates the
|
||||
// node's node-local glyph verts by this; an inline icon recorded in
|
||||
// node-local pen coords is drawn at (localX + originX, localY + originY).
|
||||
OFF_TEXT_NODE_ORIGIN_X = 0x70,
|
||||
OFF_TEXT_NODE_ORIGIN_Y = 0x74,
|
||||
// Node → per-font-page vertex buffers. The glyph emitter (FUN_TEXT_EMITTER)
|
||||
// bakes each glyph as a 4-vertex quad into one of up to TEXT_NODE_PAGE_COUNT
|
||||
// font-page buffers hanging off the node; the paint copy-out (FUN_005c8710)
|
||||
// reads them back TRANSLATE-ONLY (gpu.xy = local.xy + node origin). So a
|
||||
// rotation baked into these node-local verts survives to the screen — the
|
||||
// basis for fontstring:SetRotation (Texture::Transform::RotateFontStringNode).
|
||||
// node + OFF_TEXT_NODE_PAGE_BUFFERS + page*4 -> page buffer ptr (may be 0)
|
||||
// pageBuf + OFF_TEXT_PAGE_VERT_COUNT -> int vertex count
|
||||
// pageBuf + OFF_TEXT_PAGE_VERTS -> float* vertex array
|
||||
// each vertex = 5 floats {x, y, z, u, v}, stride TEXT_VERT_STRIDE bytes.
|
||||
// Cross-verified: the emitter (writes verts here), FUN_005c8710 (copies them
|
||||
// to the GPU buffer), and FUN_005ce0c0 (indexes the page array) all agree.
|
||||
OFF_TEXT_NODE_PAGE_BUFFERS = 0xA0,
|
||||
TEXT_NODE_PAGE_COUNT = 8,
|
||||
OFF_TEXT_PAGE_VERT_COUNT = 0xC,
|
||||
OFF_TEXT_PAGE_VERTS = 0x10,
|
||||
TEXT_VERT_STRIDE = 0x14, // bytes; 5 floats per vertex (x, y, z, u, v)
|
||||
// Per-page baked PER-GLYPH colours (BGRA dword per vertex), parallel to the
|
||||
// vertex array above. Written by the emitter's bit-3-clear path
|
||||
// (`[pageBuf+0x20] + ([pageBuf+0x1C] + i)*4 = colorState` at 0x005CCC0x);
|
||||
// the paint copy-out FUN_005c8710 uses them VERBATIM — but only when the
|
||||
// colour count equals the vertex count, else it falls back to the node's
|
||||
// uniform colour (OFF_TEXT_NODE_COLOR). No later modulation: whatever alpha
|
||||
// is baked here is final, which is why InlineTexture's flush mirrors the
|
||||
// node's live alpha into these entries per frame.
|
||||
OFF_TEXT_PAGE_COLOR_COUNT = 0x1C,
|
||||
OFF_TEXT_PAGE_COLORS = 0x20,
|
||||
// Node uniform colour (BGRA). Seeded from fs colors[0] at block creation
|
||||
// (FUN_007724a0 → FUN_0044d420) and rewritten on every colour/alpha change
|
||||
// via the fs colour-changed vmethod (FUN_00772180 → FUN_0044d650 →
|
||||
// FUN_005ccb40). Its alpha byte (+0x2F) is the FOLDED live opacity —
|
||||
// SetTextColor alpha (fs+0xA8 array) × the owning frame's effective alpha
|
||||
// (*(fs+0x9C)+0xC8), maintained by FUN_0077fac0 — so it tracks parent
|
||||
// SetAlpha per change. The paint reads it per frame for uniform text; the
|
||||
// emitter's |c splice (case 0) and the draw builder's colorState seed both
|
||||
// copy it at bake time. NOTE: FUN_005ccb40 only invalidates (re-bakes) a
|
||||
// node on colour change when bit 3 is CLEAR — a bit-3 (standalone
|
||||
// FontString) node with baked per-glyph colours goes stale instead, hence
|
||||
// the flush-side alpha mirror.
|
||||
OFF_TEXT_NODE_COLOR = 0x2C,
|
||||
// Layout node list — intrusive singly-linked list of render nodes. Head at
|
||||
// [layout+0x24]; next = *(node + [layout+0x1c] + 4); the tail sentinel has
|
||||
// its low bit set (mirrors the paint pass's own walk in FUN_005c8fe0).
|
||||
OFF_TEXT_LAYOUT_NODE_HEAD = 0x24,
|
||||
OFF_TEXT_LAYOUT_NODE_LINK = 0x1C,
|
||||
// Node source-text pointer. The draw builder FUN_005cdc20 starts from
|
||||
// [node+0x48] and advances it per wrapped line, calling the emitter once
|
||||
// per line on the SAME node — so `emitterText == [node+0x48]` identifies
|
||||
// the first wrapped line (used to clear the node's icon list once per
|
||||
// build, then accumulate across lines). This buffer is a node-owned COPY of
|
||||
// the DISPLAY text (SStrCopy'd in the node init FUN_005cd6d0, capacity
|
||||
// [node+0x4c]) — the full source when it fits, or the ellipsized
|
||||
// "<prefix>..." form when the display-text resolver truncated it. So
|
||||
// comparing it against the fs source (OFF_FONTSTRING_TEXT) is the render-
|
||||
// truth "displayed vs full" signal FontString:IsTruncated uses.
|
||||
OFF_TEXT_NODE_TEXT = 0x48,
|
||||
// Node font-size field (float), fed to the font-height helper.
|
||||
OFF_TEXT_NODE_FONT_SIZE = 0x1C,
|
||||
// Node's gxu font-face pointer. The emitter FUN_005ccbe0 reads *(node+0x44)
|
||||
// as the `this` for every glyph call (glyph lookup FUN_005cabd0, the pair
|
||||
// advance, the native-height getter below).
|
||||
OFF_TEXT_NODE_FONT_FACE = 0x44,
|
||||
// Font pixel-height helper. `__fastcall(int flag /*ecx = (nodeFlags>>7)&1*/,
|
||||
// float fontSize /*stack = [node+0x1c]*/) -> float` (returns in ST0). The
|
||||
// emitter calls it to size glyphs; we call it to derive the text's vertical
|
||||
// extent so an inline icon can be centred on the line independent of font
|
||||
// size. See FUN_005c6fa0.
|
||||
FUN_TEXT_FONT_HEIGHT = 0x005C6FA0,
|
||||
// Font-face NATIVE pixel height getter: `__fastcall(void *fontFace) -> int`
|
||||
// (returns *(font+0x24C)). The emitter's pen scale is
|
||||
// FUN_TEXT_FONT_HEIGHT(...) / (float)nativeHeight — glyph-record advances
|
||||
// are in native-font units and multiply by that scale into pen units.
|
||||
// See FUN_005cae90.
|
||||
FUN_TEXT_FONT_NATIVE_HEIGHT = 0x005CAE90,
|
||||
// Glyph PAIR advance: `__thiscall(void *fontFace, uint prevCh, uint curCh)
|
||||
// -> float (ST0)`, native-font units = baseAdvance(prevCh) +
|
||||
// kern(prevCh,curCh)·[font+0x188], memoized per pair. The emitter computes
|
||||
// this at the top of each token iteration and places the CURRENT glyph at
|
||||
// pen + pairAdvance(prev, cur) — i.e. the pen is LAZY: a glyph's own
|
||||
// advance is only consumed when the NEXT token lands. Consequence
|
||||
// (verified in FUN_005ccbe0's tail): the emitter's final pen write
|
||||
// (linkState[4]) is x(lastGlyph) + pairAdvance(secondLast, last) — the
|
||||
// last pair RE-ADDED as a stand-in for the last glyph's own advance. For a
|
||||
// single-glyph run that stand-in is 0 (no pair ever computed) → the
|
||||
// read-back reports the run as zero-width (the money-string "coin sits on
|
||||
// the lone digit" bug). The emitter co-hook corrects the read-back with
|
||||
// these helpers: pen = linkState[4] − pairAdvance(prev,last)·scale +
|
||||
// baseAdvance(last)·scale. See FUN_005ca2d0.
|
||||
FUN_TEXT_GLYPH_PAIR_ADVANCE = 0x005CA2D0,
|
||||
// The flags&0x10 variant of the pair advance (same shape; the emitter
|
||||
// selects it when node flags bit 4 is set). See FUN_005ca4b0.
|
||||
FUN_TEXT_GLYPH_PAIR_ADVANCE_ALT = 0x005CA4B0,
|
||||
// Glyph BASE advance: `__thiscall(void *fontFace, uint ch) -> float (ST0)`,
|
||||
// native-font units, from the font's glyph cache (present for any glyph the
|
||||
// emitter just drew; returns a default for uncached). This is the pair
|
||||
// advance's first term — the terminal-glyph advance with no following
|
||||
// kern, exactly what an inline icon following the glyph needs. See
|
||||
// FUN_005ca240.
|
||||
FUN_TEXT_GLYPH_BASE_ADVANCE = 0x005CA240,
|
||||
// Hyperlink hit-rect registration (the GXUFONTHYPERLINKINFO append).
|
||||
// `__thiscall(node, float yA, float xLeft, float yB, float xRight,
|
||||
// char *linkStart, uint linkLen, char *escStart, uint escLen)` — 8 stack
|
||||
// dwords, RET 0x20. Appends a 0x20-byte record to the node's link array
|
||||
// (cap +0x7C, count +0x80, data +0x84): the four floats normalized to
|
||||
// screen space ({yA,left,yB,right} — the engine's y-first rect order; the
|
||||
// emitter's |H open writes linkState[2]=xLeft, |h close linkState[4]=
|
||||
// xRight, the draw builder provides the line's yA/yB), then the four link
|
||||
// dwords raw. Called from the emitter's |h close (case 5). The y extent is
|
||||
// the TEXT band (fontH tall) — co-hooked so a link containing a tall
|
||||
// inline icon gets its hit band expanded by the icon's overflow (the
|
||||
// line-height-growth counterpart; without it only the text-high slice of
|
||||
// a 32px emote link was hoverable). See FUN_005cd310.
|
||||
FUN_TEXT_LINK_RECT_ADD = 0x005CD310,
|
||||
|
||||
// Shared `|`-escape tokenizer. `__fastcall(byte *text /*ecx*/, int
|
||||
// *bytesConsumed /*edx*/, uint *colorOut, uint flags, uint *payloadOut) ->
|
||||
// tokenType`. ~11 callers (measure loops, line-fit/wrap, the emitter, the
|
||||
// draw builder). It has NO `|T` case, so `|T` falls through to a literal
|
||||
// `|`. We co-hook it so an inline-texture span (`||T…||t` after the
|
||||
// FontString pipe-doubling, or a clean `|T…|t`) is consumed as ONE
|
||||
// near-zero-width token — this stops every measure/wrap caller from
|
||||
// counting the path text and wrapping early. The emitter detects icons on
|
||||
// its own (and delegates plain segments that never contain `|T`), so the
|
||||
// draw path is unaffected by this hook. See FUN_005c2810.
|
||||
FUN_TEXT_TOKENIZER = 0x005C2810,
|
||||
|
||||
// Shared wrap-stepper dispatcher — lays out ONE wrapped line per call.
|
||||
// `__fastcall(gxuFont /*ecx*/, byte *text /*edx*/, float fontH, float
|
||||
// wrapWidth, int *outBreak, float *outWidth, void *outNext, float indent,
|
||||
// uint flags, byte *p10)`, callee cleans 0x20 (verified RET 0x20; prologue
|
||||
// PUSH EBP; MOV EBP,ESP; FLD [EBP+8] — MinHook-safe). Thin validator that
|
||||
// routes flags&0x80 to the no-wrap path (FUN_005C7300, ignores wrapWidth)
|
||||
// and everything else to the real stepper FUN_005C7470. Its exactly 4
|
||||
// callers are EVERY wrap consumer: the draw builder FUN_005CDC20 (render
|
||||
// breaks), the height measure FUN_005C2070 (GetStringHeight), the
|
||||
// chars-that-fit counter FUN_005C21C0 (ellipsis truncation), and the
|
||||
// break-array computer FUN_005C2430 (behind FUN_00772B60) — so a co-hook
|
||||
// here keeps render, height, truncation, and break arrays mutually
|
||||
// consistent. Text::InlineTexture shrinks the wrapWidth arg by the line's
|
||||
// inline-icon advances (the tokenizer hook hides them from the measure, so
|
||||
// breaks otherwise land as if icons were 0 wide and the emitter's real
|
||||
// advances overflow the right edge). UNITS: each caller passes
|
||||
// fontH/wrapWidth in its OWN space (node text units from the builder, the
|
||||
// small anchor-converted space from the fs-level callers — the path chat
|
||||
// wraps through); a raw-pixel subtraction annihilates the small spaces
|
||||
// (shredded chat lines into 2-glyph fragments on first flight). Convert
|
||||
// px→caller units by (fontH / FUN_TEXT_FONT_HEIGHT(flag, fontH)) — the
|
||||
// engine's own pixel realization of the fontH param (FUN_005C6940's final
|
||||
// scale uses exactly that call), so the ratio is valid in every caller's
|
||||
// space.
|
||||
FUN_TEXT_WRAP_STEPPER = 0x005C7260,
|
||||
|
||||
// Focused-editbox global — holds the CSimpleEditBox that currently has
|
||||
// keyboard focus (the one showing a cursor), or 0 when no input field is
|
||||
// active. Written by EditBox SetFocus (`mov [0xcf4dc8],esi` @0x0077e3f8),
|
||||
// cleared by ClearFocus (FUN_0077e410). We read it (plus the buffer offsets
|
||||
// below) to identify the focused editbox's OWN text and suppress inline-icon
|
||||
// rendering for JUST that text, so the input field shows raw, editable
|
||||
// `|T…|t` markup while everything else (chat history icons/emotes) keeps
|
||||
// rendering. See the editbox vtable at 0x0081c8c0 / FUN_0077e410.
|
||||
VAR_FOCUSED_EDITBOX = 0x00CF4DC8,
|
||||
// CSimpleEditBox text-buffer layout — the editbox lays out and measures its
|
||||
// text IN PLACE from these buffers (no internal copy), so its layout node's
|
||||
// text pointer ([node+0x48]) and every wrapped-line text pointer point INTO
|
||||
// this buffer. Verified from the caret positioner FUN_0077da80: it selects
|
||||
// the buffer via `(*(byte*)(fe+0x318) & 8) ? *(char**)(fe+0x334) :
|
||||
// *(char**)(fe+0x32C)` and measures substrings of it directly with
|
||||
// FUN_00772ae0(fs, feBuf+off, n). Used by Text::InlineTexture to correlate a
|
||||
// node/token to the focused editbox by pointer range.
|
||||
OFF_EDITBOX_BUFFER_SELECT = 0x318, // byte; bit 3 (0x08) picks the masked buffer
|
||||
OFF_EDITBOX_BUFFER = 0x32C, // char* — normal text buffer
|
||||
OFF_EDITBOX_BUFFER_MASKED = 0x334, // char* — masked/password variant buffer
|
||||
// The editbox's DISPLAY FontString ([fe+0x328], param_1[0xca] in FUN_0077da80).
|
||||
// Its rendered text is a COPY of the input buffer, stored at
|
||||
// *(fs + OFF_FONTSTRING_TEXT) — the editbox lays out (renders) from this copy,
|
||||
// NOT from the input buffer above (which only the caret/measure path reads in
|
||||
// place). So identifying the focused editbox's own render nodes needs BOTH
|
||||
// buffers. Verified: FUN_00771d80 (FontString SetText) writes the copy to
|
||||
// *(this+0xF0) via SStrDup.
|
||||
OFF_EDITBOX_TEXT_FONTSTRING = 0x328, // CSimpleFontString* — the display text object
|
||||
|
||||
// --- FontString measure internals (GetStringWidth icon fix + GetStringHeight) ---
|
||||
// CSimpleFontString::GetStringWidthInternal — `float(__fastcall)(fs /*ecx*/)`,
|
||||
// returns in x87 ST0. Lazily computes into the fs+0xFC cache (ANCHOR units,
|
||||
// sentinel 0.0f = DAT_007ffd74): display-text getter FUN_00771EC0, then measure
|
||||
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
|
||||
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
|
||||
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
|
||||
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
|
||||
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
|
||||
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
|
||||
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
|
||||
// inline |T icon advances; the hook must NEVER write the fs+0xFC cache (the
|
||||
// original may serve the cached value — the icon sum is re-added per call).
|
||||
FUN_FONTSTRING_STRING_WIDTH = 0x00772890,
|
||||
// CSimpleFontString::GetStringHeightInternal — `float(__fastcall)(fs)`, ST0.
|
||||
// Wrap-aware: FUN_0044D750 → FUN_005C2070 → wrap engine FUN_005C7260 (the
|
||||
// same wrap math the render uses); returns lines*fontH + (lines-1)*spacing
|
||||
// (spacing = fs+0xF4), cached at fs+0x100 (anchor units, 0.0 sentinel; empty
|
||||
// text → 0). Wrap width = the effective-width vmethod (fs+0x24 vtbl +0x1C).
|
||||
// Used internally by the tooltip auto-size (0x00530640), multi-line editbox
|
||||
// height (0x0077D4D0), and ScrollingMessageFrame line stacking (0x00788750)
|
||||
// — FontString::Metrics only CALLS it (the GetStringHeight backport), no hook.
|
||||
FUN_FONTSTRING_STRING_HEIGHT = 0x007729B0,
|
||||
// Per-fontstring font-height getter, ANCHOR units (reads fs+0xE4; with
|
||||
// mode=1 and measure-flag 0x200 set, re-derives from the gxu font's native
|
||||
// height). ECX = fs, mode on the STACK (`PUSH 0x1; MOV ECX,ESI; CALL` at
|
||||
// 0x007728F4) → declare `__fastcall(fs, edx_unused, int mode)`. The ENGINE's
|
||||
// width path multiplies its return by [fs+0x7C] (the layout UI SCALE) before
|
||||
// handing it to the measure core. Our width hook instead converts it to UI
|
||||
// PIXELS (× the anchor→px push factor VAR_UI_COORD_SCALE_DIV × 1024 /
|
||||
// VAR_UI_COORD_SCALE_MUL, ≈1468) to resolve `:0` auto-sized icon dims —
|
||||
// escape sizes are PIXELS, not fs+0x7C-scaled pen units; dividing a pixel
|
||||
// advance by fs+0x7C (~0.68) inflated a 16px icon to +44k measured px on
|
||||
// first flight. Distinct from FUN_TEXT_FONT_HEIGHT (0x005C6FA0), the
|
||||
// node-level gxu helper.
|
||||
FUN_FONTSTRING_FONT_HEIGHT = 0x007727B0,
|
||||
// FontString measure-flags word (the `flags` arg the measure paths pass to
|
||||
// FUN_0044D670/FUN_0044D750, and the justify field: bits 0-2). The measure
|
||||
// core translates fs-level bits to gxu/tokenizer flags; bit 0x1000 → gxu
|
||||
// bit 0x40 (OFF_TEXT_NODE_FLAGS bit 6 — the bit the tokenizer co-hook stands
|
||||
// down on and the width hook tests).
|
||||
//
|
||||
// NOTE — bit 0x1000 is NON-SPACE-WRAP, not a dedicated "editable" flag: it is
|
||||
// exactly what FontString:SetNonSpaceWrap toggles (verified in its setter
|
||||
// FUN_0079E9F0: `fs+0x120 ^= 0x1000`). Text::InlineTexture uses gxu 0x40 as an
|
||||
// EDITBOX PROXY — it works because the macro editor (the multi-line editbox
|
||||
// the focused-buffer pointer test misses) enables non-space-wrap while chat/
|
||||
// display text does not (observed node flags: macro editor 0x4D, chat 0x20D/
|
||||
// 0x205). The correlation is not a guarantee: a DISPLAY fontstring with
|
||||
// SetNonSpaceWrap(true) + inline |T icons would have its icons wrongly
|
||||
// suppressed. Rare and never observed; the focused-buffer pointer test remains
|
||||
// the primary editbox guard.
|
||||
OFF_FONTSTRING_MEASURE_FLAGS = 0x120,
|
||||
|
||||
// CSimpleFontString::GetDisplayText — the engine's truncation MECHANISM (the
|
||||
// reason FontString:IsTruncated has anything to detect). `char* __thiscall(fs
|
||||
// /*ecx*/, float availW, float availH)` (dummy-EDX __fastcall form; RET 0x8).
|
||||
// Reads the raw text at fs+0xF0 and, when the box is BOUNDED (availW>0 AND
|
||||
// (maxLines fs+0x128 != 0 OR availH>0)), measures how much fits via the
|
||||
// chars-that-fit counter FUN_0044D960; on overflow it writes "<prefix>...\0"
|
||||
// into the shared scratch buffer VAR_TEXT_ELLIPSIS_BUFFER and returns THAT,
|
||||
// else the fs+0xF0 pointer (or NULL). The rebuild FUN_007724A0 calls it with
|
||||
// availW=(rect.right−rect.left)/[fs+0x7C], availH=(rect.bottom−rect.top)/
|
||||
// [fs+0x7C] and lays out whatever it returns — the node then COPIES that
|
||||
// string into OFF_TEXT_NODE_TEXT. IsTruncated does NOT call this (the rect at
|
||||
// fs+0x64 is zero between frames, so a Lua-time re-call always sees an
|
||||
// unbounded box); it reads the node copy instead. Kept for the availW/availH
|
||||
// derivation + the bounded-box gate. Verified in the Octo binary: prologue
|
||||
// 55 8B EC 83 EC 0C 53 56 8B F1, ellipsis literal "..." at 0x00800188.
|
||||
FUN_FONTSTRING_DISPLAY_TEXT = 0x00771EC0,
|
||||
// Shared 0x1000-byte BSS scratch buffer the display-text resolver above writes
|
||||
// the truncated, "..."-suffixed string into (memset FUN_0064A5A0 at 0x00772054,
|
||||
// suffix append at 0x00772086) and returns on truncation. Transient — every
|
||||
// layout rebuild overwrites it, and the node keeps its OWN copy (see
|
||||
// OFF_TEXT_NODE_TEXT), so it is not read directly by any live path.
|
||||
VAR_TEXT_ELLIPSIS_BUFFER = 0x00CF0CC0,
|
||||
|
||||
// Per-fontstring MAX LINE COUNT. 0 = no cap. When non-zero, the display-text
|
||||
// resolver forces availH = maxLines × lineHeight (verified in FUN_00771EC0:
|
||||
// `if (fs+0x128 != 0) param2 = fs+0x128 * lineHeight`), so text past that
|
||||
// many wrapped lines is ellipsized. Backs FontString:SetMaxLines/GetMaxLines
|
||||
// — the truncation control 1.12 never exposed for fontstrings (SetMaxLines is
|
||||
// ScrollingMessageFrame-only). Vanilla fontstrings always word-wrap, so this
|
||||
// is the way to force a single-line, ellipsized label: SetMaxLines(1).
|
||||
OFF_FONTSTRING_MAX_LINES = 0x128,
|
||||
// Lazily-computed measure caches (anchor units, 0.0f = dirty sentinel):
|
||||
// fs+0xFC = string width (GetStringWidth), fs+0x100 = string height
|
||||
// (GetStringHeight). SetText (FUN_00771D80) zeroes BOTH when the content
|
||||
// changes; SetMaxLines mirrors that so the getters recompute after the cap.
|
||||
OFF_FONTSTRING_WIDTH_CACHE = 0xFC,
|
||||
OFF_FONTSTRING_HEIGHT_CACHE = 0x100,
|
||||
// Ref-counted handle DecRef (resolve via FUN_0041af30 → --refcount → dtor at
|
||||
// 0). `__fastcall(void *handle)`. SetText releases the text-block handle
|
||||
// (fs+0xF8) through it before nulling the field; SetMaxLines does the same to
|
||||
// force a fresh node rebuild with the new line cap.
|
||||
FUN_HANDLE_RELEASE = 0x0041AED0,
|
||||
// VisibleRegion layout invalidate — marks the region (the fs+0x24 layout
|
||||
// subobject) dirty and propagates to parent/children so it re-lays-out.
|
||||
// SetText's tail calls it `FUN_007680e0(fs+0x24, 0)`. `__thiscall(region,
|
||||
// int)` (dummy-EDX form; the int arg is on the stack).
|
||||
FUN_FONTSTRING_LAYOUT_INVALIDATE = 0x007680E0,
|
||||
};
|
||||
|
||||
@@ -103,14 +103,14 @@ int CGItemCount(const uint8_t *item) {
|
||||
auto *desc = Item::ObjectFields(item);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const int *>(desc + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
return Game::Read<int>(desc, Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
}
|
||||
|
||||
uint64_t CGItemGuid(const uint8_t *item) {
|
||||
auto *inst = Item::InstanceBlock(item);
|
||||
if (inst == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
return Game::Read<uint64_t>(inst, Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
}
|
||||
|
||||
struct SlotInfo {
|
||||
@@ -135,18 +135,18 @@ SlotInfo ReadSlot(void *L, int bag, int slot) {
|
||||
// --- auction-house globals ----------------------------------------------
|
||||
|
||||
uint64_t AuctioneerGuid() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(
|
||||
const uint32_t lo = Game::Read<uint32_t>(
|
||||
Offsets::VAR_AUCTION_AUCTIONEER_GUID_LO);
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(
|
||||
const uint32_t hi = Game::Read<uint32_t>(
|
||||
Offsets::VAR_AUCTION_AUCTIONEER_GUID_HI);
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
}
|
||||
bool AHOpen() { return AuctioneerGuid() != 0; }
|
||||
|
||||
void SetSellGuid(uint64_t guid) {
|
||||
*reinterpret_cast<uint32_t *>(Offsets::VAR_AUCTION_SELL_GUID_LO) =
|
||||
Game::Ref<uint32_t>(Offsets::VAR_AUCTION_SELL_GUID_LO) =
|
||||
static_cast<uint32_t>(guid);
|
||||
*reinterpret_cast<uint32_t *>(Offsets::VAR_AUCTION_SELL_GUID_HI) =
|
||||
Game::Ref<uint32_t>(Offsets::VAR_AUCTION_SELL_GUID_HI) =
|
||||
static_cast<uint32_t>(guid >> 32);
|
||||
}
|
||||
|
||||
@@ -181,8 +181,8 @@ bool BagIsGeneral(int bag) {
|
||||
const uint8_t *rec = Item::PeekRecord(static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_ITEMSTATS_BAG_FAMILY) == 0;
|
||||
return Game::Read<uint32_t>(
|
||||
rec, Offsets::OFF_ITEMSTATS_BAG_FAMILY) == 0;
|
||||
}
|
||||
|
||||
bool FindFreeGeneralSlot(void *L, int *outBag, int *outSlot) {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "net/PacketReader.h"
|
||||
#include "net/SendObserver.h"
|
||||
#include "spell/Mod.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -32,12 +33,7 @@ namespace Aura::ComboDuration {
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
using Time::Clock::NowMs;
|
||||
|
||||
// ---- Combo-point snapshot (NetClient send co-hook) ------------------------
|
||||
|
||||
|
||||
+99
-76
@@ -21,6 +21,7 @@
|
||||
#include "guid/Guid.h"
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/IsSelfBuff.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -144,12 +145,10 @@ double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
// Permanent auras (paladin auras, passives, racials) carry an infinite
|
||||
// Spell.dbc duration (SpellDuration base -1); the server stores the fixed
|
||||
// cast-time in this slot for them, so it's always "already elapsed" and the
|
||||
// engine shows no countdown. The signed `(int)` compare also absorbs the
|
||||
// ~24.86-day GetTickCount wrap (both operands wrap together).
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
const uint32_t nowMs = reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
if (static_cast<int32_t>(nowMs - expirationMs) >= 0)
|
||||
// engine shows no countdown. `Time::Clock::Reached` takes the signed diff, so
|
||||
// it also absorbs the ~24.86-day GetTickCount wrap (both operands wrap
|
||||
// together).
|
||||
if (Time::Clock::Reached(expirationMs))
|
||||
return 0.0; // expiration reached/passed → permanent or no timer
|
||||
return static_cast<double>(expirationMs) * 0.001;
|
||||
}
|
||||
@@ -209,12 +208,7 @@ double SpellBaseDurationSeconds(uint32_t spellID, int unitLevel) {
|
||||
// the ~24.86-day GetTickCount wrap). An already-elapsed cached expiration is
|
||||
// not meaningful — the caster is kept regardless, but the time is reported as
|
||||
// unknown rather than as a negative remaining.
|
||||
bool ExpirationElapsed(uint32_t expMs) {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
const uint32_t nowMs = reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
return static_cast<int32_t>(nowMs - expMs) >= 0;
|
||||
}
|
||||
bool ExpirationElapsed(uint32_t expMs) { return Time::Clock::Reached(expMs); }
|
||||
|
||||
int PlayerLevel(const uint8_t *player) {
|
||||
auto *desc = Descriptor(player);
|
||||
@@ -224,23 +218,33 @@ int PlayerLevel(const uint8_t *player) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
}
|
||||
|
||||
// The caster GUID attributed to `(guid, spellID)`: the observed caster from
|
||||
// the Aura::Source cache, else — when we never saw the cast — the unit itself
|
||||
// if the spell is a self-only-target buff (self-buffs can't be cross-cast).
|
||||
// 0 when genuinely unknown. Single source of truth for both display
|
||||
// (sourceGUID/sourceUnit) and the PLAYER caster filter, so a self-buff shows a
|
||||
// source AND matches HELPFUL|PLAYER instead of the two paths disagreeing.
|
||||
uint64_t EffectiveCaster(uint64_t guid, uint32_t spellID) {
|
||||
// Caster + timing attributed to ONE aura instance — the aura `spellID` sitting
|
||||
// in absolute descriptor `slot` on `guid` (`Aura::Source::SLOT_UNBOUND` when
|
||||
// the caller has no slot: the out-of-range group array, the cache fallback).
|
||||
// The slot is what tells two casters' copies of one spell apart; without it
|
||||
// they are indistinguishable from a spell ID alone.
|
||||
//
|
||||
// The caster is the observed one from the Aura::Source cache, else — when we
|
||||
// never saw the cast — the unit itself if the spell is a self-only-target buff
|
||||
// (self-buffs can't be cross-cast), else 0. Single source of truth for both
|
||||
// display (sourceGUID/sourceUnit) and the PLAYER caster filter, so a self-buff
|
||||
// shows a source AND matches HELPFUL|PLAYER instead of the two paths
|
||||
// disagreeing.
|
||||
struct Attribution {
|
||||
uint64_t caster;
|
||||
uint32_t expirationMs;
|
||||
uint32_t durationMs;
|
||||
};
|
||||
|
||||
Attribution Attribute(uint64_t guid, uint32_t spellID, int slot) {
|
||||
Attribution a = {0, 0, 0};
|
||||
if (guid == 0 || spellID == 0)
|
||||
return 0;
|
||||
uint64_t c = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(guid, spellID, &c, &expMs, &durMs) && c != 0)
|
||||
return c; // observed caster
|
||||
if (Spell::IsSelfBuff::IsSelfBuff(spellID))
|
||||
return guid; // self-only-target aura → cast by the unit itself
|
||||
return 0;
|
||||
return a;
|
||||
Aura::Source::Get(guid, spellID, slot, &a.caster, &a.expirationMs,
|
||||
&a.durationMs);
|
||||
if (a.caster == 0 && Spell::IsSelfBuff::IsSelfBuff(spellID))
|
||||
a.caster = guid; // self-only-target aura → cast by the unit itself
|
||||
return a;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -277,7 +281,7 @@ bool IsPlayerCast(const uint8_t *unit, int slot) {
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 && EffectiveCaster(UnitGuid(unit), spellID) == player;
|
||||
return player != 0 && Attribute(UnitGuid(unit), spellID, slot).caster == player;
|
||||
}
|
||||
|
||||
// Applies the PLAYER / !PLAYER caster restriction. `isPlayerCast` is the
|
||||
@@ -327,12 +331,13 @@ bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID) {
|
||||
CcMatches(match.cc, spellID);
|
||||
}
|
||||
|
||||
// Group-array analog of IsPlayerCast: the group aura carries no caster, so we
|
||||
// consult the Aura::Source cache by (guid, spellID). A miss counts as "not the
|
||||
// player" (same as IsPlayerCast).
|
||||
// Group-array analog of IsPlayerCast: the member has no descriptor, so there is
|
||||
// no slot to attribute by and the cache is consulted by (guid, spellID) alone.
|
||||
// A miss counts as "not the player" (same as IsPlayerCast).
|
||||
bool GroupIsPlayerCast(uint64_t guid, uint32_t spellID) {
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 && EffectiveCaster(guid, spellID) == player;
|
||||
return player != 0 &&
|
||||
Attribute(guid, spellID, Aura::Source::SLOT_UNBOUND).caster == player;
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
@@ -527,10 +532,21 @@ static void BuildTable(void *L, uint32_t spellID, int applications,
|
||||
// buff table (gated on `isPlayer`); for everyone else it, the caster, and the
|
||||
// applied (caster-modified) duration come from the `Aura::Source` SMSG_SPELL_GO
|
||||
// cache when it observed the cast — a miss leaves the modern-truthful defaults
|
||||
// (expiration 0, no sourceUnit/GUID).
|
||||
// (expiration 0, no sourceUnit/GUID). `slot` is the absolute descriptor slot
|
||||
// the aura occupies, which is what attributes it to the right caster when two
|
||||
// of them hold the same spell on the unit; `Aura::Source::SLOT_UNBOUND` for the
|
||||
// paths with no descriptor to read one from.
|
||||
//
|
||||
// `known` short-circuits the by-(guid,spell,slot) resolve: the cache-fallback
|
||||
// paths already hold the exact entry's attribution (Enumerate copied it out),
|
||||
// so they pass it straight through. Re-resolving there would call `Get` a second
|
||||
// time and, for an unbound entry sharing its spell with another caster's entry,
|
||||
// return a miss — dropping a caster Enumerate already knew. Null for the
|
||||
// descriptor / group-array paths, which have no per-entry attribution in hand.
|
||||
static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
bool isHelpful, int applications, int unitLevel,
|
||||
bool isPlayer) {
|
||||
bool isPlayer, int slot,
|
||||
const Attribution *known = nullptr) {
|
||||
double duration = 0.0;
|
||||
double expirationTime = 0.0;
|
||||
uint64_t casterGuid = 0;
|
||||
@@ -542,36 +558,42 @@ static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
expirationTime = PlayerBuffExpirationSeconds(entry);
|
||||
}
|
||||
if (spellID != 0 && guid != 0) {
|
||||
uint64_t c = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(guid, spellID, &c, &expMs, &durMs)) {
|
||||
// A cached expiration that already elapsed while the aura is still
|
||||
// present (non-player casters get an underestimated base duration)
|
||||
// is not meaningful — report unknown (0) rather than a negative
|
||||
// remaining time.
|
||||
if (expirationTime == 0.0 && expMs != 0 && !ExpirationElapsed(expMs))
|
||||
expirationTime = static_cast<double>(expMs) * 0.001;
|
||||
if (durMs != 0)
|
||||
duration = static_cast<double>(durMs) * 0.001;
|
||||
}
|
||||
// Caster (sourceGUID/sourceUnit): observed caster, else the self-buff
|
||||
// inference — same resolution the PLAYER filter uses, so a self-buff
|
||||
// both shows a source and matches HELPFUL|PLAYER. Timing stays unknown
|
||||
// when we never saw the cast; only the caster is inferred, and it's
|
||||
// exact.
|
||||
casterGuid = EffectiveCaster(guid, spellID);
|
||||
// Caster (sourceGUID/sourceUnit) and timing resolved together, so a
|
||||
// self-buff both shows a source and matches HELPFUL|PLAYER instead of
|
||||
// the two paths disagreeing.
|
||||
Attribution a = (known != nullptr) ? *known : Attribute(guid, spellID, slot);
|
||||
// Self-buff inference for a pre-resolved entry whose caster the cache
|
||||
// never learned (idempotent on the Attribute() path, which already
|
||||
// applied it — the caster is nonzero there when it matched).
|
||||
if (a.caster == 0 && Spell::IsSelfBuff::IsSelfBuff(spellID))
|
||||
a.caster = guid;
|
||||
// A cached expiration that already elapsed while the aura is still
|
||||
// present (non-player casters get an underestimated base duration)
|
||||
// is not meaningful — report unknown (0) rather than a negative
|
||||
// remaining time.
|
||||
if (expirationTime == 0.0 && a.expirationMs != 0 &&
|
||||
!ExpirationElapsed(a.expirationMs))
|
||||
expirationTime = static_cast<double>(a.expirationMs) * 0.001;
|
||||
if (a.durationMs != 0)
|
||||
duration = static_cast<double>(a.durationMs) * 0.001;
|
||||
casterGuid = a.caster;
|
||||
}
|
||||
BuildTable(L, spellID, applications, isHelpful, duration, expirationTime,
|
||||
casterGuid);
|
||||
}
|
||||
|
||||
// Attribution held directly by an `Aura::Source` cache entry (from `Enumerate`),
|
||||
// for the cache-fallback push paths to hand to `PushEnriched` as `known`.
|
||||
static Attribution CachedAttribution(const Aura::Source::CachedAura &c) {
|
||||
return {c.casterGuid, c.expirationMs, c.durationMs};
|
||||
}
|
||||
|
||||
void Push(void *L, const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
PushEnriched(L, UnitGuid(unit), spellID, isHelpful, ReadStacks(unit, slot),
|
||||
unitLevel, unit != nullptr && unit == LocalPlayer());
|
||||
unitLevel, unit != nullptr && unit == LocalPlayer(), slot);
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -582,12 +604,15 @@ namespace {
|
||||
// applied (caster-modified) ms when known, else the Spell.dbc base.
|
||||
void PushFromCache(void *L, const uint8_t *unit,
|
||||
const Aura::Source::CachedAura &c, bool isHelpful) {
|
||||
// `PushEnriched` re-reads the same cache entry `c` came from (by
|
||||
// guid+spellID) to fill caster / expiration / applied duration, so the
|
||||
// result is identical to reading `c`'s fields directly. Stacks aren't in
|
||||
// SMSG_SPELL_GO, so `applications` is 1.
|
||||
// `c` already carries this exact instance's caster + timing (Enumerate
|
||||
// copied it out), so hand it to `PushEnriched` as `known` rather than
|
||||
// re-resolving by (guid, spell, slot) — an unbound entry sharing its spell
|
||||
// with another caster's would otherwise resolve to a miss and drop the
|
||||
// caster. Stacks aren't in SMSG_SPELL_GO, so `applications` is 1.
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
PushEnriched(L, UnitGuid(unit), c.spellId, isHelpful, 1, unitLevel, false);
|
||||
const Attribution a = CachedAttribution(c);
|
||||
PushEnriched(L, UnitGuid(unit), c.spellId, isHelpful, 1, unitLevel, false,
|
||||
c.slot, &a);
|
||||
}
|
||||
|
||||
// Reconciles the `Aura::Source` cache against `unit`'s descriptor: when the
|
||||
@@ -604,15 +629,9 @@ void ReconcileCache(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
uint32_t present[Offsets::UNIT_AURA_TOTAL];
|
||||
int n = 0;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
const uint32_t id = ReadSpellID(unit, slot);
|
||||
if (id != 0)
|
||||
present[n++] = id;
|
||||
}
|
||||
if (n == 0)
|
||||
return;
|
||||
Aura::Source::EvictAbsent(UnitGuid(unit), present, n);
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot)
|
||||
present[slot] = ReadSpellID(unit, slot);
|
||||
Aura::Source::EvictAbsent(UnitGuid(unit), present);
|
||||
}
|
||||
|
||||
// True if `unit`'s descriptor currently exposes any visible aura in either
|
||||
@@ -818,8 +837,9 @@ bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
if (++seen == oneBasedIndex) {
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
GroupMemberLevel(guid), false, buf[i].slot, &a);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -838,8 +858,9 @@ void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1, level,
|
||||
false);
|
||||
false, buf[i].slot, &a);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
}
|
||||
@@ -861,8 +882,9 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
}
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
continue;
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
PushEnriched(L, guid, buf[i].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
false);
|
||||
false, buf[i].slot, &a);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -887,7 +909,7 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
if (++matches == oneBasedIndex) {
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
filter == Filter::Helpful, 1, GroupMemberLevel(guid),
|
||||
false);
|
||||
false, Aura::Source::SLOT_UNBOUND);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -918,7 +940,7 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, spellID, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
GroupMemberLevel(guid), false, Aura::Source::SLOT_UNBOUND);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
@@ -959,7 +981,7 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, id, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
GroupMemberLevel(guid), false, Aura::Source::SLOT_UNBOUND);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
@@ -992,7 +1014,8 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
filter == Filter::Helpful, 1, level, false);
|
||||
filter == Filter::Helpful, 1, level, false,
|
||||
Aura::Source::SLOT_UNBOUND);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
|
||||
+293
-111
@@ -26,6 +26,7 @@
|
||||
#include "spell/CastEvents.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "totem/Tracker.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -54,11 +55,7 @@ bool SpellAppliesAura(const uint8_t *spellRecord) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
using Time::Clock::NowMs;
|
||||
|
||||
// Server-authoritative duration (ms). When the local player is the caster
|
||||
// we let the engine fold in the player's duration modifiers (skipMod = 0);
|
||||
@@ -125,6 +122,18 @@ bool WasRecentPlayerCast(uint32_t spellId) {
|
||||
// application hook fires for the same aura.
|
||||
enum Kind : int8_t { KIND_UNKNOWN = -1, KIND_HELPFUL = 0, KIND_HARMFUL = 1 };
|
||||
|
||||
// An entry is one AURA INSTANCE, identified the way the server identifies one:
|
||||
// `(target, spell, caster)`. Two same-class raiders' Corruption on one boss are
|
||||
// two auras occupying two descriptor slots, so a `(target, spell)` identity
|
||||
// collapses them into one entry — the later cast overwrites the earlier
|
||||
// caster's timing, and `OnAuraRemoved` for either copy evicts both.
|
||||
//
|
||||
// The descriptor only stores spell IDs, so recovering WHICH instance a slot
|
||||
// holds needs the slot bound to its entry. SpellGo knows the caster but no
|
||||
// slot; `OnAuraAdded` knows the slot but no caster. They arrive in that order
|
||||
// (SpellGo, then the SMSG_UPDATE_OBJECT that seats the aura), so the
|
||||
// application hook binds the cast capture it belongs to — the newest entry for
|
||||
// this `(target, spell)` still awaiting a slot.
|
||||
struct Entry {
|
||||
uint64_t targetGuid;
|
||||
uint64_t casterGuid;
|
||||
@@ -132,12 +141,13 @@ struct Entry {
|
||||
uint32_t expirationMs; // 0 = infinite / unknown duration
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
uint32_t stampMs; // last write time; EvictAbsent grace (see below)
|
||||
int16_t slot; // absolute descriptor slot, SLOT_UNBOUND until bound
|
||||
int8_t kind; // Kind; descriptor-slot-derived, KIND_UNKNOWN if only seen via SpellGo
|
||||
bool used;
|
||||
};
|
||||
|
||||
// Sized for the realistic worst case: a fully raid-buffed 40-man plus its
|
||||
// debuff load. We cache one entry per (target, spellId) for EVERY
|
||||
// debuff load. We cache one entry per (target, spellId, caster) for EVERY
|
||||
// aura-applying SMSG_SPELL_GO we observe — not just auras on the player, but
|
||||
// every buff cast on every unit in view — so the live working set in a raid
|
||||
// is ~40 members × ~30 persistent buffs ≈ 1200, plus debuffs. At the old 256
|
||||
@@ -145,10 +155,21 @@ struct Entry {
|
||||
// dropping their source (the "lost sourceGUID on a buff" report). 2048 gives
|
||||
// a fully-buffed 40-man comfortable headroom; the tick sweep still reclaims
|
||||
// expired/orphaned entries so it rarely approaches full outside a raid.
|
||||
// (~40 bytes/entry → ~80 KB static.)
|
||||
// (~40 bytes/entry → ~80 KB static.) Per-caster identity adds one entry per
|
||||
// extra caster of the same spell on one target, which is bounded by the 16
|
||||
// debuff slots that can hold them.
|
||||
constexpr int kCacheSize = 2048;
|
||||
Entry g_cache[kCacheSize];
|
||||
|
||||
// One past the highest slot ever claimed since the last flush — the active
|
||||
// prefix `[0, g_usedHigh)`. Every used entry lives below it (Claim is the sole
|
||||
// allocator and extends it; FlushAll resets it), so all lookups scan only this
|
||||
// prefix instead of the full 2048. Outside a raid the working set is a handful
|
||||
// of auras, so this is the difference between a ~30-entry scan and a 2048-entry
|
||||
// one on the per-aura query path. Never lowered on eviction — an over-estimate
|
||||
// only costs a few extra skipped (!used) slots, never a missed live entry.
|
||||
int g_usedHigh = 0;
|
||||
|
||||
// SMSG_SPELL_GO arrives before the SMSG_UPDATE_OBJECT that adds the aura to
|
||||
// the target's descriptor, so for a brief window a just-captured entry names
|
||||
// an aura the descriptor doesn't list yet. `EvictAbsent` (run from a query's
|
||||
@@ -190,48 +211,106 @@ bool DescriptorListsAura(uint64_t guid, uint32_t spellId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// `fromCast` true: the SpellGo hook — authoritative caster + caster-modified
|
||||
// (talented) timing. False: the OnAuraAdded application hook — timing only,
|
||||
// no caster, and it must not clobber an entry SpellGo already owns (that
|
||||
// would replace talented timing with the unmodified base), so it skips
|
||||
// entries that already carry a caster.
|
||||
void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
uint32_t expirationMs, uint32_t durationMs, bool fromCast,
|
||||
int8_t kind) {
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
// ---- Entry lookup --------------------------------------------------------
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
// The exact aura instance `caster` has on `targetGuid` — the server's own
|
||||
// identity for one aura, and what a repeat cast refreshes.
|
||||
Entry *FindByCaster(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid) {
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId &&
|
||||
e.casterGuid == casterGuid)
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Update an existing entry for this exact aura instance.
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
|
||||
e.stampMs = now; // refresh EvictAbsent grace on any touch
|
||||
// Learn the classification whenever a slot-derived kind arrives —
|
||||
// independent of caster/timing ownership, and never downgrade a
|
||||
// known kind back to unknown.
|
||||
if (kind != KIND_UNKNOWN)
|
||||
e.kind = kind;
|
||||
if (!fromCast && e.casterGuid != 0)
|
||||
return; // SpellGo owns this entry; keep its caster + timing
|
||||
if (casterGuid != 0)
|
||||
e.casterGuid = casterGuid;
|
||||
e.expirationMs = expirationMs;
|
||||
e.durationMs = durationMs;
|
||||
return;
|
||||
// The instance seated in descriptor slot `slot`.
|
||||
Entry *FindBySlot(uint64_t targetGuid, uint32_t spellId, int slot) {
|
||||
if (slot < 0)
|
||||
return nullptr;
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId &&
|
||||
e.slot == slot)
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The newest cast capture for this aura still awaiting a descriptor slot —
|
||||
// what an `OnAuraAdded` for `(target, spell)` is seating. Newest wins because
|
||||
// SpellGo precedes the application by a packet, so the freshest unbound entry
|
||||
// is the cast that just landed; an older one is a capture that never seated
|
||||
// (fully resisted, immune) and is left to expire on its own. "Newest" is the
|
||||
// smallest `Time::Clock::Elapsed(stamp, now)` — a wrap-safe signed difference;
|
||||
// a raw `stampMs >` compare inverts across the 2^32 ms tick wrap.
|
||||
Entry *FindFreshestUnbound(uint64_t targetGuid, uint32_t spellId, uint32_t now) {
|
||||
Entry *best = nullptr;
|
||||
uint32_t bestElapsed = 0;
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != targetGuid || e.spellId != spellId ||
|
||||
e.slot != SLOT_UNBOUND)
|
||||
continue;
|
||||
const uint32_t elapsed = Time::Clock::Elapsed(e.stampMs, now);
|
||||
if (best == nullptr || elapsed < bestElapsed) {
|
||||
best = &e;
|
||||
bestElapsed = elapsed;
|
||||
}
|
||||
}
|
||||
// Take a free slot, else an expired one whose aura the descriptor no
|
||||
// longer lists (a still-present aura keeps its slot so its caster isn't
|
||||
// lost — see DescriptorListsAura), else evict round-robin.
|
||||
return best;
|
||||
}
|
||||
|
||||
// The one entry for `(target, spell)`, or null when there are none or several.
|
||||
// Several means two casters and no way to tell their instances apart from a
|
||||
// spell ID alone, which is exactly when guessing is what produces a wrong
|
||||
// caster and a wrong timer.
|
||||
Entry *FindSole(uint64_t targetGuid, uint32_t spellId) {
|
||||
Entry *found = nullptr;
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != targetGuid || e.spellId != spellId)
|
||||
continue;
|
||||
if (found != nullptr)
|
||||
return nullptr;
|
||||
found = &e;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// The instance a slot-bearing query means: the entry seated in `slot`, else the
|
||||
// unit's sole entry for the spell — every single-caster case and the slotless
|
||||
// (out-of-range group array, cache fallback) paths. Two casters' entries with
|
||||
// no slot binding resolve to null (see FindSole), never to a coin flip. This is
|
||||
// the read-side identity rule; both `Get` and `Evict` resolve through it so they
|
||||
// can't drift into naming different instances for the same query.
|
||||
Entry *FindInstance(uint64_t targetGuid, uint32_t spellId, int slot) {
|
||||
Entry *e = FindBySlot(targetGuid, spellId, slot);
|
||||
return e != nullptr ? e : FindSole(targetGuid, spellId);
|
||||
}
|
||||
|
||||
// Marks the slot `e` occupies as within the active prefix, so later lookups
|
||||
// reach it. Every new entry passes through here (Claim is the only allocator),
|
||||
// which is what keeps the `[0, g_usedHigh)` invariant true for all writers.
|
||||
Entry *Register(Entry *e) {
|
||||
const int idx = static_cast<int>(e - g_cache);
|
||||
if (idx >= g_usedHigh)
|
||||
g_usedHigh = idx + 1;
|
||||
return e;
|
||||
}
|
||||
|
||||
// Takes a free slot, else an expired one whose aura the descriptor no longer
|
||||
// lists (a still-present aura keeps its slot so its caster isn't lost — see
|
||||
// DescriptorListsAura), else evicts. Scans the full array — as the allocator it
|
||||
// must be able to hand out slots beyond the current active prefix (that is how
|
||||
// the prefix grows), so it can't bound itself by g_usedHigh.
|
||||
Entry *Claim(uint32_t now) {
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used ||
|
||||
(e.expirationMs != 0 && now >= e.expirationMs &&
|
||||
!DescriptorListsAura(e.targetGuid, e.spellId))) {
|
||||
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
|
||||
now, kind, true};
|
||||
return;
|
||||
}
|
||||
!DescriptorListsAura(e.targetGuid, e.spellId)))
|
||||
return Register(&e);
|
||||
}
|
||||
// Saturated. Honor the same invariant as the tick sweep: an entry whose
|
||||
// aura is still present on a resolvable unit is NEVER evicted — its caster
|
||||
@@ -256,9 +335,91 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
if (orphan == nullptr || e.stampMs < orphan->stampMs)
|
||||
orphan = &e;
|
||||
}
|
||||
Entry *victim = (orphan != nullptr) ? orphan : lru;
|
||||
*victim = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
kind, true};
|
||||
return Register((orphan != nullptr) ? orphan : lru);
|
||||
}
|
||||
|
||||
// ---- Writes --------------------------------------------------------------
|
||||
|
||||
// The SpellGo hook: authoritative caster + caster-modified (talented) timing.
|
||||
// Identity is `(target, spell, caster)`, so a second caster of the same spell
|
||||
// opens its own entry instead of overwriting the first's, and a recast by the
|
||||
// same caster refreshes theirs — keeping the descriptor slot already bound to
|
||||
// it.
|
||||
void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
uint32_t expirationMs, uint32_t durationMs) {
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
const uint32_t now = NowMs();
|
||||
Entry *e = FindByCaster(targetGuid, spellId, casterGuid);
|
||||
// A lone unattributed entry for this aura is the same instance seen without
|
||||
// a caster (seated by an application hook while we were out of view, or a
|
||||
// group-array guess), so claim it rather than opening a second entry for
|
||||
// one aura — the same adoption `ApplyDurationModifiers` does. Only when
|
||||
// it's the unit's only entry for the spell: with several, "the one this
|
||||
// cast refreshes" is a guess, and guessing wrong writes this caster's timer
|
||||
// onto another's aura.
|
||||
if (e == nullptr) {
|
||||
Entry *sole = FindSole(targetGuid, spellId);
|
||||
if (sole != nullptr && sole->casterGuid == 0)
|
||||
e = sole;
|
||||
}
|
||||
if (e == nullptr) {
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
|
||||
now, SLOT_UNBOUND, KIND_UNKNOWN, true};
|
||||
return;
|
||||
}
|
||||
e->stampMs = now; // refresh EvictAbsent grace on any touch
|
||||
e->casterGuid = casterGuid;
|
||||
e->expirationMs = expirationMs;
|
||||
e->durationMs = durationMs;
|
||||
}
|
||||
|
||||
// The OnAuraAdded / OnAuraStacksChanged application hooks: a descriptor slot,
|
||||
// a classification, and base timing — but no caster (except the local player's
|
||||
// own recent cast, which `StampApplication` resolves). Seats the slot on the
|
||||
// cast capture it belongs to, and must not clobber the caster + talented
|
||||
// timing SpellGo already owns for that instance.
|
||||
//
|
||||
// `slot` is SLOT_UNBOUND for the out-of-range group-array path, which has no
|
||||
// descriptor at all; there the only available identity is `(target, spell)`.
|
||||
void StoreFromApplication(uint64_t targetGuid, uint32_t spellId,
|
||||
uint64_t casterGuid, uint32_t expirationMs,
|
||||
uint32_t durationMs, int slot, int8_t kind) {
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
const uint32_t now = NowMs();
|
||||
// Anything outside the descriptor's slot range can't index it, so it binds
|
||||
// nothing — `EvictAbsent` indexes the array by a bound entry's slot.
|
||||
if (slot < 0 || slot >= Offsets::UNIT_AURA_TOTAL)
|
||||
slot = SLOT_UNBOUND;
|
||||
|
||||
Entry *e = FindBySlot(targetGuid, spellId, slot);
|
||||
if (e == nullptr)
|
||||
e = FindFreshestUnbound(targetGuid, spellId, now);
|
||||
if (e == nullptr && slot < 0)
|
||||
e = FindSole(targetGuid, spellId);
|
||||
if (e == nullptr) {
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
static_cast<int16_t>(slot), kind, true};
|
||||
return;
|
||||
}
|
||||
|
||||
e->stampMs = now;
|
||||
if (slot >= 0)
|
||||
e->slot = static_cast<int16_t>(slot);
|
||||
// Learn the classification whenever a slot-derived kind arrives —
|
||||
// independent of caster/timing ownership, and never downgrade a known kind
|
||||
// back to unknown.
|
||||
if (kind != KIND_UNKNOWN)
|
||||
e->kind = kind;
|
||||
if (e->casterGuid != 0)
|
||||
return; // SpellGo owns this entry; keep its caster + talented timing
|
||||
if (casterGuid != 0)
|
||||
e->casterGuid = casterGuid;
|
||||
e->expirationMs = expirationMs;
|
||||
e->durationMs = durationMs;
|
||||
}
|
||||
|
||||
// ---- Out-of-range group-member aura snapshots ---------------------------
|
||||
@@ -296,25 +457,27 @@ void StampGroupGuess(uint64_t guid, uint16_t spellId, int8_t kind, uint32_t now)
|
||||
const uint32_t base = SpellDurationMs(rec, /*casterIsPlayer*/ false);
|
||||
if (base == 0)
|
||||
return;
|
||||
Store(guid, spellId, /*casterGuid*/ 0, now + base, base, /*fromCast*/ false,
|
||||
kind);
|
||||
StoreFromApplication(guid, spellId, /*casterGuid*/ 0, now + base, base,
|
||||
SLOT_UNBOUND, kind);
|
||||
}
|
||||
|
||||
// Evict the entry for an aura the engine reports gone. Keyed by (target,
|
||||
// spell) like the rest of the cache. Without this, the GetAuraDataByIndex
|
||||
// fallback would keep surfacing a dropped aura until its computed expiry —
|
||||
// e.g. a Rank 1 buff replaced by Rank 2 (engine drops Rank 1 from the
|
||||
// descriptor) would show as a phantom second aura, or a dispelled buff would
|
||||
// linger.
|
||||
void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
// Evict the entry for an aura the engine reports gone, identified by the
|
||||
// descriptor slot it vacated. Without this, the GetAuraDataByIndex fallback
|
||||
// would keep surfacing a dropped aura until its computed expiry — e.g. a Rank
|
||||
// 1 buff replaced by Rank 2 (engine drops Rank 1 from the descriptor) would
|
||||
// show as a phantom second aura, or a dispelled buff would linger.
|
||||
//
|
||||
// The slot is what makes this safe when two casters hold the same spell on one
|
||||
// target: only the instance that actually fell off goes. Falling back to a
|
||||
// spell-ID match when nothing is bound to the slot keeps the pre-binding
|
||||
// behaviour, but only while the match is unambiguous — dropping one of two
|
||||
// casters' entries at random would take a live aura's caster with it.
|
||||
void Evict(uint64_t targetGuid, uint32_t spellId, int slot) {
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
|
||||
e.used = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Entry *e = FindInstance(targetGuid, spellId, slot);
|
||||
if (e != nullptr)
|
||||
e->used = false;
|
||||
}
|
||||
|
||||
// ---- Server-side duration modifiers (trigger-driven inference) -----------
|
||||
@@ -472,7 +635,8 @@ void ApplyDurationModifiers(uint32_t triggerSpellId, uint64_t caster,
|
||||
if (!TriggerMatches(m, triggerSpellId, triggerRec))
|
||||
continue;
|
||||
for (int t = 0; t < numTargets; ++t) {
|
||||
for (auto &e : g_cache) {
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != targets[t])
|
||||
continue;
|
||||
// The mechanic acts on the trigger-caster's own aura. Cast-applied
|
||||
@@ -580,8 +744,9 @@ const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
|
||||
// Wipe the whole cache. Used on a map transition (see OnWorldTick).
|
||||
void FlushAll() {
|
||||
for (auto &e : g_cache)
|
||||
e.used = false;
|
||||
for (int i = 0; i < g_usedHigh; ++i)
|
||||
g_cache[i].used = false;
|
||||
g_usedHigh = 0; // active prefix is empty again
|
||||
// Reset transition baselines too: post-transition group auras re-sync and
|
||||
// should be treated as first-sight (unknown age), not diffed as new.
|
||||
for (auto &s : g_groupSnaps)
|
||||
@@ -617,8 +782,9 @@ void OnWorldTick() {
|
||||
}
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.expirationMs == 0 || now < e.expirationMs)
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.expirationMs == 0 || !Time::Clock::Reached(now, e.expirationMs))
|
||||
continue;
|
||||
// A timer elapse doesn't prove the aura is gone — expirationMs is only
|
||||
// a base-duration estimate for non-player casters. Keep any entry whose
|
||||
@@ -705,13 +871,11 @@ void HandleSpellGo(uint64_t caster, uint32_t spellId, const uint64_t *targets,
|
||||
|
||||
if (numTargets == 0) {
|
||||
// No explicit hit list (self-cast with caster-implicit target).
|
||||
Store(caster, spellId, caster, expirationMs, durationMs, true,
|
||||
KIND_UNKNOWN);
|
||||
StoreFromCast(caster, spellId, caster, expirationMs, durationMs);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
Store(targets[i], spellId, caster, expirationMs, durationMs, true,
|
||||
KIND_UNKNOWN);
|
||||
StoreFromCast(targets[i], spellId, caster, expirationMs, durationMs);
|
||||
}
|
||||
|
||||
// SMSG_SPELL_GO parse (funnel subscriber). At the leaf handler the engine has
|
||||
@@ -744,12 +908,19 @@ const Net::PacketDispatch::AutoSubscribe _spellGoSub{&SpellGoSub};
|
||||
|
||||
// ---- Aura-application co-hooks (timing for proc / triggered auras) -------
|
||||
|
||||
// Stamp expiration for an aura that just landed/refreshed on `unit`. Used by
|
||||
// both the add and stack-change hooks. No caster is available from these
|
||||
// paths, so it stamps timing only with `fromCast=false` — Store skips any
|
||||
// entry SpellGo already owns, so a directly-cast aura keeps its talented
|
||||
// timing. Base (unmodified) duration is the best estimate without a caster.
|
||||
void StampApplication(void *unit, uint32_t spellId, int8_t kind) {
|
||||
// Classify by the absolute aura slot: 0..BUFF_COUNT-1 = buff (helpful),
|
||||
// BUFF_COUNT..TOTAL-1 = debuff (harmful).
|
||||
int8_t KindForSlot(int slot) {
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
}
|
||||
|
||||
// Stamp expiration for an aura that just landed/refreshed in `slot` on `unit`.
|
||||
// Used by both the add and stack-change hooks. No caster is available from
|
||||
// these paths, so it stamps timing only — StoreFromApplication keeps whatever
|
||||
// SpellGo already owns, so a directly-cast aura keeps its talented timing.
|
||||
// Base (unmodified) duration is the best estimate without a caster. The slot
|
||||
// is what binds this application to the cast capture behind it.
|
||||
void StampApplication(void *unit, uint32_t spellId, int slot) {
|
||||
if (spellId == 0)
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
@@ -766,14 +937,8 @@ void StampApplication(void *unit, uint32_t spellId, int8_t kind) {
|
||||
const uint32_t durationMs = SpellDurationMs(rec, byPlayer);
|
||||
const uint64_t caster = byPlayer ? Unit::Identity::PlayerGuid() : 0;
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
Store(unitGuid, spellId, caster, expirationMs, durationMs,
|
||||
/*fromCast*/ false, kind);
|
||||
}
|
||||
|
||||
// Classify by the absolute aura slot: 0..BUFF_COUNT-1 = buff (helpful),
|
||||
// BUFF_COUNT..TOTAL-1 = debuff (harmful).
|
||||
int8_t KindForSlot(int slot) {
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
StoreFromApplication(unitGuid, spellId, caster, expirationMs, durationMs,
|
||||
slot, KindForSlot(slot));
|
||||
}
|
||||
|
||||
// Bump the player-stat-inputs signal when an aura change hits the LOCAL
|
||||
@@ -793,7 +958,7 @@ OnAuraAdded_t g_origOnAuraAdded = nullptr;
|
||||
void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId) {
|
||||
g_origOnAuraAdded(unit, edx, slot, spellId);
|
||||
StampApplication(unit, spellId, KindForSlot(static_cast<int>(slot)));
|
||||
StampApplication(unit, spellId, static_cast<int>(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
@@ -813,8 +978,7 @@ void __fastcall OnAuraStacksChanged_h(void *unit, void *edx, int slot,
|
||||
g_origOnAuraStacksChanged(unit, edx, slot, stackCount);
|
||||
StampApplication(
|
||||
unit,
|
||||
Aura::Data::ReadSpellID(static_cast<const uint8_t *>(unit), slot),
|
||||
KindForSlot(slot));
|
||||
Aura::Data::ReadSpellID(static_cast<const uint8_t *>(unit), slot), slot);
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
@@ -843,8 +1007,7 @@ OnAuraRemoved_t g_origOnAuraRemoved = nullptr;
|
||||
void __fastcall OnAuraRemoved_h(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId) {
|
||||
g_origOnAuraRemoved(unit, edx, slot, spellId);
|
||||
(void)slot;
|
||||
Evict(Unit::Identity::GuidForObject(unit), spellId);
|
||||
Evict(Unit::Identity::GuidForObject(unit), spellId, static_cast<int>(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
@@ -854,19 +1017,21 @@ const Game::HookAutoRegister _hookAuraRemoved{
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs) {
|
||||
if (unitGuid == 0 || spellId == 0)
|
||||
return false;
|
||||
for (const auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == unitGuid && e.spellId == spellId) {
|
||||
*outCaster = e.casterGuid;
|
||||
*outExpirationMs = e.expirationMs;
|
||||
*outDurationMs = e.durationMs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
// Slot first, then the sole entry (see FindInstance): several entries and no
|
||||
// slot binding is a genuine ambiguity, so reporting the miss leaves the
|
||||
// caller its unknown-caster defaults instead of one caster's timer on the
|
||||
// other's aura.
|
||||
const Entry *e = FindInstance(unitGuid, spellId, slot);
|
||||
if (e == nullptr)
|
||||
return false;
|
||||
*outCaster = e->casterGuid;
|
||||
*outExpirationMs = e->expirationMs;
|
||||
*outDurationMs = e->durationMs;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AddDurationMod(uint32_t triggerSpellId, uint32_t affectedFamily,
|
||||
@@ -884,7 +1049,8 @@ uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
if (unitGuid == 0 || mask == 0 || casterGuid == 0)
|
||||
return 0;
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
// Caller's own aura only, scoped as ApplyDurationModifiers scopes a
|
||||
@@ -911,11 +1077,20 @@ uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
return 0;
|
||||
}
|
||||
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count) {
|
||||
if (unitGuid == 0 || presentSpellIds == nullptr || count <= 0)
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *slotSpellIds) {
|
||||
if (unitGuid == 0 || slotSpellIds == nullptr)
|
||||
return;
|
||||
bool anyPresent = false;
|
||||
for (int s = 0; s < Offsets::UNIT_AURA_TOTAL; ++s)
|
||||
if (slotSpellIds[s] != 0) {
|
||||
anyPresent = true;
|
||||
break;
|
||||
}
|
||||
if (!anyPresent)
|
||||
return; // out of range vs genuinely buffless — see the header
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
// Fresh SpellGo capture the descriptor hasn't synced yet — see
|
||||
@@ -923,11 +1098,18 @@ void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count)
|
||||
if (now - e.stampMs < kEvictGraceMs)
|
||||
continue;
|
||||
bool present = false;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (presentSpellIds[i] == e.spellId) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
if (e.slot != SLOT_UNBOUND) {
|
||||
// A bound entry is only backed by the slot it was seated in, so a
|
||||
// slot now holding a different spell (or nothing) leaves it stale
|
||||
// even when another caster's copy keeps the spell ID on the unit.
|
||||
present = slotSpellIds[e.slot] == e.spellId;
|
||||
} else {
|
||||
for (int s = 0; s < Offsets::UNIT_AURA_TOTAL; ++s)
|
||||
if (slotSpellIds[s] == e.spellId) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!present)
|
||||
e.used = false;
|
||||
}
|
||||
@@ -939,14 +1121,14 @@ int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
|
||||
const int8_t want = harmful ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
const uint32_t now = NowMs();
|
||||
int n = 0;
|
||||
for (const auto &e : g_cache) {
|
||||
if (n >= maxOut)
|
||||
break;
|
||||
for (int i = 0; i < g_usedHigh && n < maxOut; ++i) {
|
||||
const Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid || e.kind != want)
|
||||
continue;
|
||||
if (e.expirationMs != 0 && now >= e.expirationMs)
|
||||
continue; // expired (infinite-duration entries pass)
|
||||
out[n++] = {e.spellId, e.casterGuid, e.expirationMs, e.durationMs};
|
||||
out[n++] = {e.spellId, e.casterGuid, e.expirationMs, e.durationMs,
|
||||
e.slot};
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
+47
-21
@@ -19,7 +19,15 @@
|
||||
// the client ever sees the caster + a server-authoritative duration is the
|
||||
// `SMSG_SPELL_GO` packet at cast time. We parse it (the same packet
|
||||
// nampower parses for its `AURA_CAST_ON_*` events) and remember, per
|
||||
// `(targetGuid, spellId)`, who cast it and when it should expire.
|
||||
// `(targetGuid, spellId, casterGuid)`, who cast it and when it should expire.
|
||||
//
|
||||
// The caster belongs in that key because it belongs in the server's: two
|
||||
// same-class raiders' Corruption on one boss are two auras in two descriptor
|
||||
// slots, and a `(target, spell)` cache holds one entry for both — the second
|
||||
// cast overwrites the first caster's timing, and either copy falling off
|
||||
// evicts the survivor's entry too. The descriptor stores only spell IDs, so
|
||||
// each entry additionally records the slot its aura was seated in
|
||||
// (`OnAuraAdded`), which is what lets a per-slot query name the right instance.
|
||||
//
|
||||
// `Aura::Data::Push` consults this to fill `sourceUnit` (caster, resolved
|
||||
// to a unit token) and `expirationTime` for non-player units. The cache is
|
||||
@@ -31,15 +39,25 @@
|
||||
|
||||
namespace Aura::Source {
|
||||
|
||||
// Looks up the cached caster + timing for the aura `spellId` currently on
|
||||
// the unit identified by `unitGuid`. Returns true and fills the out params
|
||||
// on a hit. `*outExpirationMs` is an absolute `GetTickCount`-epoch timestamp
|
||||
// (0 = unknown / infinite-duration aura); `*outDurationMs` is the applied
|
||||
// duration including the caster's modifiers (talents etc.; 0 = none) — use
|
||||
// it for the `duration` field so it stays consistent with `expirationTime`;
|
||||
// `*outCaster` is the caster's 64-bit GUID (never 0 on a hit). Returns false
|
||||
// on a miss or for zero inputs.
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
// `slot` value for a query with no descriptor slot to go on (out-of-range
|
||||
// group array, cache fallback) and for an entry not yet seated in one.
|
||||
constexpr int SLOT_UNBOUND = -1;
|
||||
|
||||
// Looks up the cached caster + timing for the aura `spellId` occupying
|
||||
// absolute descriptor `slot` on the unit identified by `unitGuid`. Returns
|
||||
// true and fills the out params on a hit. `*outExpirationMs` is an absolute
|
||||
// `GetTickCount`-epoch timestamp (0 = unknown / infinite-duration aura);
|
||||
// `*outDurationMs` is the applied duration including the caster's modifiers
|
||||
// (talents etc.; 0 = none) — use it for the `duration` field so it stays
|
||||
// consistent with `expirationTime`; `*outCaster` is the caster's 64-bit GUID
|
||||
// (0 when the aura was seen applied but its cast never observed). Returns
|
||||
// false on a miss or for zero inputs.
|
||||
//
|
||||
// Pass `SLOT_UNBOUND` when the caller has no slot. Resolution is by slot
|
||||
// first, then by spell ID when the unit carries only one entry for it; two
|
||||
// casters' entries with no slot binding resolve to a miss rather than to a
|
||||
// coin flip between them.
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// Refreshes `casterGuid`'s aura on `unitGuid` matching the same selector the
|
||||
@@ -76,19 +94,27 @@ struct CachedAura {
|
||||
uint64_t casterGuid; // 0 = caster unknown (application hook, no SpellGo)
|
||||
uint32_t expirationMs; // 0 = infinite / unknown
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
int slot; // descriptor slot the aura was seated in; feed it
|
||||
// back to `Get` to reach this exact entry again
|
||||
};
|
||||
|
||||
// Evicts every cached entry for `unitGuid` whose spellId is NOT present in
|
||||
// `presentSpellIds[0..count)`. Used to reconcile the cache against a unit's
|
||||
// authoritative descriptor when it is back in view (a populated aura array
|
||||
// means the unit is fully synced): an entry the descriptor no longer lists
|
||||
// was removed while we couldn't observe it — e.g. a buff the owner cancelled
|
||||
// while out of our range, whose `OnAuraRemoved` we never received — so drop
|
||||
// it before the descriptor-drop fallback resurfaces it as a phantom. The
|
||||
// caller must only invoke this when the descriptor is populated (count > 0);
|
||||
// an empty array can't distinguish "out of range" from "genuinely buffless",
|
||||
// so reconciling then would wrongly wipe still-valid out-of-range entries.
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count);
|
||||
// Evicts every cached entry for `unitGuid` the unit's descriptor contradicts.
|
||||
// `slotSpellIds` is the raw `UNIT_FIELD_AURA` array — exactly
|
||||
// `Offsets::UNIT_AURA_TOTAL` spell IDs indexed by absolute slot, 0 for empty.
|
||||
// Used to reconcile the cache against a unit's authoritative descriptor when
|
||||
// it is back in view (a populated aura array means the unit is fully synced):
|
||||
// an entry the descriptor no longer backs was removed while we couldn't
|
||||
// observe it — e.g. a buff the owner cancelled while out of our range, whose
|
||||
// `OnAuraRemoved` we never received — so drop it before the descriptor-drop
|
||||
// fallback resurfaces it as a phantom.
|
||||
//
|
||||
// An entry seated in a slot is checked against THAT slot, so one caster's
|
||||
// copy going away retires its own entry even while another caster keeps the
|
||||
// spell ID present on the unit. Entries never seated are checked against the
|
||||
// whole array. An all-empty array is ignored: it can't distinguish "out of
|
||||
// range" from "genuinely buffless", and reconciling then would wrongly wipe
|
||||
// still-valid out-of-range entries.
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *slotSpellIds);
|
||||
|
||||
// Fills `out` with up to `maxOut` cached, non-expired auras on `unitGuid`
|
||||
// whose helpful/harmful classification matches `harmful`. Returns the count
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
// 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/>.
|
||||
|
||||
// Backports the direct-action and override binding APIs introduced in 2.0.
|
||||
// Normal bindings are stored by vanilla's SetBinding implementation using the
|
||||
// later-client SPELL/ITEM/MACRO/CLICK command strings. The resolved-command
|
||||
// hook executes those four command families; every native binding command is
|
||||
// passed through unchanged.
|
||||
//
|
||||
// Override bindings live in a separate owner-scoped layer in front of the
|
||||
// native key lookup. They are deliberately never written to the saved binding
|
||||
// table, so clearing an owner immediately reveals the next override or the
|
||||
// unchanged native binding underneath it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "macro/Execute.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Bindings::Api {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char kSpellPrefix[] = "SPELL ";
|
||||
constexpr const char kItemPrefix[] = "ITEM ";
|
||||
constexpr const char kMacroPrefix[] = "MACRO ";
|
||||
constexpr const char kClickPrefix[] = "CLICK ";
|
||||
constexpr const char kDefaultMouseButton[] = "LeftButton";
|
||||
|
||||
using ScriptSetBinding_t = int(__fastcall *)(void *L);
|
||||
using KeyDispatch_t = int(__thiscall *)(void *mgr, const char *key, int isDown);
|
||||
using CommandExecute_t = int(__thiscall *)(void *mgr, const char *cmd, int isDown);
|
||||
|
||||
KeyDispatch_t KeyDispatch_o = nullptr;
|
||||
CommandExecute_t CommandExecute_o = nullptr;
|
||||
|
||||
enum class CommandType {
|
||||
None,
|
||||
Spell,
|
||||
Item,
|
||||
Macro,
|
||||
Click,
|
||||
};
|
||||
|
||||
struct ParsedCommand {
|
||||
CommandType type = CommandType::None;
|
||||
const char *argument = nullptr;
|
||||
};
|
||||
|
||||
struct OverrideEntry {
|
||||
const void *owner = nullptr;
|
||||
std::string key;
|
||||
std::string command;
|
||||
bool priority = false;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
std::vector<OverrideEntry> g_overrides;
|
||||
std::unordered_map<std::string, std::string> g_activeOverrides;
|
||||
uint64_t g_nextSequence = 0;
|
||||
|
||||
// Normalizes a binding key to the engine's own canonical form so a stored
|
||||
// override matches the string FUN_BINDING_KEY_DISPATCH builds at keypress. The
|
||||
// engine's key-string builder (FUN_004b6630) always emits modifiers in the
|
||||
// fixed order ALT-CTRL-SHIFT- ahead of the base key, regardless of the order
|
||||
// the caller passed them; vanilla SetBinding never reorders, so the override
|
||||
// layer does it here. Both the store and the lookup run through this, so they
|
||||
// stay consistent while also matching the engine's keypress form.
|
||||
std::string CanonicalKey(const char *key) {
|
||||
std::string result = key ? key : "";
|
||||
for (char &character : result) {
|
||||
if (character >= 'a' && character <= 'z')
|
||||
character = static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
|
||||
// Strip the three modifier prefixes in whatever order/repetition they
|
||||
// appear, then re-emit them in the engine's fixed order. Prefix-stripping
|
||||
// (rather than splitting on '-') keeps the minus base key correct, e.g.
|
||||
// "SHIFT--" -> modifier SHIFT + base "-".
|
||||
bool alt = false, ctrl = false, shift = false;
|
||||
size_t base = 0;
|
||||
for (bool matched = true; matched;) {
|
||||
matched = false;
|
||||
if (result.compare(base, 4, "ALT-") == 0) {
|
||||
alt = true;
|
||||
base += 4;
|
||||
matched = true;
|
||||
} else if (result.compare(base, 5, "CTRL-") == 0) {
|
||||
ctrl = true;
|
||||
base += 5;
|
||||
matched = true;
|
||||
} else if (result.compare(base, 6, "SHIFT-") == 0) {
|
||||
shift = true;
|
||||
base += 6;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alt && !ctrl && !shift)
|
||||
return result;
|
||||
|
||||
std::string canonical;
|
||||
if (alt)
|
||||
canonical += "ALT-";
|
||||
if (ctrl)
|
||||
canonical += "CTRL-";
|
||||
if (shift)
|
||||
canonical += "SHIFT-";
|
||||
canonical += result.substr(base);
|
||||
|
||||
return canonical;
|
||||
}
|
||||
|
||||
std::string MakeCommand(const char *prefix, const char *arg) {
|
||||
std::string cmd = prefix;
|
||||
cmd += arg;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
std::string MakeClickCommand(const char *btnName, const char *mouseBtn) {
|
||||
std::string cmd = MakeCommand(kClickPrefix, btnName);
|
||||
if (mouseBtn != nullptr) {
|
||||
cmd += ':';
|
||||
cmd += mouseBtn;
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ParsedCommand ParseCommand(const char *cmd) {
|
||||
if (cmd == nullptr)
|
||||
return {};
|
||||
|
||||
struct PrefixEntry {
|
||||
const char *prefix;
|
||||
size_t length;
|
||||
CommandType type;
|
||||
};
|
||||
|
||||
static constexpr PrefixEntry prefixes[] = {
|
||||
{kSpellPrefix, sizeof(kSpellPrefix) - 1, CommandType::Spell},
|
||||
{kItemPrefix, sizeof(kItemPrefix) - 1, CommandType::Item},
|
||||
{kMacroPrefix, sizeof(kMacroPrefix) - 1, CommandType::Macro},
|
||||
{kClickPrefix, sizeof(kClickPrefix) - 1, CommandType::Click},
|
||||
};
|
||||
|
||||
for (const PrefixEntry &entry : prefixes) {
|
||||
if (std::strncmp(cmd, entry.prefix, entry.length) == 0)
|
||||
return {entry.type, cmd + entry.length};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool CallGlobalString(void *L, const char *func, const char *arg) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (!Game::Lua::PushGlobalFunction(L, func)) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, arg);
|
||||
const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0;
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool CallTableString(void *L, const char *table, const char *func, const char *arg) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, table);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int tableIndex = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, func);
|
||||
Game::Lua::GetTable(L, tableIndex);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, arg);
|
||||
const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0;
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
void InvokeButtonClick(void *L, const char *btnName, size_t btnNameLength, const char *mouseBtn) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushLString(L, btnName, static_cast<unsigned int>(btnNameLength));
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return;
|
||||
}
|
||||
|
||||
const int frameIndex = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, "Click");
|
||||
Game::Lua::GetTable(L, frameIndex);
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::PushValue(L, frameIndex);
|
||||
Game::Lua::PushString(L, mouseBtn);
|
||||
Game::Lua::PCall(L, 2, 0, 0);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
void ExecuteClick(void *L, const char *arg) {
|
||||
const char *separator = std::strrchr(arg, ':');
|
||||
if (separator == arg || (separator != nullptr && separator[1] == '\0'))
|
||||
return;
|
||||
|
||||
const char *mouseBtn = kDefaultMouseButton;
|
||||
size_t btnNameLength = std::strlen(arg);
|
||||
if (separator != nullptr) {
|
||||
btnNameLength = static_cast<size_t>(separator - arg);
|
||||
mouseBtn = separator + 1;
|
||||
}
|
||||
|
||||
if (btnNameLength == 0)
|
||||
return;
|
||||
|
||||
InvokeButtonClick(L, arg, btnNameLength, mouseBtn);
|
||||
}
|
||||
|
||||
// Returns true for every recognized direct-action command, including malformed
|
||||
// or stale commands. Vanilla cannot execute these namespaces, so a bad command
|
||||
// is consumed as a no-op instead of falling through to an unrelated handler.
|
||||
bool ExecuteSpecialCommand(const char *cmd, int isDown) {
|
||||
const ParsedCommand parsed = ParseCommand(cmd);
|
||||
if (parsed.type == CommandType::None)
|
||||
return false;
|
||||
|
||||
if (!isDown || parsed.argument == nullptr || *parsed.argument == '\0')
|
||||
return true;
|
||||
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return true;
|
||||
|
||||
switch (parsed.type) {
|
||||
case CommandType::Spell:
|
||||
CallGlobalString(L, "CastSpellByName", parsed.argument);
|
||||
break;
|
||||
case CommandType::Item:
|
||||
CallTableString(L, "C_Item", "UseItemByName", parsed.argument);
|
||||
break;
|
||||
case CommandType::Macro:
|
||||
Macro::Execute::Saved(L, parsed.argument);
|
||||
break;
|
||||
case CommandType::Click:
|
||||
ExecuteClick(L, parsed.argument);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const OverrideEntry *FindEffectiveOverride(const std::string &key) {
|
||||
const OverrideEntry *best = nullptr;
|
||||
for (const OverrideEntry &entry : g_overrides) {
|
||||
if (entry.key != key)
|
||||
continue;
|
||||
|
||||
if (best == nullptr || entry.priority > best->priority ||
|
||||
(entry.priority == best->priority && entry.sequence > best->sequence))
|
||||
best = &entry;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
void SetOverride(const void *owner, bool priority, const std::string &key, const std::string &cmd) {
|
||||
auto existing =
|
||||
std::find_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner && entry.key == key;
|
||||
});
|
||||
|
||||
if (existing == g_overrides.end()) {
|
||||
g_overrides.push_back(
|
||||
{owner, key, cmd, priority, ++g_nextSequence});
|
||||
return;
|
||||
}
|
||||
|
||||
existing->command = cmd;
|
||||
existing->priority = priority;
|
||||
existing->sequence = ++g_nextSequence;
|
||||
}
|
||||
|
||||
void RemoveOverride(const void *owner, const std::string &key) {
|
||||
g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner &&
|
||||
entry.key == key;
|
||||
}),
|
||||
g_overrides.end());
|
||||
}
|
||||
|
||||
int ExecuteCommand(void *mgr, const char *cmd, int isDown) {
|
||||
if (ExecuteSpecialCommand(cmd, isDown))
|
||||
return 1;
|
||||
|
||||
return CommandExecute_o(mgr, cmd, isDown);
|
||||
}
|
||||
|
||||
int __fastcall CommandExecute_h(void *mgr, void *, const char *cmd, int isDown) {
|
||||
return ExecuteCommand(mgr, cmd, isDown);
|
||||
}
|
||||
|
||||
// Runs an override-resolved command with the same execution context the engine
|
||||
// wraps around a native keypress. FUN_BINDING_KEY_DISPATCH saves the frame-
|
||||
// script exec-context, zeroes it for the nested command, marks the manager as
|
||||
// mid-command (`+0xD8`), then restores the context and floors the nesting
|
||||
// depth. Native-table bindings already run inside this (the engine sets it up
|
||||
// before calling FUN_BINDING_COMMAND_EXECUTE, which we co-hook); overrides
|
||||
// bypass that path, so we mirror it here. Without it, a native Bindings.xml
|
||||
// command dispatched through an override would run without the context its
|
||||
// handler expects. Special SPELL/ITEM/MACRO/CLICK commands run harmlessly
|
||||
// inside the same wrapper.
|
||||
int DispatchOverrideCommand(void *mgr, const char *cmd, int isDown) {
|
||||
volatile uint32_t *context =
|
||||
reinterpret_cast<volatile uint32_t *>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
volatile uint32_t *depth = reinterpret_cast<volatile uint32_t *>(
|
||||
Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH);
|
||||
volatile uint32_t *executing =
|
||||
Game::Ptr<volatile uint32_t>(mgr, Offsets::OFF_BINDING_MANAGER_EXECUTING);
|
||||
|
||||
const uint32_t savedContext = *context;
|
||||
*depth = *depth + 1;
|
||||
if (*depth != 0)
|
||||
*context = 0;
|
||||
|
||||
*executing = 1;
|
||||
const int result = ExecuteCommand(mgr, cmd, isDown);
|
||||
*executing = 0;
|
||||
|
||||
if (*depth != 0)
|
||||
*context = savedContext;
|
||||
if (static_cast<int32_t>(*depth) - 1 > 0)
|
||||
*depth = *depth - 1;
|
||||
else
|
||||
*depth = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int __fastcall KeyDispatch_h(void *mgr, void *, const char *key, int isDown) {
|
||||
const std::string canonicalKey = CanonicalKey(key);
|
||||
|
||||
// Keep a key-down paired with its override command even if that override is
|
||||
// cleared while the command executes. The newly revealed native binding
|
||||
// must not receive only the key-up half of the same physical press.
|
||||
if (!isDown) {
|
||||
auto active = g_activeOverrides.find(canonicalKey);
|
||||
if (active != g_activeOverrides.end()) {
|
||||
DispatchOverrideCommand(mgr, active->second.c_str(), 0);
|
||||
g_activeOverrides.erase(active);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const OverrideEntry *entry = FindEffectiveOverride(canonicalKey);
|
||||
if (entry == nullptr)
|
||||
return KeyDispatch_o(mgr, key, isDown);
|
||||
|
||||
if (isDown)
|
||||
g_activeOverrides[canonicalKey] = entry->command;
|
||||
|
||||
return DispatchOverrideCommand(mgr, entry->command.c_str(), isDown);
|
||||
}
|
||||
|
||||
int SetBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string key = Game::Lua::ToString(L, 1);
|
||||
const std::string command = MakeCommand(prefix, Game::Lua::ToString(L, 2));
|
||||
|
||||
// Preserve the stock SetBinding return value, binding-set selection,
|
||||
// key normalization, UPDATE_BINDINGS event, and SaveBindings behavior.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
auto setBinding =
|
||||
reinterpret_cast<ScriptSetBinding_t>(Offsets::FUN_SCRIPT_SET_BINDING);
|
||||
|
||||
return setBinding(L);
|
||||
}
|
||||
|
||||
const char *OptionalMouseButton(void *L, int index, const char *usage) {
|
||||
if (Game::Lua::GetTop(L) < index || Game::Lua::Type(L, index) == Game::Lua::TYPE_NIL)
|
||||
return nullptr;
|
||||
|
||||
if (!Game::Lua::IsString(L, index)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Game::Lua::ToString(L, index);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingSpell(void *L) {
|
||||
return SetBindingCommand(L, kSpellPrefix, "Usage: SetBindingSpell(key, spell)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingItem(void *L) {
|
||||
return SetBindingCommand(L, kItemPrefix, "Usage: SetBindingItem(key, item)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingMacro(void *L) {
|
||||
return SetBindingCommand(L, kMacroPrefix, "Usage: SetBindingMacro(key, macro)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingClick(void *L) {
|
||||
constexpr const char *usage = "Usage: SetBindingClick(key, buttonName [, mouseButton])";
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string key = Game::Lua::ToString(L, 1);
|
||||
const char *buttonName = Game::Lua::ToString(L, 2);
|
||||
const char *mouseButton = OptionalMouseButton(L, 3, usage);
|
||||
const std::string command = MakeClickCommand(buttonName, mouseButton);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
auto setBinding =
|
||||
reinterpret_cast<ScriptSetBinding_t>(Offsets::FUN_SCRIPT_SET_BINDING);
|
||||
|
||||
return setBinding(L);
|
||||
}
|
||||
|
||||
bool ReadOverrideHeader(void *L, const char *usage, const void **owner,
|
||||
bool *priority, std::string *key) {
|
||||
*owner = Game::Lua::ResolveObject(L, 1);
|
||||
if (*owner == nullptr || Game::Lua::Type(L, 2) != Game::Lua::TYPE_BOOLEAN ||
|
||||
!Game::Lua::IsString(L, 3)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return false;
|
||||
}
|
||||
|
||||
*priority = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
*key = CanonicalKey(Game::Lua::ToString(L, 3));
|
||||
return true;
|
||||
}
|
||||
|
||||
int SetOverrideBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SetOverride(owner, priority, key,
|
||||
MakeCommand(prefix, Game::Lua::ToString(L, 4)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBinding(void *L) {
|
||||
constexpr const char *usage = "Usage: SetOverrideBinding(owner, isPriority, key [, command])";
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (Game::Lua::GetTop(L) < 4 ||
|
||||
Game::Lua::Type(L, 4) == Game::Lua::TYPE_NIL) {
|
||||
RemoveOverride(owner, key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SetOverride(owner, priority, key, Game::Lua::ToString(L, 4));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingSpell(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kSpellPrefix,
|
||||
"Usage: SetOverrideBindingSpell(owner, isPriority, key, spell)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingItem(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kItemPrefix,
|
||||
"Usage: SetOverrideBindingItem(owner, isPriority, key, item)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingMacro(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kMacroPrefix,
|
||||
"Usage: SetOverrideBindingMacro(owner, isPriority, key, macro)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingClick(void *L) {
|
||||
constexpr const char *usage =
|
||||
"Usage: SetOverrideBindingClick(owner, "
|
||||
"isPriority, key, buttonName [, mouseButton])";
|
||||
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *buttonName = Game::Lua::ToString(L, 4);
|
||||
const char *mouseButton = OptionalMouseButton(L, 5, usage);
|
||||
SetOverride(owner, priority, key,
|
||||
MakeClickCommand(buttonName, mouseButton));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_ClearOverrideBindings(void *L) {
|
||||
const void *owner = Game::Lua::ResolveObject(L, 1);
|
||||
if (owner == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: ClearOverrideBindings(owner)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner;
|
||||
}),
|
||||
g_overrides.end());
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// A fresh FrameScript state makes every owner pointer from the previous
|
||||
// state stale. Permanent bindings remain in vanilla's binding table.
|
||||
g_overrides.clear();
|
||||
g_activeOverrides.clear();
|
||||
g_nextSequence = 0;
|
||||
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingSpell", &Script_SetBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingItem", &Script_SetBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingMacro", &Script_SetBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingClick", &Script_SetBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBinding",
|
||||
&Script_SetOverrideBinding);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell",
|
||||
&Script_SetOverrideBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem",
|
||||
&Script_SetOverrideBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro",
|
||||
&Script_SetOverrideBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick",
|
||||
&Script_SetOverrideBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("ClearOverrideBindings",
|
||||
&Script_ClearOverrideBindings);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
const Game::HookAutoRegister _hookKeyDispatch{
|
||||
Offsets::FUN_BINDING_KEY_DISPATCH, reinterpret_cast<void *>(&KeyDispatch_h),
|
||||
reinterpret_cast<void **>(&KeyDispatch_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookCommandExecute{
|
||||
Offsets::FUN_BINDING_COMMAND_EXECUTE,
|
||||
reinterpret_cast<void *>(&CommandExecute_h),
|
||||
reinterpret_cast<void **>(&CommandExecute_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Bindings::Api
|
||||
+17
-49
@@ -18,29 +18,19 @@
|
||||
// against an internal cache to identify the player. With this we
|
||||
// just read the GUID directly.
|
||||
//
|
||||
// Implementation: the SMSG_MESSAGECHAT handler at FUN_0049D560 reads
|
||||
// the senderGUID from the packet (offsets `[ebp-0x10]` / `[ebp-0xC]`
|
||||
// in that function's stack frame) and forwards it as args 9-10 to
|
||||
// the chat dispatcher at FUN_0049A870. The dispatcher in turn fires
|
||||
// the appropriate CHAT_MSG_* event during its execution. We hook the
|
||||
// dispatcher's entry, stash the GUID into a file-scope global, call
|
||||
// the original (which fires the event synchronously), then clear the
|
||||
// global on return. Inside the addon's OnEvent, `GetCurrentChatGUID`
|
||||
// reads the global.
|
||||
//
|
||||
// Why not hook the SMSG handler directly: the GUID isn't in argument
|
||||
// registers/stack at the handler's entry — it's read mid-function
|
||||
// into locals. Hooking the dispatcher (which receives the GUID as
|
||||
// args) is structurally simpler. The trade-off: the dispatcher is
|
||||
// also called for synthetic chat (system notifications, arena team
|
||||
// events, etc.) with GUID = 0; for those, `GetCurrentChatGUID()`
|
||||
// returns nil — matches the modern API's behavior of nil GUID for
|
||||
// non-player-sourced chat.
|
||||
// The GUID is published for the duration of a chat dispatch by a
|
||||
// `DispatchScope` (see the header), which Chat::Dispatch — the single
|
||||
// owner of the FUN_0049A870 chat-dispatch hook — constructs around the
|
||||
// original call. The engine fires the CHAT_MSG_* event synchronously
|
||||
// inside that call, so `GetCurrentChatGUID` reads the right GUID from
|
||||
// the addon's OnEvent. The dispatcher is also called for synthetic chat
|
||||
// (system notifications, arena team events, etc.) with GUID = 0; for
|
||||
// those `GetCurrentChatGUID()` returns nil — matching the modern API's
|
||||
// behavior of a nil GUID for non-player-sourced chat.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -69,39 +59,17 @@ void RegisterLuaFunctions() {
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatDispatch_t ChatDispatch_o = nullptr;
|
||||
DispatchScope::DispatchScope(uint32_t guidLo, uint32_t guidHi)
|
||||
: prevLo_(g_currentGuidLo), prevHi_(g_currentGuidHi) {
|
||||
g_currentGuidLo = guidLo;
|
||||
g_currentGuidHi = guidHi;
|
||||
}
|
||||
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *sender, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi)
|
||||
{
|
||||
// Save before original — the engine's chat event fires DURING
|
||||
// origCall, and the addon's OnEvent runs synchronously inside
|
||||
// that window. The GUID must be readable at that point.
|
||||
const uint32_t prevLo = g_currentGuidLo;
|
||||
const uint32_t prevHi = g_currentGuidHi;
|
||||
g_currentGuidLo = static_cast<uint32_t>(senderGuidLo);
|
||||
g_currentGuidHi = static_cast<uint32_t>(senderGuidHi);
|
||||
|
||||
ChatDispatch_o(sender, chatType, arg3, arg4, arg5, arg6, arg7,
|
||||
targetGuidLo, targetGuidHi, arg10,
|
||||
senderGuidLo, senderGuidHi);
|
||||
|
||||
// Restore prior value rather than clearing to 0, so nested
|
||||
// dispatch (one chat event firing during another's OnEvent —
|
||||
// e.g. an addon calling SendChatMessage from its handler) leaves
|
||||
// the outer context's GUID intact when the inner returns.
|
||||
g_currentGuidLo = prevLo;
|
||||
g_currentGuidHi = prevHi;
|
||||
DispatchScope::~DispatchScope() {
|
||||
g_currentGuidLo = prevLo_;
|
||||
g_currentGuidHi = prevHi_;
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_CHAT_DISPATCH,
|
||||
reinterpret_cast<void *>(&ChatDispatch_h),
|
||||
reinterpret_cast<void **>(&ChatDispatch_o)};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
|
||||
+21
-48
@@ -13,55 +13,28 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::CurrentGUID {
|
||||
|
||||
// Hook for FUN_CHAT_DISPATCH (0x0049A870). Saves the senderGUID into
|
||||
// a file-scope global for the duration of the original call, then
|
||||
// clears it on return. Inside that window the engine fires the
|
||||
// appropriate CHAT_MSG_* event, so `GetCurrentChatGUID()` called from
|
||||
// the addon's OnEvent sees the GUID for the message that triggered it.
|
||||
//
|
||||
// 12-arg __fastcall signature. The function has a stack-probe
|
||||
// prologue (`mov eax, 0x1F0C; call _chkstk`) — its frame is ~8KB.
|
||||
// Verified by disassembling 0x0049A870 in 1.12.1: stack-arg access
|
||||
// reaches `[ebp+0x2C]` (= param 12) and the function exits with
|
||||
// `ret 0x28` (cleans 40 bytes = 10 stack args + 2 register args).
|
||||
//
|
||||
// ECX = arg1 (sender name string)
|
||||
// EDX = arg2 (chat type byte)
|
||||
// [esp+ 4] = arg3
|
||||
// [esp+ 8] = arg4
|
||||
// [esp+ c] = arg5
|
||||
// [esp+10] = arg6
|
||||
// [esp+14] = arg7
|
||||
// [esp+18] = arg8 — target GUID lo (for whisper/channel target)
|
||||
// [esp+1c] = arg9 — target GUID hi
|
||||
// [esp+20] = arg10
|
||||
// [esp+24] = arg11 — sender GUID lo ← captured
|
||||
// [esp+28] = arg12 — sender GUID hi ← captured
|
||||
//
|
||||
// Identified args 11/12 as senderGUID by tracing the AFK auto-reply
|
||||
// path at 0x0049AB39: reads params 11/12, OR's them to check for
|
||||
// non-zero, pushes them onto the packet stream via FUN_00418370
|
||||
// (the "write u64" helper). That's the GUID the AFK whisper-back is
|
||||
// addressed to — i.e. the original message sender.
|
||||
//
|
||||
// An earlier 10-arg signature derived from Ghidra (on the wrong
|
||||
// binary) had `ret 0x20` cleanup. The 8-byte cleanup mismatch with
|
||||
// the engine's `ret 0x28`-shaped call left 8 bytes of stack
|
||||
// uncleaned per chat-dispatch invocation, corrupting the engine's
|
||||
// stack frame and crashing on a later indirect call. See the
|
||||
// commit message for the full diagnosis.
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *sender, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
extern ChatDispatch_t ChatDispatch_o;
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *sender, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
// RAII scope that publishes the sender GUID of the chat message currently being
|
||||
// dispatched, for `GetCurrentChatGUID()` to read. Chat::Dispatch constructs one
|
||||
// around the original chat-dispatch call: the engine fires the CHAT_MSG_* event
|
||||
// synchronously inside that call, so an addon's OnEvent sees the GUID of the
|
||||
// message that triggered it. Restores the prior value on destruction rather than
|
||||
// clearing to 0, so nested dispatch (a chat event firing during another's
|
||||
// OnEvent — e.g. an addon calling SendChatMessage from its handler) leaves the
|
||||
// outer context's GUID intact when the inner returns.
|
||||
class DispatchScope {
|
||||
public:
|
||||
DispatchScope(uint32_t guidLo, uint32_t guidHi);
|
||||
~DispatchScope();
|
||||
DispatchScope(const DispatchScope &) = delete;
|
||||
DispatchScope &operator=(const DispatchScope &) = delete;
|
||||
|
||||
private:
|
||||
uint32_t prevLo_;
|
||||
uint32_t prevHi_;
|
||||
};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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/>.
|
||||
|
||||
// Single owner of the chat-dispatch hook (FUN_0049A870).
|
||||
//
|
||||
// The engine routes every server-delivered chat message — say/yell/whisper/
|
||||
// channel/emote and synthetic system notifications — through this one function.
|
||||
// (Addon `print`/`AddMessage` does NOT pass through it.) Several independent
|
||||
// concerns need to observe or transform a dispatched message, but MinHook allows
|
||||
// only ONE hook per target, so this module owns the hook and orchestrates them,
|
||||
// mirroring Net::PacketDispatch (one hook, fanned out to independent concerns):
|
||||
//
|
||||
// - Chat::IconFilter::Sanitize — strip player-injected `|T` icon spoofs from
|
||||
// the message before the engine builds the chat line AND the speech bubble
|
||||
// (both come from the same buffer, so one sanitize covers both).
|
||||
// - Chat::RaidMarkers::Substitute — expand `{rt1}`/`{skull}`/… raid-target
|
||||
// tokens into inline marker icons. Runs AFTER Sanitize so a player gets the
|
||||
// fixed marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::CurrentGUID::DispatchScope — publish the sender GUID for the
|
||||
// synchronous CHAT_MSG_* event so `GetCurrentChatGUID()` can read it.
|
||||
//
|
||||
// Signature — `__fastcall`, `RET 0x28`:
|
||||
// ECX = arg1 — the MESSAGE text (verified from the call site in FUN_0049D560:
|
||||
// `MOV ECX,[EBP-0x10]` = the raw/processed message. An older
|
||||
// note mislabeled this "sender name".) Copied into one buffer
|
||||
// fed to both the chat frame and the bubble spawn FUN_00608AC0.
|
||||
// EDX = arg2 — chat type byte
|
||||
// [esp+04] = arg3 [esp+08] = arg4 [esp+0c] = arg5
|
||||
// [esp+10] = arg6 [esp+14] = arg7
|
||||
// [esp+18] = arg8 — target GUID lo (whisper/channel target)
|
||||
// [esp+1c] = arg9 — target GUID hi
|
||||
// [esp+20] = arg10
|
||||
// [esp+24] = arg11 — sender GUID lo ← published
|
||||
// [esp+28] = arg12 — sender GUID hi ← published
|
||||
//
|
||||
// The `RET 0x28` cleanup (40 bytes = 10 stack args) MUST match the declared
|
||||
// signature exactly; an earlier 10-arg / `ret 0x20` version left 8 bytes
|
||||
// uncleaned per call and crashed the engine on a later indirect call.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
#include "IconFilter.h"
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::Dispatch {
|
||||
|
||||
namespace {
|
||||
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
|
||||
ChatDispatch_t g_original = nullptr;
|
||||
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi)
|
||||
{
|
||||
// Strip player-injected `|T` icon escapes (covers chat line + speech bubble;
|
||||
// both build from this message). The 0x800 buffer matches the engine's own
|
||||
// message-buffer cap.
|
||||
char iconBuf[0x800];
|
||||
const char *msg = Chat::IconFilter::Sanitize(message, iconBuf, sizeof iconBuf);
|
||||
|
||||
// Expand raid-target tokens ({rt1}/{skull}/…) into inline marker icons —
|
||||
// after the sanitize, so only these fixed, safe escapes are added.
|
||||
char markerBuf[0x800];
|
||||
msg = Chat::RaidMarkers::Substitute(msg, markerBuf, sizeof markerBuf);
|
||||
|
||||
// Publish the sender GUID for the CHAT_MSG_* event the original fires
|
||||
// synchronously; restored when this scope ends (after the original returns).
|
||||
Chat::CurrentGUID::DispatchScope guid(static_cast<uint32_t>(senderGuidLo),
|
||||
static_cast<uint32_t>(senderGuidHi));
|
||||
|
||||
g_original(msg, chatType, arg3, arg4, arg5, arg6, arg7,
|
||||
targetGuidLo, targetGuidHi, arg10,
|
||||
senderGuidLo, senderGuidHi);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_CHAT_DISPATCH,
|
||||
reinterpret_cast<void *>(&ChatDispatch_h),
|
||||
reinterpret_cast<void **>(&g_original)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Chat::Dispatch
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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/>.
|
||||
|
||||
// Inline-texture (`|T`) chat anti-spoof.
|
||||
//
|
||||
// Vanilla neutralizes player-*typed* escapes in chat (a typed `|cff…|r`,
|
||||
// `|Hitem…|h` or `|T…|t` shows as raw text) — only *trusted* text renders them
|
||||
// (addon `AddMessage`, shift-clicked links). Our Text::InlineTexture backport
|
||||
// breaks that parity for `|T` alone: it detects the neutralized `||T` form and
|
||||
// draws the icon anyway, so a player could `/say` `|Tpath|t` and have a real
|
||||
// icon appear in everyone's chat AND in the speech bubble over their head.
|
||||
//
|
||||
// This restores vanilla's behavior for `|T` by defanging it in the chat path,
|
||||
// while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw for typed
|
||||
// input). It CANNOT be done in the text emitter: addon `|T` and player `|T`
|
||||
// reach the emitter as byte-identical `||T` (vanilla doubles the unrecognized
|
||||
// `|T` for both), so the only place they're distinguishable is the source —
|
||||
// server-delivered chat flows through the chat dispatcher, addon `print` does
|
||||
// not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
//
|
||||
// Defang = replace the `|` of each `|T` with a space, breaking the pipe→T
|
||||
// adjacency the inline-texture detector keys on (`|T` and `||T` both contain the
|
||||
// `|T` substring, so this catches the doubled form too). The path text stays
|
||||
// readable, the closer `|t` is inert without an opener, and every other escape
|
||||
// is untouched.
|
||||
|
||||
#include "IconFilter.h"
|
||||
|
||||
namespace Chat::IconFilter {
|
||||
|
||||
namespace {
|
||||
|
||||
// True if `s` contains an inline-texture opener (`|T`) anywhere.
|
||||
bool HasIconEscape(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
if (s[0] == '|' && s[1] == 'T')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *Sanitize(const char *msg, char *buf, size_t bufSize) {
|
||||
if (msg == nullptr || !HasIconEscape(msg))
|
||||
return msg; // common case: forward untouched, no copy
|
||||
|
||||
// NEVER write `msg` in place: several of the chat handler's ~50 callers pass
|
||||
// read-only `.rdata` literals (system notifications), so an in-place edit
|
||||
// would fault. We copy only when a `|T` is actually present.
|
||||
//
|
||||
// Defanging must not leave a LONE `|` followed by a non-escape char — that's
|
||||
// an invalid escape, which the chat frame tolerates but the speech-bubble
|
||||
// text setter chokes on, producing an empty bubble (verified in-game). By the
|
||||
// time chat reaches us the escape is usually the doubled `||T` (vanilla's own
|
||||
// neutralization of typed escapes), where `||` is a VALID literal-pipe escape.
|
||||
// So: keep the pipe(s) and blank the `T` when the pipe completes a `||`; blank
|
||||
// BOTH chars for a lone `|T`. Either way no dangling `|` remains, and no `|T`
|
||||
// opener survives for the inline-texture detector to fire on.
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (msg[i] == '|' && msg[i + 1] == 'T') {
|
||||
const bool doubledPipe = (i > 0 && msg[i - 1] == '|');
|
||||
buf[j++] = doubledPipe ? '|' : ' '; // keep pipe only if it completes `||`
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' '; // blank the `T`
|
||||
i += 2; // consumed `|T`
|
||||
} else {
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
}
|
||||
buf[j] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace Chat::IconFilter
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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 <cstddef>
|
||||
|
||||
namespace Chat::IconFilter {
|
||||
|
||||
// Strips player-injected inline-texture (`|T…|t`) escapes from a chat message.
|
||||
// Returns `msg` unchanged when it contains no `|T` (the common case — no copy),
|
||||
// otherwise a sanitized copy written into `buf`. Called by Chat::Dispatch on the
|
||||
// message the engine's chat handler is about to display. See IconFilter.cpp for
|
||||
// the anti-spoof rationale and why sanitizing here (not in the text emitter) is
|
||||
// the only place player-typed and addon `|T` are distinguishable.
|
||||
const char *Sanitize(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::IconFilter
|
||||
@@ -0,0 +1,154 @@
|
||||
// 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/>.
|
||||
|
||||
// Raid-target marker chat substitutions ({rt1}..{rt8} / {star} / {skull} / ...).
|
||||
//
|
||||
// Modern WoW lets players type `{skull}` (etc.) in chat and everyone sees the
|
||||
// raid-target icon. Vanilla 1.12 has no such substitution. We backport it onto
|
||||
// the inline-texture (`|T…|t`) renderer: each recognized token is rewritten into
|
||||
// a `|T` escape that crops the shared raid-target sprite sheet
|
||||
// `Interface\TargetingFrame\UI-RaidTargetingIcons` to the matching 64×64 cell.
|
||||
//
|
||||
// Runs in Chat::Dispatch AFTER Chat::IconFilter::Sanitize. Order is deliberate:
|
||||
// sanitize first defangs any player-typed raw `|T` spoof, then we add ONLY the
|
||||
// fixed, known-safe marker escapes — so players get the markers without a path to
|
||||
// inject arbitrary textures.
|
||||
//
|
||||
// Icon→texcoord mapping verified against the client's own UnitPopup.lua
|
||||
// (RAID_TARGET_1..8): the sheet is 256×256 with 64-pixel cells, marker N at
|
||||
// column (N-1)%4, row (N-1)/4. So RT1(star) is the top-left cell and RT8(skull)
|
||||
// is row 1, column 3. The inline renderer's OpenGL v-flip already draws this sheet
|
||||
// right-side-up (it's the sheet that flip was verified against), so top<bottom
|
||||
// pixel coords render correctly.
|
||||
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Chat::RaidMarkers {
|
||||
|
||||
namespace {
|
||||
|
||||
// On-screen pixel size of an inline marker. The default chat font is ~14px, so 14
|
||||
// keeps a marker about one line tall. Fixed (the substitution is a static string
|
||||
// rewrite and can't see the target font); tunable here if it reads too small/large.
|
||||
constexpr int kMarkerSizePx = 14;
|
||||
|
||||
struct Token {
|
||||
const char *name; // lowercase alias, no braces
|
||||
int marker; // raid-target index 1..8
|
||||
};
|
||||
|
||||
// Aliases per the modern client, all case-insensitive. `{coin}` is a colloquial
|
||||
// alias for `{circle}` (RT2) and `{x}` for `{cross}` (RT7) — both shown in the
|
||||
// in-game substitution help.
|
||||
constexpr Token kTokens[] = {
|
||||
{"rt1", 1}, {"star", 1},
|
||||
{"rt2", 2}, {"circle", 2}, {"coin", 2},
|
||||
{"rt3", 3}, {"diamond", 3},
|
||||
{"rt4", 4}, {"triangle", 4},
|
||||
{"rt5", 5}, {"moon", 5},
|
||||
{"rt6", 6}, {"square", 6},
|
||||
{"rt7", 7}, {"cross", 7}, {"x", 7},
|
||||
{"rt8", 8}, {"skull", 8},
|
||||
};
|
||||
|
||||
char LowerAscii(char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c; }
|
||||
|
||||
// Case-insensitive match of the n-char token body [s, s+n) against a lowercase
|
||||
// literal — true only when `lit` is exactly n chars long.
|
||||
bool EqCI(const char *s, size_t n, const char *lit) {
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (lit[i] == '\0' || LowerAscii(s[i]) != lit[i])
|
||||
return false;
|
||||
return lit[n] == '\0';
|
||||
}
|
||||
|
||||
// Raid-target index (1..8) for the token body [name, name+n), or 0 if unknown.
|
||||
int MatchMarker(const char *name, size_t n) {
|
||||
for (const Token &t : kTokens)
|
||||
if (EqCI(name, n, t.name))
|
||||
return t.marker;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool HasBrace(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
if (*s == '{')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
if (msg == nullptr || !HasBrace(msg))
|
||||
return msg; // common case: forward untouched, no copy
|
||||
|
||||
// A token inflates ~28x (a 3-char `{rtN}` -> ~85-byte `|T` markup), so a
|
||||
// marker-dense message can exceed `buf`. Never DROP message content to fit:
|
||||
// only expand a token when the expansion AND the whole remaining input (worst
|
||||
// case copied 1:1 as literal text) still fit. Past that point later tokens
|
||||
// stay literal `{rtN}`, so the full message text is always preserved — a DLL
|
||||
// user never sees a shorter line than a non-DLL user, just fewer icons.
|
||||
const size_t msgLen = std::strlen(msg);
|
||||
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (msg[i] == '{') {
|
||||
// Scan the token body to the closing brace (bounded — longest alias
|
||||
// is 8 chars, so a `{` with no nearby `}` is just literal text).
|
||||
size_t k = i + 1;
|
||||
while (msg[k] != '\0' && msg[k] != '}' && k - i <= 12)
|
||||
++k;
|
||||
if (msg[k] == '}') {
|
||||
const int marker = MatchMarker(msg + i + 1, k - (i + 1));
|
||||
if (marker != 0) {
|
||||
const int col = (marker - 1) % 4;
|
||||
const int row = (marker - 1) / 4;
|
||||
const int l = col * 64, r = l + 64, t = row * 64, b = t + 64;
|
||||
// Emit the pipe-DOUBLED form `||T…||t`. The inline emitter
|
||||
// renders both `|T` and `||T`, but the vanilla speech-bubble
|
||||
// text builder (FUN_004b1600) TRUNCATES at a lone `|T` while
|
||||
// PRESERVING `||` — so the doubled form is what survives into
|
||||
// the bubble's FontString (which pfUI's bubble reskin copies
|
||||
// and renders). Anti-spoof Sanitize runs before this, so these
|
||||
// are the only `||T` openers a player can produce.
|
||||
char markup[128];
|
||||
const int m = std::snprintf(
|
||||
markup, sizeof markup,
|
||||
"||TInterface\\TargetingFrame\\UI-RaidTargetingIcons:%d:%d:0:0:256:256:"
|
||||
"%d:%d:%d:%d||t",
|
||||
kMarkerSizePx, kMarkerSizePx, l, r, t, b);
|
||||
// Reserve room for the literal tail after this token
|
||||
// (msg[k+1..]) so the rest of the message always fits.
|
||||
const size_t tail = msgLen - (k + 1);
|
||||
if (m > 0 && j + static_cast<size_t>(m) + tail + 1 <= bufSize) {
|
||||
std::memcpy(buf + j, markup, static_cast<size_t>(m));
|
||||
j += static_cast<size_t>(m);
|
||||
i = k + 1; // consume through the closing `}`
|
||||
continue;
|
||||
}
|
||||
// No room for the expansion — fall through and copy the
|
||||
// literal `{` so the message is never left truncated mid-token.
|
||||
}
|
||||
}
|
||||
}
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
buf[j] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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 <cstddef>
|
||||
|
||||
namespace Chat::RaidMarkers {
|
||||
|
||||
// Expands raid-target marker tokens in a chat message into inline raid-target
|
||||
// icon textures — the modern `{rt1}`..`{rt8}` / `{star}` / `{circle}` / `{skull}`
|
||||
// (etc.) chat substitutions, backported onto the inline-texture renderer. Returns
|
||||
// `msg` unchanged when it contains no `{` (the common case — no copy), otherwise a
|
||||
// rewritten copy in `buf`. Called by Chat::Dispatch AFTER the `|T` anti-spoof
|
||||
// sanitize, so a player can use the fixed, safe marker icons but still can't
|
||||
// inject an arbitrary texture path. See RaidMarkers.cpp for the token table and
|
||||
// the icon→texcoord mapping.
|
||||
const char *Substitute(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
+19
-19
@@ -80,12 +80,12 @@ bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
// readable-text id is non-zero (letters / server-authored notes).
|
||||
bool ItemReadable(const uint8_t *item, const uint8_t *record) {
|
||||
if (record != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_PAGE_TEXT) != 0)
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_PAGE_TEXT) != 0)
|
||||
return true;
|
||||
const uint8_t *descriptor = Item::ObjectFields(item);
|
||||
return descriptor != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_READABLE_TEXT_ID) != 0;
|
||||
Game::Read<uint32_t>(
|
||||
descriptor, Offsets::OFF_DESCRIPTOR_READABLE_TEXT_ID) != 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
@@ -116,14 +116,14 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
int stackCount = 0;
|
||||
bool isBound = false;
|
||||
if (descriptor != nullptr) {
|
||||
stackCount = *reinterpret_cast<const int *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
isBound = (*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_FLAGS) &
|
||||
stackCount = Game::Read<int>(
|
||||
descriptor, Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
isBound = (Game::Read<uint32_t>(
|
||||
descriptor, Offsets::OFF_DESCRIPTOR_FLAGS) &
|
||||
Offsets::ITEM_FLAG_SOULBOUND) != 0;
|
||||
}
|
||||
const bool isLocked = (*reinterpret_cast<const uint32_t *>(
|
||||
item + Offsets::OFF_ITEM_CLIENT_LOCK) &
|
||||
const bool isLocked = (Game::Read<uint32_t>(
|
||||
item, Offsets::OFF_ITEM_CLIENT_LOCK) &
|
||||
Offsets::ITEM_CLIENT_LOCK_BIT) != 0;
|
||||
const bool isReadable = ItemReadable(item, record);
|
||||
|
||||
@@ -135,17 +135,17 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
bool hasLoot = false;
|
||||
bool hasNoValue = false;
|
||||
if (record != nullptr) {
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
const uint32_t displayInfoID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
haveIcon = BuildIconPath(displayInfoID, iconPath, sizeof(iconPath));
|
||||
quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
quality = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
haveQuality = true;
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
const uint32_t flags = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
hasLoot = (flags & Offsets::ITEM_FLAG_LOOTABLE) != 0;
|
||||
hasNoValue = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SELL_PRICE) == 0;
|
||||
hasNoValue = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SELL_PRICE) == 0;
|
||||
}
|
||||
|
||||
const char *hyperlink = Item::Link::FromCGItem(item);
|
||||
@@ -155,8 +155,8 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
if (Item::Link::NameFromCGItem(item, nameBuf, sizeof(nameBuf))) {
|
||||
itemName = nameBuf;
|
||||
} else if (record != nullptr) {
|
||||
const char *baseName = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *baseName = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (baseName != nullptr && baseName[0] != '\0')
|
||||
itemName = baseName;
|
||||
}
|
||||
|
||||
+33
-21
@@ -53,6 +53,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cache/QueryLoad.h"
|
||||
#include "creature/Info.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "dbc/Names.h"
|
||||
#include "event/Custom.h"
|
||||
@@ -94,14 +95,14 @@ int __fastcall Script_GetCreatureInfoByID(void *L) {
|
||||
if (rec == nullptr)
|
||||
return 0; // not cached -> nil
|
||||
|
||||
const char *name = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_NAME);
|
||||
const char *name = Game::Read<const char *>(rec, Offsets::OFF_CREATURE_NAME);
|
||||
const char *subName =
|
||||
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_SUBNAME);
|
||||
const uint32_t type = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_TYPE);
|
||||
const uint32_t family = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_FAMILY);
|
||||
const uint32_t rank = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_RANK);
|
||||
Game::Read<const char *>(rec, Offsets::OFF_CREATURE_SUBNAME);
|
||||
const uint32_t type = Game::Read<uint32_t>(rec, Offsets::OFF_CREATURE_TYPE);
|
||||
const uint32_t family = Game::Read<uint32_t>(rec, Offsets::OFF_CREATURE_FAMILY);
|
||||
const uint32_t rank = Game::Read<uint32_t>(rec, Offsets::OFF_CREATURE_RANK);
|
||||
const uint32_t displayID =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_DISPLAYID);
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_CREATURE_DISPLAYID);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "creatureID", creatureID);
|
||||
@@ -212,7 +213,7 @@ int __fastcall Script_GetCreatureFamilyIDs(void *L) {
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CREATUREFAMILY_COUNT);
|
||||
Game::Read<int>(Offsets::VAR_CREATUREFAMILY_COUNT);
|
||||
int n = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CREATUREFAMILY_RECORDS,
|
||||
@@ -249,8 +250,8 @@ int __fastcall Script_GetFactionInfo(void *L) {
|
||||
static_cast<uint32_t>(raceID));
|
||||
if (crRec == nullptr)
|
||||
return 0; // unknown race
|
||||
const int ftId = *reinterpret_cast<const int *>(
|
||||
crRec + Offsets::OFF_CHRRACES_FACTION_TEMPLATE);
|
||||
const int ftId = Game::Read<int>(
|
||||
crRec, Offsets::OFF_CHRRACES_FACTION_TEMPLATE);
|
||||
if (ftId <= 0)
|
||||
return 0;
|
||||
|
||||
@@ -259,31 +260,31 @@ int __fastcall Script_GetFactionInfo(void *L) {
|
||||
static_cast<uint32_t>(ftId));
|
||||
if (ftRec == nullptr)
|
||||
return 0;
|
||||
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
|
||||
ftRec + Offsets::OFF_FACTIONTEMPLATE_GROUP_MASK);
|
||||
const uint32_t mask = Game::Read<uint32_t>(
|
||||
ftRec, Offsets::OFF_FACTIONTEMPLATE_GROUP_MASK);
|
||||
|
||||
// FactionGroup is stored contiguously (records-base + i*stride), not as a
|
||||
// pointer array — iterate by hand rather than via DBC::Record.
|
||||
auto *fgBase = *reinterpret_cast<const uint8_t *const *>(
|
||||
auto *fgBase = Game::Read<const uint8_t *>(
|
||||
Offsets::VAR_FACTIONGROUP_RECORDS);
|
||||
const int fgCount =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_FACTIONGROUP_COUNT);
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
Game::Read<int>(Offsets::VAR_FACTIONGROUP_COUNT);
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (fgBase == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int i = 0; i < fgCount; ++i) {
|
||||
const uint8_t *row = fgBase + i * Offsets::FACTIONGROUP_STRIDE;
|
||||
const int bit = *reinterpret_cast<const int *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_BIT);
|
||||
const int bit = Game::Read<int>(
|
||||
row, Offsets::OFF_FACTIONGROUP_BIT);
|
||||
if ((mask & (1u << (bit & 0x1F))) == 0)
|
||||
continue;
|
||||
const char *loc = *reinterpret_cast<const char *const *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_NAMES + locale * 4);
|
||||
const char *loc = Game::Read<const char *>(
|
||||
row, Offsets::OFF_FACTIONGROUP_NAMES + locale * 4);
|
||||
if (loc == nullptr || *loc == '\0')
|
||||
continue; // "Player" / "Monster" have empty localized names
|
||||
const char *eng = *reinterpret_cast<const char *const *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_ENGLISH);
|
||||
const char *eng = Game::Read<const char *>(
|
||||
row, Offsets::OFF_FACTIONGROUP_ENGLISH);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", loc);
|
||||
@@ -323,7 +324,7 @@ int __fastcall Script_GetCreatureTypeIDs(void *L) {
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CREATURETYPE_COUNT);
|
||||
Game::Read<int>(Offsets::VAR_CREATURETYPE_COUNT);
|
||||
int n = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CREATURETYPE_RECORDS,
|
||||
@@ -367,4 +368,15 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Public accessor for Model::DisplayInfo's SetCreature — the same peek
|
||||
// GetCreatureInfoByID uses, reduced to the display-ID field. 0 = uncached.
|
||||
uint32_t DisplayID(uint32_t creatureID) {
|
||||
if (creatureID == 0)
|
||||
return 0;
|
||||
const uint8_t *rec = PeekCreature(creatureID);
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
return Game::Read<uint32_t>(rec, Offsets::OFF_CREATURE_DISPLAYID);
|
||||
}
|
||||
|
||||
} // namespace Creature::Info
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 Creature::Info {
|
||||
|
||||
// The cached model display ID for a creature/NPC entry, or 0 when the creature
|
||||
// is not in the client creature cache (creaturecache.wdb, or queried this
|
||||
// session). A synchronous peek — no network query. Backs `Model:SetCreature`:
|
||||
// the cache being keyed by entry ID and carrying a display ID is exactly the
|
||||
// entry-ID → model mapping vanilla has no DBC for.
|
||||
uint32_t DisplayID(uint32_t creatureID);
|
||||
|
||||
} // namespace Creature::Info
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetCoinTextureString` / `C_CurrencyInfo.GetCoinTextureString` — the coin-icon
|
||||
// money string (e.g. "12<gold> 34<silver> 56<copper>"), a C++ backport of the
|
||||
// retail C function.
|
||||
//
|
||||
// Behaviour derived from 3.3.5's `Script_GetCoinTextureString` (FUN_00510d00 →
|
||||
// formatter FUN_007e7e10): split the copper amount into gold / silver / copper,
|
||||
// emit only the non-zero denominations in that order, space-separated; a zero
|
||||
// amount shows "0" copper. Each denomination is built from a localized
|
||||
// GlobalString — `GOLD_AMOUNT_TEXTURE` / `SILVER_AMOUNT_TEXTURE` /
|
||||
// `COPPER_AMOUNT_TEXTURE` — exactly as the engine does (its formatter
|
||||
// `FUN_0076f070` is a C-format over the same GlobalString). Those strings don't
|
||||
// exist in stock 1.12, so they're backported in `locales/enUS.lua`
|
||||
// (language-independent, so the enUS base covers every locale); the embedded
|
||||
// `!!!ClassicAPI` addon guarantees they're loaded.
|
||||
//
|
||||
// 1.12 has no separate UI-GoldIcon / UI-SilverIcon / UI-CopperIcon textures
|
||||
// (those are a later-expansion split); vanilla uses ONE sprite sheet
|
||||
// `Interface\MoneyFrame\UI-MoneyIcons` cropped per coin (gold 0-0.25, silver
|
||||
// 0.25-0.5, copper 0.5-0.75 across a 4-cell strip — from CoinPickupFrame.xml).
|
||||
// So the format carries the inline-texture texcoord fields (texW=4 texH=1, then
|
||||
// the per-coin left:right cell). The `|T` markup renders as real coin icons via
|
||||
// ClassicAPI's inline-texture backport (`Text::InlineTexture`).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace Currency::CoinText {
|
||||
|
||||
namespace {
|
||||
|
||||
// Icon size. Retail defaults fontHeight to 0 = "use the display font's height",
|
||||
// and the inline-texture renderer now honours 0 (it resolves the coin to the
|
||||
// line's font height at draw time), so coins match the surrounding text instead
|
||||
// of a fixed size. Callers may still pass an explicit height.
|
||||
constexpr int kDefaultHeight = 0;
|
||||
|
||||
// Fallbacks byte-identical to the backported GlobalStrings, used only if the
|
||||
// locale layer somehow isn't loaded when this is first called.
|
||||
// Format: count |T sheet : height=0 : width=0 : offsetX=0 : offsetY=0 : texW=4 :
|
||||
// texH=1 : left : right : top=0 : bottom=1 |t (left:right selects the coin cell).
|
||||
// height/width 0 = size to the font. offsetX stays 0: the inline-texture
|
||||
// renderer's font-relative pad spaces the coin from its digits (the historical
|
||||
// "coin sits on the digit" bug was the emitter's lazy pen read-back, fixed in
|
||||
// the DLL). Never grow a per-coin offset here.
|
||||
constexpr const char *kGoldFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
constexpr const char *kSilverFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
// The three coin arts occupy IDENTICAL pixels within their sheet cells (x 0-14,
|
||||
// y 1-14 of 16 — confirmed by decoding the DXT3 alpha of the exported
|
||||
// UI-MoneyIcons.blp); only the left:right cell select differs (gold 0:1,
|
||||
// silver 1:2, copper 2:3).
|
||||
constexpr const char *kCopperFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:2:3:0:1|t";
|
||||
|
||||
// Formats one denomination from its localized GlobalString and appends it to
|
||||
// `out`. Reads `_G[globalName]` the way retail reads the coin GlobalStrings; the
|
||||
// format consumes (count, iconHeight, iconWidth). No separator between
|
||||
// denominations — the renderer's symmetric per-icon pad already spaces each coin
|
||||
// from the surrounding digits, so a space here would double the gap.
|
||||
void AppendDenom(void *L, std::string &out, const char *globalName, const char *fallback,
|
||||
int count, int height) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushLocalizedString(L, globalName, fallback); // [.., fmt]
|
||||
const char *fmt = Game::Lua::ToString(L, -1);
|
||||
char piece[256];
|
||||
piece[0] = '\0';
|
||||
if (fmt != nullptr)
|
||||
std::snprintf(piece, sizeof piece, fmt, count, height, height);
|
||||
Game::Lua::SetTop(L, top); // pop the format string
|
||||
out += piece;
|
||||
}
|
||||
|
||||
// GetCoinTextureString(amount [, fontHeight]) -> string
|
||||
int __fastcall Script_GetCoinTextureString(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetCoinTextureString(amount [, fontHeight])");
|
||||
return 0; // unreachable — Error long-jumps
|
||||
}
|
||||
const double amt = Game::Lua::ToNumber(L, 1);
|
||||
long long amount = static_cast<long long>(amt < 0.0 ? amt - 0.5 : amt + 0.5); // round
|
||||
if (amount < 0)
|
||||
amount = 0;
|
||||
// Vanilla money is a uint32 copper field, so 0xFFFFFFFF copper (~429,496
|
||||
// gold) is the most the game can represent. Clamp to it: an unclamped huge
|
||||
// input (e.g. 1e15) makes `gold` exceed INT_MAX, and the cast below truncates
|
||||
// it to the low 32 bits, printing a garbage count through the "%d" format.
|
||||
constexpr long long kMaxCopper = 0xFFFFFFFFLL;
|
||||
if (amount > kMaxCopper)
|
||||
amount = kMaxCopper;
|
||||
|
||||
int height = kDefaultHeight;
|
||||
if (Game::Lua::IsNumber(L, 2)) {
|
||||
const int h = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (h > 0)
|
||||
height = h;
|
||||
}
|
||||
|
||||
const int gold = static_cast<int>(amount / 10000);
|
||||
const int silver = static_cast<int>((amount % 10000) / 100);
|
||||
const int copper = static_cast<int>(amount % 100);
|
||||
|
||||
std::string out;
|
||||
if (amount == 0) {
|
||||
AppendDenom(L, out, "COPPER_AMOUNT_TEXTURE", kCopperFallback, 0, height);
|
||||
} else {
|
||||
if (gold > 0)
|
||||
AppendDenom(L, out, "GOLD_AMOUNT_TEXTURE", kGoldFallback, gold, height);
|
||||
if (silver > 0)
|
||||
AppendDenom(L, out, "SILVER_AMOUNT_TEXTURE", kSilverFallback, silver, height);
|
||||
if (copper > 0)
|
||||
AppendDenom(L, out, "COPPER_AMOUNT_TEXTURE", kCopperFallback, copper, height);
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, out.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCoinTextureString", &Script_GetCoinTextureString);
|
||||
Game::Lua::RegisterTableFunction("C_CurrencyInfo", "GetCoinTextureString",
|
||||
&Script_GetCoinTextureString);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Currency::CoinText
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Storage.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -25,7 +26,7 @@ namespace EquipmentSet::Storage {
|
||||
namespace {
|
||||
|
||||
const char *ReadAccountName() {
|
||||
return *reinterpret_cast<const char *const *>(Offsets::VAR_ACCOUNT_NAME_PTR);
|
||||
return Game::Read<const char *>(Offsets::VAR_ACCOUNT_NAME_PTR);
|
||||
}
|
||||
|
||||
const char *ReadCharacterName() {
|
||||
@@ -34,10 +35,10 @@ const char *ReadCharacterName() {
|
||||
}
|
||||
|
||||
const char *ReadRealmName() {
|
||||
auto *info = *reinterpret_cast<uint8_t **>(Offsets::VAR_REALM_INFO_PTR);
|
||||
auto *info = Game::Read<uint8_t *>(Offsets::VAR_REALM_INFO_PTR);
|
||||
if (info == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(info + Offsets::OFF_REALM_INFO_NAME);
|
||||
return Game::Read<const char *>(info, Offsets::OFF_REALM_INFO_NAME);
|
||||
}
|
||||
|
||||
// Removes trailing CR/LF/whitespace. Names from the engine are ASCII
|
||||
|
||||
@@ -210,8 +210,8 @@ int __fastcall Script_GameTooltipSetEquipmentSet(void *L) {
|
||||
unnamedMissing++;
|
||||
continue;
|
||||
}
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *name = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr || name[0] == '\0') {
|
||||
unnamedMissing++;
|
||||
continue;
|
||||
|
||||
+29
-29
@@ -112,13 +112,13 @@ void SMemFree(void *ptr) {
|
||||
int TryClaim(const char *eventName) {
|
||||
if (!g_writesEnabled)
|
||||
return -1;
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto **namePtr = reinterpret_cast<const char **>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
auto **namePtr = Game::Ptr<const char *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (*namePtr == nullptr) {
|
||||
char *copy = SStrDup(eventName);
|
||||
@@ -169,15 +169,15 @@ void Grow() {
|
||||
return;
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr || count <= 0 || g_reservedCount <= 0)
|
||||
return;
|
||||
|
||||
int nulls = 0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (*reinterpret_cast<const char *const *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
if (Game::Read<const char *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME) == nullptr)
|
||||
++nulls;
|
||||
|
||||
@@ -193,8 +193,8 @@ void Grow() {
|
||||
// moving the buffer would corrupt their subscription). Abort if so and
|
||||
// fall back to claim-NULL.
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint32_t head = *reinterpret_cast<const uint32_t *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
const uint32_t head = Game::Read<uint32_t>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
if (head != 0 && (head & 1) == 0) { // populated chain (not 0 / sentinel)
|
||||
Debug::Log::Printf(
|
||||
"[event] grow aborted — chain live at slot %d (not first-reg)", i);
|
||||
@@ -231,9 +231,9 @@ void Grow() {
|
||||
// Publish base BEFORE count so the globals never advertise more entries
|
||||
// than the buffer holds. Then free the OLD buffer only — the name
|
||||
// strings live on in newBuf and are freed by the reload teardown.
|
||||
*reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR) = newBuf;
|
||||
*reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_CAP) = newCount;
|
||||
*reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT) = newCount;
|
||||
Game::Ref<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) = newBuf;
|
||||
Game::Ref<int>(Offsets::VAR_EVENT_TABLE_CAP) = newCount;
|
||||
Game::Ref<int>(Offsets::VAR_EVENT_TABLE_COUNT) = newCount;
|
||||
SMemFree(base);
|
||||
|
||||
Debug::Log::Printf("[event] grew table %d -> %d (%d NULL, %d reserved%s)",
|
||||
@@ -268,13 +268,13 @@ int Lookup(const char *name) {
|
||||
int LookupByName(const char *name) {
|
||||
if (name == nullptr)
|
||||
return -1;
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const char *entryName = *reinterpret_cast<const char *const *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
const char *entryName = Game::Read<const char *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (entryName != nullptr && std::strcmp(entryName, name) == 0)
|
||||
return i;
|
||||
@@ -285,16 +285,16 @@ int LookupByName(const char *name) {
|
||||
bool HasListeners(int slot) {
|
||||
if (slot < 0)
|
||||
return false;
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || slot >= count)
|
||||
return false;
|
||||
// The entry's chain head (`+0x0C`): a populated subscriber chain is a
|
||||
// non-zero pointer with the low bit clear; `0` or an odd (tagged)
|
||||
// value is the empty self-sentinel — same test the grow-safety scan
|
||||
// uses. So this is true iff at least one frame registered for the event.
|
||||
const uint32_t head = *reinterpret_cast<const uint32_t *>(
|
||||
base + slot * Offsets::EVENT_ENTRY_STRIDE + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
const uint32_t head = Game::Read<uint32_t>(
|
||||
base + slot * Offsets::EVENT_ENTRY_STRIDE, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
return head != 0 && (head & 1) == 0;
|
||||
}
|
||||
|
||||
@@ -330,12 +330,12 @@ int __fastcall Script_DumpSlot(void *L) {
|
||||
return 0;
|
||||
}
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
const char *name = nullptr;
|
||||
if (base != nullptr && slot >= 0 && slot < count) {
|
||||
name = *reinterpret_cast<const char *const *>(
|
||||
base + slot * Offsets::EVENT_ENTRY_STRIDE +
|
||||
name = Game::Read<const char *>(
|
||||
base + slot * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
}
|
||||
Debug::Log::Printf("[dumpslot] base=0x%08X count=%d slot=%d name='%s'",
|
||||
@@ -345,16 +345,16 @@ int __fastcall Script_DumpSlot(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_DumpAllEvents(void *) {
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
Debug::Log::Printf("[dumpall] base=0x%08X count=%d",
|
||||
static_cast<unsigned>(reinterpret_cast<uintptr_t>(base)),
|
||||
count);
|
||||
if (base == nullptr)
|
||||
return 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
const char *name = Game::Read<const char *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name != nullptr && *name != '\0')
|
||||
Debug::Log::Printf("[%d] %s", i, name);
|
||||
|
||||
@@ -50,22 +50,20 @@ int __fastcall Script_GetFramesRegisteredForEvent(void *L) {
|
||||
if (slot < 0)
|
||||
return 0;
|
||||
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr)
|
||||
return 0;
|
||||
|
||||
const uint8_t *entry = base + slot * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const uintptr_t head = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
const uintptr_t head = Game::Read<uintptr_t>(entry, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
|
||||
// First pass: count subscribers so we can reserve stack up front — an
|
||||
// event with many registered frames would otherwise risk overflowing
|
||||
// the default Lua stack slack as we push.
|
||||
int count = 0;
|
||||
for (uintptr_t node = head; (node & 1) == 0 && node != 0;
|
||||
node = *reinterpret_cast<const uintptr_t *>(
|
||||
node + Offsets::OFF_EVENT_NODE_NEXT)) {
|
||||
if (*reinterpret_cast<void *const *>(node + Offsets::OFF_EVENT_NODE_FRAME))
|
||||
node = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT)) {
|
||||
if (Game::Read<void *>(node + Offsets::OFF_EVENT_NODE_FRAME))
|
||||
++count;
|
||||
}
|
||||
if (count == 0)
|
||||
@@ -74,10 +72,8 @@ int __fastcall Script_GetFramesRegisteredForEvent(void *L) {
|
||||
|
||||
int pushed = 0;
|
||||
for (uintptr_t node = head; (node & 1) == 0 && node != 0;
|
||||
node = *reinterpret_cast<const uintptr_t *>(
|
||||
node + Offsets::OFF_EVENT_NODE_NEXT)) {
|
||||
void *frame = *reinterpret_cast<void *const *>(
|
||||
node + Offsets::OFF_EVENT_NODE_FRAME);
|
||||
node = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT)) {
|
||||
void *frame = Game::Read<void *>(node + Offsets::OFF_EVENT_NODE_FRAME);
|
||||
if (frame != nullptr) {
|
||||
UI::FrameObject::Push(L, frame);
|
||||
++pushed;
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
// 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.
|
||||
|
||||
// FontString metric backports.
|
||||
//
|
||||
// 1.12's FontString method table (32 entries at 0x0087C1D8, registry context
|
||||
// VAR_FONTSTRING_METHOD_REGISTRY) ships GetStringWidth but none of the later
|
||||
// metric bindings. The engine already has every internal getter — these are
|
||||
// the thin Lua bindings Blizzard never wrote for 1.12:
|
||||
//
|
||||
// GetStringHeight() — 2.3.0 binding; internal getter
|
||||
// FUN_FONTSTRING_STRING_HEIGHT (wrap-aware,
|
||||
// lines×fontH + (lines−1)×spacing, cache
|
||||
// fs+0x100, 0 for empty text).
|
||||
// GetUnboundedStringWidth() — the substring measure with the WHOLE string
|
||||
// (FUN_FONTSTRING_MEASURE_SUBSTRING, len 0 =
|
||||
// strlen): the string's width with no wrap cap.
|
||||
// Icon-aware for free — the call routes through
|
||||
// text/InlineTexture.cpp's co-hook.
|
||||
// GetWrappedWidth() — the RENDERED width: the widest wrapped line
|
||||
// (the tooltip auto-size's own recipe — break
|
||||
// array + per-segment substring measures).
|
||||
// GetNumLines() — the node's built line count (render truth,
|
||||
// OFF_TEXT_NODE_BUILT_LINES) once painted; for
|
||||
// an unbuilt node, the engine's own break-array
|
||||
// computer (FUN_FONTSTRING_BREAK_ARRAY — also
|
||||
// icon-aware via the wrap-stepper co-hook).
|
||||
// GetLineHeight() — the fs font height via the same getter the
|
||||
// rebuild feeds the text node (FUN_007727b0);
|
||||
// excludes spacing, like retail (GetSpacing is
|
||||
// its own method).
|
||||
// IsTruncated() — did the engine cut the text off with an
|
||||
// ellipsis? Reads the render's own output: the
|
||||
// built text node's copy of the laid-out string
|
||||
// (OFF_TEXT_NODE_TEXT) vs the source. Same
|
||||
// "displayed vs full" comparison retail does; no
|
||||
// reconstruction of the fit/wrap/maxLines logic.
|
||||
// Reflects the last rendered layout (the display-
|
||||
// text resolver's rect is zero until a render, so
|
||||
// a Lua-time re-call cannot see the bound).
|
||||
// SetMaxLines(n)/GetMaxLines() — cap the fontstring to n wrapped lines; text
|
||||
// past the cap is ellipsized. Writes the engine's
|
||||
// own per-fontstring maxLines field (fs+0x128,
|
||||
// already honored by the truncation resolver) and
|
||||
// re-invalidates the layout like SetText does.
|
||||
// 1.12 has no SetWordWrap, so SetMaxLines(1) is
|
||||
// how you get a single-line, ellipsized label.
|
||||
// SetFormattedText(fmt,...) — string.format + SetText convenience. The format
|
||||
// runs through the engine's OWN str_format C
|
||||
// function (FUN_LUA_STR_FORMAT) under pcall — NOT
|
||||
// _G.string.format, so an addon replacing
|
||||
// string.format can't hijack it (matches retail).
|
||||
// The set goes through FUN_FONTSTRING_SET_TEXT,
|
||||
// keeping the sanitizer/pipe handling identical to
|
||||
// SetText.
|
||||
//
|
||||
// Push conversion mirrors Script_GetStringWidth (0x0079E510): resolve self,
|
||||
// call the internal getter, convert anchor units → UI pixels, push.
|
||||
//
|
||||
// No inline-icon adjustment lives HERE: the width/height internal getters are
|
||||
// already icon-adjusted by their co-hooks (calling the hooked VA routes
|
||||
// through the detour), and icons never change GetLineHeight.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace FontString::Metrics {
|
||||
namespace {
|
||||
|
||||
// FUN_FONTSTRING_STRING_HEIGHT — ECX = fs, no stack args, float in x87 ST0.
|
||||
using StringHeightInternal_t = float(__fastcall *)(void *fs);
|
||||
// FUN_FONTSTRING_MEASURE_SUBSTRING — __thiscall(fs, text, len); dummy-EDX form.
|
||||
using MeasureSubstring_t = float(__fastcall *)(void *fs, void *edx, const char *text, int len);
|
||||
// FUN_FONTSTRING_FONT_HEIGHT — __thiscall(fs, mode-on-stack); dummy-EDX form.
|
||||
using FsFontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
|
||||
// FUN_FONTSTRING_BREAK_ARRAY — __thiscall(fs, text, wrapWidth, outBreaks, cap).
|
||||
using BreakArray_t = uint32_t(__fastcall *)(void *fs, void *edx, const char *text, float width,
|
||||
int *outBreaks, int cap);
|
||||
// FUN_FONTSTRING_SET_TEXT — __thiscall(fs, text, flag = 0); dummy-EDX form.
|
||||
using FsSetText_t = void(__fastcall *)(void *fs, void *edx, const char *text, int flag);
|
||||
// FUN_HANDLE_RELEASE — DecRef a ref-counted handle. __fastcall(handle).
|
||||
using HandleRelease_t = void(__fastcall *)(void *handle);
|
||||
// FUN_FONTSTRING_LAYOUT_INVALIDATE — region layout invalidate. __thiscall(region,
|
||||
// int); dummy-EDX form.
|
||||
using LayoutInvalidate_t = void(__fastcall *)(void *region, void *edx, int arg);
|
||||
|
||||
// Invalidate a fontstring's layout exactly as SetText (FUN_00771D80) does after
|
||||
// a content change, so a layout-affecting property change (max lines) takes
|
||||
// effect on the next draw: drop the cached measures, release the built node,
|
||||
// clear the layout-dirty bit, and notify the region to re-lay-out. Re-calling
|
||||
// SetText would NOT work — it early-outs when the text string is unchanged.
|
||||
void InvalidateLayout(void *fs) {
|
||||
Game::Ref<float>(fs, Offsets::OFF_FONTSTRING_WIDTH_CACHE) = 0.0f;
|
||||
Game::Ref<float>(fs, Offsets::OFF_FONTSTRING_HEIGHT_CACHE) = 0.0f;
|
||||
void *block = Game::Read<void *>(fs, Offsets::OFF_FONTSTRING_TEXT_BLOCK);
|
||||
if (block != nullptr) {
|
||||
reinterpret_cast<HandleRelease_t>(Offsets::FUN_HANDLE_RELEASE)(block);
|
||||
Game::Ref<void *>(fs, Offsets::OFF_FONTSTRING_TEXT_BLOCK) = nullptr;
|
||||
}
|
||||
Game::Ref<uint32_t>(fs, Offsets::OFF_FONTSTRING_DIRTY_FLAGS) &= ~1u;
|
||||
void *region = reinterpret_cast<void *>(reinterpret_cast<uintptr_t>(fs) + 0x24);
|
||||
reinterpret_cast<LayoutInvalidate_t>(Offsets::FUN_FONTSTRING_LAYOUT_INVALIDATE)(
|
||||
region, nullptr, 0);
|
||||
}
|
||||
|
||||
// Anchor units → UI pixels: `FUN_0041AE40(FUN_0041AD70() × DAT_007FFD68 × v)`
|
||||
// = v × [VAR_UI_COORD_SCALE_DIV] × 1024 / [VAR_UI_COORD_SCALE_MUL] — the exact
|
||||
// push chain Script_GetStringWidth uses (the inverse of
|
||||
// Tooltip::LinePool::PixelToInternal).
|
||||
double InternalToPixel(float v) {
|
||||
const float mul = Game::Read<float>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float div = Game::Read<float>(Offsets::VAR_UI_COORD_SCALE_DIV);
|
||||
if (mul == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(v) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
}
|
||||
|
||||
// The fs's wrap budget in the units FUN_FONTSTRING_BREAK_ARRAY expects
|
||||
// (width-getter units — it multiplies by fs+0x7C itself before gxu, verified
|
||||
// decompile), or 0 when the rect is unresolved. rectWidth ÷ scale, calibrated
|
||||
// EMPIRICALLY against the render with a three-point probe (a 100px box at
|
||||
// scale ~0.91, text whose candidate lines measure 78.1 / 94.6 / 101.6px):
|
||||
// raw rectW → effective budget ~91px → broke one word EARLY,
|
||||
// rectW/scale² → effective budget ~110px → broke one word LATE,
|
||||
// rectW/scale → effective budget 100px → breaks exactly where the
|
||||
// render does (94.6 ≤ 100 < 101.6).
|
||||
// All three observations fit budget_px = pxOf(param) × scale, pinning the
|
||||
// single ÷scale. (A pure paper derivation from the rebuild's node budget
|
||||
// predicted /scale² — one hop of that chain models the live behaviour wrong;
|
||||
// the probe is the arbiter.)
|
||||
float WrapBudget(void *fs) {
|
||||
const float *rc = Game::Ptr<const float>(fs, Offsets::OFF_REGION_RECT);
|
||||
float width = (rc[3] > rc[1]) ? (rc[3] - rc[1]) : (rc[1] - rc[3]);
|
||||
const float s = Game::Read<float>(fs, Offsets::OFF_LAYOUT_SCALE);
|
||||
if (width > 0.0f && s > 0.0f)
|
||||
width /= s;
|
||||
return width;
|
||||
}
|
||||
|
||||
// Shared self-resolve for every method here.
|
||||
void *ResolveSelf(void *L, const char *usage) {
|
||||
void *fs = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
fs = Game::Lua::ResolveObject(L, 1);
|
||||
if (fs == nullptr)
|
||||
Game::Lua::Error(L, "%s", usage);
|
||||
return fs;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetStringHeight(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:GetStringHeight()");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
const float h = reinterpret_cast<StringHeightInternal_t>(
|
||||
Offsets::FUN_FONTSTRING_STRING_HEIGHT)(fs);
|
||||
Game::Lua::PushNumber(L, InternalToPixel(h));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetUnboundedStringWidth(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:GetUnboundedStringWidth()");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
const char *text = Game::Read<const char *>(fs, Offsets::OFF_FONTSTRING_TEXT);
|
||||
if (text == nullptr || *text == '\0') {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
const float w = reinterpret_cast<MeasureSubstring_t>(
|
||||
Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(fs, nullptr, text, 0);
|
||||
Game::Lua::PushNumber(L, InternalToPixel(w));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetNumLines(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:GetNumLines()");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
const char *text = Game::Read<const char *>(fs, Offsets::OFF_FONTSTRING_TEXT);
|
||||
if (text == nullptr || *text == '\0') {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
// Render truth first: the built node's line count. Zero until the node
|
||||
// paints once (hidden fs, mid-rebuild) — fall back below.
|
||||
void *block = Game::Read<void *>(fs, Offsets::OFF_FONTSTRING_TEXT_BLOCK);
|
||||
if (block != nullptr) {
|
||||
void *node = Game::Read<void *>(block, Offsets::OFF_TEXTBLOCK_NODE);
|
||||
if (node != nullptr) {
|
||||
const int built = Game::Read<int>(node, Offsets::OFF_TEXT_NODE_BUILT_LINES);
|
||||
if (built > 0) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(built));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unbuilt: the engine's own break-array computer, fed the fs's wrap
|
||||
// budget (see WrapBudget's scale note). An unresolved rect (0 width)
|
||||
// means no wrap constraint exists yet — one line, like a fresh
|
||||
// single-anchor fs.
|
||||
const float width = WrapBudget(fs);
|
||||
if (width <= 0.0f) {
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
return 1;
|
||||
}
|
||||
int breaks[64];
|
||||
uint32_t lines = reinterpret_cast<BreakArray_t>(Offsets::FUN_FONTSTRING_BREAK_ARRAY)(
|
||||
fs, nullptr, text, width, breaks, 64);
|
||||
if (lines > 64u)
|
||||
lines = 64u;
|
||||
if (lines == 0u)
|
||||
lines = 1u;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(lines));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetWrappedWidth(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:GetWrappedWidth()");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
const char *text = Game::Read<const char *>(fs, Offsets::OFF_FONTSTRING_TEXT);
|
||||
if (text == nullptr || *text == '\0') {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
auto measure =
|
||||
reinterpret_cast<MeasureSubstring_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING);
|
||||
// The fs's wrap budget (see WrapBudget's scale note). Without one
|
||||
// (unresolved rect) nothing wraps, and the rendered width IS the
|
||||
// one-line width.
|
||||
const float budget = WrapBudget(fs);
|
||||
if (budget <= 0.0f) {
|
||||
Game::Lua::PushNumber(L, InternalToPixel(measure(fs, nullptr, text, 0)));
|
||||
return 1;
|
||||
}
|
||||
// The tooltip auto-size's own recipe (FUN_00530640): break positions from
|
||||
// the break-array computer, then the widest per-segment measure. Both
|
||||
// callees are icon-aware through their co-hooks, and a segment-final icon
|
||||
// reports its ink edge — so this is the rendered width, wrap and icons
|
||||
// included.
|
||||
int breaks[64];
|
||||
uint32_t segs = reinterpret_cast<BreakArray_t>(Offsets::FUN_FONTSTRING_BREAK_ARRAY)(
|
||||
fs, nullptr, text, budget, breaks, 64);
|
||||
if (segs > 64u)
|
||||
segs = 64u;
|
||||
if (segs <= 1u) {
|
||||
Game::Lua::PushNumber(L, InternalToPixel(measure(fs, nullptr, text, 0)));
|
||||
return 1;
|
||||
}
|
||||
int len = 0;
|
||||
while (len < 0x4000 && text[len] != '\0')
|
||||
++len;
|
||||
float widest = 0.0f;
|
||||
for (uint32_t i = 0; i < segs; ++i) {
|
||||
const int start = breaks[i];
|
||||
int end = (i + 1 < segs) ? breaks[i + 1] : len;
|
||||
if (start < 0 || start >= end || end > len)
|
||||
continue;
|
||||
// A segment runs to the NEXT segment's start, which includes the
|
||||
// whitespace the wrap consumed — the render never draws it, and a
|
||||
// trailing space also flips the last real letter from ink to
|
||||
// full-advance treatment (+~1.5px, measured). Trim to what renders.
|
||||
while (end > start && (text[end - 1] == ' ' || text[end - 1] == '\n' ||
|
||||
text[end - 1] == '\r' || text[end - 1] == '\t'))
|
||||
--end;
|
||||
if (start >= end)
|
||||
continue;
|
||||
const float w = measure(fs, nullptr, text + start, end - start);
|
||||
if (w > widest)
|
||||
widest = w;
|
||||
}
|
||||
Game::Lua::PushNumber(L, InternalToPixel(widest));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetLineHeight(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:GetLineHeight()");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
const float h = reinterpret_cast<FsFontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(
|
||||
fs, nullptr, 1);
|
||||
Game::Lua::PushNumber(L, InternalToPixel(h));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsTruncated(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:IsTruncated()");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
// Retail computes this by running the display-text resolver and strcmp'ing
|
||||
// its result against the source (5.4.8 inner fn 0x0045E001). 1.12's resolver
|
||||
// needs the LAID-OUT rect (fs+0x64), which is zero until a render pass, so a
|
||||
// Lua-time re-call always sees an unbounded box and never truncates. Read the
|
||||
// render's OWN output instead: the built text node keeps a copy of exactly
|
||||
// the string it laid out (OFF_TEXT_NODE_TEXT) — the ellipsized "<prefix>..."
|
||||
// form on truncation, else the source verbatim. So this reflects the last
|
||||
// rendered layout (the fontstring must have drawn once), matching retail's
|
||||
// "displayed vs full" semantics.
|
||||
bool truncated = false;
|
||||
void *block = Game::Read<void *>(fs, Offsets::OFF_FONTSTRING_TEXT_BLOCK);
|
||||
const char *src = Game::Read<const char *>(fs, Offsets::OFF_FONTSTRING_TEXT);
|
||||
if (block != nullptr && src != nullptr) {
|
||||
void *node = Game::Read<void *>(block, Offsets::OFF_TEXTBLOCK_NODE);
|
||||
const char *shown =
|
||||
(node != nullptr) ? Game::Read<const char *>(node, Offsets::OFF_TEXT_NODE_TEXT)
|
||||
: nullptr;
|
||||
if (shown != nullptr) {
|
||||
// A truncated display is exactly <source-prefix> + "...". Require the
|
||||
// "..." suffix, a length strictly shorter than the source, and a
|
||||
// matching byte-prefix. This rejects a STALE node — one left from a
|
||||
// previous, different text after a fresh SetText, not yet re-laid-out
|
||||
// — which would otherwise read as a truncation of the new text.
|
||||
const size_t nlen = std::strlen(shown);
|
||||
const size_t slen = std::strlen(src);
|
||||
if (nlen >= 3 && nlen < slen && shown[nlen - 3] == '.' &&
|
||||
shown[nlen - 2] == '.' && shown[nlen - 1] == '.' &&
|
||||
std::memcmp(shown, src, nlen - 3) == 0)
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushBool(L, truncated);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetMaxLines(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:SetMaxLines(maxLines)");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
// <= 0 or a non-number means "no cap" — the engine treats 0 as unlimited.
|
||||
int maxLines = 0;
|
||||
if (Game::Lua::IsNumber(L, 2)) {
|
||||
const double n = Game::Lua::ToNumber(L, 2);
|
||||
if (n > 0.0)
|
||||
maxLines = static_cast<int>(n);
|
||||
}
|
||||
if (maxLines != Game::Read<int>(fs, Offsets::OFF_FONTSTRING_MAX_LINES)) {
|
||||
Game::Ref<int>(fs, Offsets::OFF_FONTSTRING_MAX_LINES) = maxLines;
|
||||
InvalidateLayout(fs);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMaxLines(void *L) {
|
||||
void *fs = ResolveSelf(L, "Usage: fontstring:GetMaxLines()");
|
||||
if (fs == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(Game::Read<int>(fs, Offsets::OFF_FONTSTRING_MAX_LINES)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetFormattedText(void *L) {
|
||||
void *fs = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
fs = Game::Lua::ResolveObject(L, 1);
|
||||
if (fs == nullptr || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: fontstring:SetFormattedText(\"format\"[, ...])");
|
||||
return 0;
|
||||
}
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::CheckStack(L, top + 4);
|
||||
Game::Lua::PushCClosure(
|
||||
L, reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_FORMAT), 0);
|
||||
for (int i = 2; i <= top; ++i)
|
||||
Game::Lua::PushValue(L, i);
|
||||
if (Game::Lua::PCall(L, top - 1, 1, 0) != 0) {
|
||||
const char *msg = Game::Lua::ToString(L, -1);
|
||||
Game::Lua::Error(L, "%s", (msg != nullptr) ? msg : "SetFormattedText: format failed");
|
||||
return 0;
|
||||
}
|
||||
const char *result = Game::Lua::ToString(L, -1);
|
||||
reinterpret_cast<FsSetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(
|
||||
fs, nullptr, (result != nullptr) ? result : "", 0);
|
||||
Game::Lua::SetTop(L, top);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"GetStringHeight", &Script_GetStringHeight},
|
||||
{"GetUnboundedStringWidth", &Script_GetUnboundedStringWidth},
|
||||
{"GetWrappedWidth", &Script_GetWrappedWidth},
|
||||
{"GetNumLines", &Script_GetNumLines},
|
||||
{"GetLineHeight", &Script_GetLineHeight},
|
||||
{"IsTruncated", &Script_IsTruncated},
|
||||
{"SetMaxLines", &Script_SetMaxLines},
|
||||
{"GetMaxLines", &Script_GetMaxLines},
|
||||
{"SetFormattedText", &Script_SetFormattedText},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
} // namespace FontString::Metrics
|
||||
+70
-93
@@ -58,9 +58,11 @@
|
||||
// resolve via ReadModAttr's precedence. Verbs: `target` (with the engine's
|
||||
// default-interaction precedence — pending spell / cursor item cast/drop on the
|
||||
// unit instead of switching target), `assist`, `focus`, `spell` (reads the
|
||||
// `spell` attribute and casts it on the unit via our native C_Spell.CastAtUnit —
|
||||
// the unit's GUID goes straight to the cast dispatcher, no target juggling, and
|
||||
// ground-target spells land at the unit's feet), `item` (uses the `item`
|
||||
// `spell` attribute and casts it: with a `unit` attribute, on that unit via our
|
||||
// native C_Spell.CastAtUnit — the unit's GUID goes straight to the cast
|
||||
// dispatcher, no target juggling, and ground-target spells land at its feet;
|
||||
// with no `unit`, on the current target via the plain CastSpellByName), `item`
|
||||
// (uses the `item`
|
||||
// attribute — a name/itemID/link via C_Item.UseItemByName with the unit as the
|
||||
// use target, or a "bag slot" string like "0 1" via UseContainerItem; the
|
||||
// deprecated `bag`/`slot` attributes also work), `macro` (from the
|
||||
@@ -98,6 +100,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "cursor/Info.h"
|
||||
#include "macro/Execute.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "spell/AtUnit.h"
|
||||
@@ -299,78 +302,6 @@ bool CallTableFunc2Str(void *L, const char *table, const char *method,
|
||||
return called;
|
||||
}
|
||||
|
||||
// ---- macrotext execution (stock ChatEdit_ParseText, no addon dependency) ----
|
||||
//
|
||||
// 1.12 has no `RunMacroText`/`RunMacro` global — those are addon shims (pfUI
|
||||
// wraps `ChatEdit_ParseText`; SuperCleveRoidMacros ships its own `RunMacro`).
|
||||
// To run macro text without depending on an addon, we replicate pfUI's shim in
|
||||
// C: build a throwaway "edit box" whose `GetText` returns the line and whose
|
||||
// every other method is a harmless no-op (via an `__index` metamethod), then
|
||||
// hand it to the stock FrameXML `ChatEdit_ParseText(editBox, 1)` — the same
|
||||
// path the real chat box uses to dispatch a slash command / send a line.
|
||||
|
||||
// GetText: returns upvalue(1), the captured line.
|
||||
int __fastcall MacroGetText_c(void *L) {
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A no-op standing in for any edit-box method the parser happens to call.
|
||||
int __fastcall MacroNoop_c(void *) { return 0; }
|
||||
|
||||
// __index(tab, key): hand back the no-op so `editBox:AnyMethod()` is safe.
|
||||
int __fastcall MacroIndex_c(void *L) {
|
||||
Game::Lua::PushCClosure(L, &MacroNoop_c, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Runs a single macro line through the stock chat parser.
|
||||
void RunMacroLineC(void *L, const char *line, size_t len) {
|
||||
if (len == 0) return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::NewTable(L); // fake editBox
|
||||
const int obj = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, "GetText"); // obj.GetText = closure over line
|
||||
Game::Lua::PushLString(L, line, static_cast<unsigned int>(len));
|
||||
Game::Lua::PushCClosure(L, &MacroGetText_c, 1);
|
||||
Game::Lua::SetTable(L, obj);
|
||||
|
||||
Game::Lua::NewTable(L); // metatable { __index = noop }
|
||||
const int mt = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, "__index");
|
||||
Game::Lua::PushCClosure(L, &MacroIndex_c, 0);
|
||||
Game::Lua::SetTable(L, mt);
|
||||
|
||||
// No lua_setmetatable binding — use the Lua global.
|
||||
if (Game::Lua::PushGlobalFunction(L, "setmetatable")) {
|
||||
Game::Lua::PushValue(L, obj);
|
||||
Game::Lua::PushValue(L, mt);
|
||||
Game::Lua::Call(L, 2, 0);
|
||||
}
|
||||
|
||||
if (Game::Lua::PushGlobalFunction(L, "ChatEdit_ParseText")) {
|
||||
Game::Lua::PushValue(L, obj);
|
||||
Game::Lua::PushNumber(L, 1); // send = 1
|
||||
Game::Lua::Call(L, 2, 0);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// Runs macro text one line at a time — a real macro is line-delimited, and a
|
||||
// single ChatEdit_ParseText call only dispatches one command.
|
||||
void RunMacroTextC(void *L, const char *text) {
|
||||
if (!text) return;
|
||||
for (const char *p = text; *p;) {
|
||||
const char *nl = p;
|
||||
while (*nl && *nl != '\n') ++nl;
|
||||
RunMacroLineC(L, p, static_cast<size_t>(nl - p));
|
||||
p = (*nl == '\n') ? nl + 1 : nl;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- mouse-focus poll (drives the native mouseover slot) -------------------
|
||||
|
||||
// frame CFrameScriptObject* → current `unit` token, for the mouse-focus poll.
|
||||
@@ -384,12 +315,11 @@ std::unordered_map<const void *, std::string> g_unitByFrame;
|
||||
uint64_t g_lastSet = 0;
|
||||
|
||||
const void *CurrentMouseFocus() {
|
||||
const void *ctx = *reinterpret_cast<const void *const *>(
|
||||
const void *ctx = Game::Read<const void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
if (ctx == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(ctx) + Offsets::OFF_UI_CONTEXT_MOUSE_FOCUS);
|
||||
return Game::Read<const void *>(ctx, Offsets::OFF_UI_CONTEXT_MOUSE_FOCUS);
|
||||
}
|
||||
|
||||
// The engine's real mouseover setter — `__stdcall(guidLo, guidHi, prevLo,
|
||||
@@ -422,7 +352,7 @@ bool GuidHasLiveUnit(uint64_t guid) {
|
||||
// works for an offline member). The engine's setter (FUN_00492890) gates this
|
||||
// call on a live object, so offline members get nothing — we do it here.
|
||||
void BuildRosterUnitTooltip(uint64_t guid) {
|
||||
void *tooltip = *reinterpret_cast<void *const *>(
|
||||
void *tooltip = Game::Read<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_GAMETOOLTIP_OBJECT_PTR));
|
||||
if (tooltip == nullptr)
|
||||
return;
|
||||
@@ -434,9 +364,8 @@ void BuildRosterUnitTooltip(uint64_t guid) {
|
||||
// no owner and never re-appears (only a subsequent live-unit hover, which
|
||||
// takes the engine's own anchor path, would). Use the self-contained
|
||||
// invoker FUN_FRAME_INVOKE_SCRIPT, safe to call from the WorldTick.
|
||||
const int anchorHandler = *reinterpret_cast<const int *>(
|
||||
reinterpret_cast<uint8_t *>(tooltip) +
|
||||
Offsets::OFF_TOOLTIP_SET_DEFAULT_ANCHOR_HANDLER);
|
||||
const int anchorHandler = Game::Read<int>(
|
||||
tooltip, Offsets::OFF_TOOLTIP_SET_DEFAULT_ANCHOR_HANDLER);
|
||||
if (anchorHandler != 0)
|
||||
reinterpret_cast<void(__fastcall *)(int, void *)>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAME_INVOKE_SCRIPT))(anchorHandler,
|
||||
@@ -620,11 +549,18 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "spell")) {
|
||||
if (!unit) return false;
|
||||
char spell[128];
|
||||
if (!ReadModAttr(L, fi, prefix, "spell", suffix, spell, sizeof spell))
|
||||
return false;
|
||||
Spell::AtUnit::CastByName(spell, unit);
|
||||
// With a `unit`, cast straight at it (GUID → dispatcher, no target
|
||||
// juggling; a ground-target spell lands at its feet). With no `unit`,
|
||||
// fall back to the plain global cast on the current target — a
|
||||
// `type="spell"` button that sets no `unit` attribute (issue #18) now
|
||||
// works like `/cast <spell>` instead of doing nothing.
|
||||
if (unit)
|
||||
Spell::AtUnit::CastByName(spell, unit);
|
||||
else
|
||||
Game::Lua::CallGlobalString(L, "CastSpellByName", spell);
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "item")) {
|
||||
@@ -646,16 +582,57 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
return false;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "macro")) {
|
||||
// Give the macro a unit to act on. Vanilla macros have no `@unit`
|
||||
// conditional, and unlike the `spell` verb (which feeds a GUID straight
|
||||
// to the cast dispatcher) we can't inject a target into arbitrary macro
|
||||
// text — so a plain `/cast Flash Heal` would hit the current target, not
|
||||
// the clicked unit. Emulate the classic Clique click-heal: snapshot the
|
||||
// current target, retarget the clicked unit, run the macro, then restore
|
||||
// the previous target so the click doesn't leave the player retargeted.
|
||||
// The macro's actions (/cast etc.) dispatch against the clicked unit
|
||||
// synchronously while it's selected, so restoring the target afterward
|
||||
// doesn't affect them.
|
||||
uint64_t prevTarget = 0;
|
||||
const bool swapped = unit != nullptr;
|
||||
if (swapped) {
|
||||
prevTarget =
|
||||
(static_cast<uint64_t>(Game::Read<volatile uint32_t>(
|
||||
Offsets::VAR_CURRENT_SELECTION_GUID_HI))
|
||||
<< 32) |
|
||||
Game::Read<volatile uint32_t>(
|
||||
Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
Game::Lua::CallGlobalString(L, "TargetUnit", unit);
|
||||
}
|
||||
char macro[512];
|
||||
if (!ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro) &&
|
||||
!ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro))
|
||||
return false;
|
||||
// Prefer an addon-provided RunMacro (SuperCleveRoidMacros, pfUI, …) — it
|
||||
// handles named macros and extended macro text; fall back to the stock
|
||||
// ChatEdit_ParseText path when no RunMacro global is present.
|
||||
if (!Game::Lua::CallGlobalString(L, "RunMacro", macro))
|
||||
RunMacroTextC(L, macro);
|
||||
return true;
|
||||
bool handled = false;
|
||||
// `macrotext` is raw macro text (the modern default). Run it as text
|
||||
// through the stock chat parser so each line dispatches via
|
||||
// SlashCmdList — that routes /cast etc. through any addon slash hooks
|
||||
// (e.g. SuperCleveRoidMacros' conditional /cast). A name-based RunMacro
|
||||
// (which is what SuperCleveRoidMacros ships) can't interpret text and
|
||||
// would silently resolve nothing.
|
||||
if (ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro)) {
|
||||
Macro::Execute::Text(L, macro);
|
||||
handled = true;
|
||||
}
|
||||
// Deprecated `macro` form: a saved-macro name/index. Prefer an
|
||||
// addon-provided RunMacro (SuperCleveRoidMacros, pfUI, …) to run the
|
||||
// named macro's body; fall back to the stock parser when none exists.
|
||||
else if (ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro)) {
|
||||
handled = Macro::Execute::Saved(L, macro);
|
||||
}
|
||||
// Restore the target the click swapped away from. `FUN_TARGET_BY_GUID`
|
||||
// validates the GUID resolves to a live unit and bails otherwise, so a
|
||||
// target that despawned mid-macro is dropped cleanly rather than
|
||||
// committed; a zero prior target means "no target" → ClearTarget.
|
||||
if (swapped) {
|
||||
if (prevTarget)
|
||||
reinterpret_cast<void(__fastcall *)(const uint64_t *)>(
|
||||
Offsets::FUN_TARGET_BY_GUID)(&prevTarget);
|
||||
else
|
||||
Game::Lua::CallGlobal(L, "ClearTarget");
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "stop") || Ascii::EqualCI(verb, "stopcasting")) {
|
||||
Game::Lua::CallGlobal(L, "SpellStopCasting");
|
||||
|
||||
+11
-17
@@ -131,8 +131,7 @@ int __fastcall Script_IsMouseOver(void *L) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const float effScale = *reinterpret_cast<const float *>(
|
||||
reinterpret_cast<const uint8_t *>(obj) + Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
const float effScale = Game::Read<float>(obj, Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
|
||||
double left, right, top, bottom;
|
||||
if (effScale == 0.0f ||
|
||||
@@ -193,13 +192,10 @@ int __fastcall Script_GetRect(void *L) {
|
||||
// there, so it reports false, as it should.
|
||||
int __fastcall Script_IsDragging(void *L) {
|
||||
void *self = Game::Lua::ResolveObject(L, 1);
|
||||
void *ctx = *reinterpret_cast<void *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
void *ctx = Game::Read<void *>(static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
void *dragTarget =
|
||||
ctx != nullptr
|
||||
? *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(ctx) +
|
||||
Offsets::OFF_UI_CONTEXT_DRAG_TARGET)
|
||||
? Game::Read<void *>(ctx, Offsets::OFF_UI_CONTEXT_DRAG_TARGET)
|
||||
: nullptr;
|
||||
Game::Lua::PushBool(L, self != nullptr && self == dragTarget);
|
||||
return 1;
|
||||
@@ -348,9 +344,9 @@ int __fastcall Script_HookScript(void *L) {
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
bool FrameHasEvent(void *frame, const char *eventName) {
|
||||
const uint32_t count = *reinterpret_cast<const uint32_t *>(
|
||||
const uint32_t count = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_COUNT));
|
||||
auto *base = *reinterpret_cast<const uint8_t *const *>(
|
||||
auto *base = Game::Read<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_BASE_PTR));
|
||||
if (count == 0 || base == nullptr)
|
||||
return false;
|
||||
@@ -360,21 +356,19 @@ bool FrameHasEvent(void *frame, const char *eventName) {
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
const char *name = Game::Read<const char *>(
|
||||
entry, Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
if (sstrcmpi(name, eventName, 0x7FFFFFFF) != 0)
|
||||
continue;
|
||||
// Names are unique, so this is the one entry — walk its chain.
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
uintptr_t node = Game::Read<uintptr_t>(
|
||||
entry, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
while ((node & 1) == 0 && node != 0) {
|
||||
if (*reinterpret_cast<void *const *>(
|
||||
node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
|
||||
if (Game::Read<void *>(node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
|
||||
return true;
|
||||
node = *reinterpret_cast<const uintptr_t *>(
|
||||
node + Offsets::OFF_EVENT_NODE_NEXT);
|
||||
node = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+6
-12
@@ -54,8 +54,7 @@ int CallScript(uintptr_t fn, void *L) {
|
||||
|
||||
// The UI context is a pointer stored at VAR_UI_CONTEXT_PTR (null pre-world).
|
||||
void *UIContext() {
|
||||
return *reinterpret_cast<void *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
return Game::Read<void *>(static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
}
|
||||
|
||||
// Strata name -> draw rank, low (behind) to high (on top). Unknown -> 0.
|
||||
@@ -110,8 +109,7 @@ Cursor ReadCursor(void *L) {
|
||||
|
||||
// With `self` at Lua idx 1, is the cursor within the frame's rect?
|
||||
bool CursorOverSelf(void *L, void *obj, const Cursor &cur) {
|
||||
const float effScale = *reinterpret_cast<const float *>(
|
||||
reinterpret_cast<const uint8_t *>(obj) + Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
const float effScale = Game::Read<float>(obj, Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
if (effScale == 0.0f)
|
||||
return false; // unpositioned region — no rect
|
||||
double left, right, top, bottom;
|
||||
@@ -141,19 +139,16 @@ int __fastcall Script_GetMouseFoci(void *L) {
|
||||
|
||||
// The engine's authoritative topmost focus — the frame GetMouseFocus
|
||||
// returns. Forced to index 1 below.
|
||||
void *focus = *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<uint8_t *>(ctx) + Offsets::OFF_UI_CONTEXT_MOUSE_FOCUS);
|
||||
void *focus = Game::Read<void *>(ctx, Offsets::OFF_UI_CONTEXT_MOUSE_FOCUS);
|
||||
|
||||
const Cursor cur = ReadCursor(L);
|
||||
|
||||
std::vector<Hit> hits;
|
||||
void *node = *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<uint8_t *>(ctx) + Offsets::OFF_UI_CONTEXT_FRAME_LIST_HEAD);
|
||||
void *node = Game::Read<void *>(ctx, Offsets::OFF_UI_CONTEXT_FRAME_LIST_HEAD);
|
||||
while (node != nullptr && (reinterpret_cast<uintptr_t>(node) & 1) == 0) {
|
||||
// Read the next link before any Lua work (the walk is engine-list
|
||||
// based, independent of the Lua stack, but this keeps it obvious).
|
||||
void *next = *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<uint8_t *>(node) + Offsets::OFF_FRAME_ENUM_NEXT);
|
||||
void *next = Game::Read<void *>(node, Offsets::OFF_FRAME_ENUM_NEXT);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
UI::FrameObject::Push(L, node); // (self)
|
||||
@@ -164,8 +159,7 @@ int __fastcall Script_GetMouseFoci(void *L) {
|
||||
Game::Lua::SetTop(L, 1);
|
||||
CallScript(Offsets::FUN_SCRIPT_FRAME_GET_STRATA, L); // (self, strata)
|
||||
const int rank = StrataRank(Game::Lua::ToString(L, 2));
|
||||
const int level = *reinterpret_cast<const int *>(
|
||||
reinterpret_cast<uint8_t *>(node) + Offsets::OFF_FRAME_LEVEL);
|
||||
const int level = Game::Read<int>(node, Offsets::OFF_FRAME_LEVEL);
|
||||
hits.push_back(Hit{node, rank, level});
|
||||
}
|
||||
|
||||
|
||||
@@ -77,11 +77,10 @@ const auto RawGetI = reinterpret_cast<RawGetI_t>(Offsets::LUA_RAWGETI);
|
||||
// Returns the frame's Lua object ref (frame+0x08), lazily creating it exactly
|
||||
// as the engine runner does (`if (refcount == 0) ScriptRegister(frame, 0)`).
|
||||
int EnsureLuaRef(void *frame) {
|
||||
auto *f = reinterpret_cast<uint8_t *>(frame);
|
||||
if (*reinterpret_cast<int *>(f + Offsets::OFF_COBJECT_LUA_REFCOUNT) == 0)
|
||||
if (Game::Read<int>(frame, Offsets::OFF_COBJECT_LUA_REFCOUNT) == 0)
|
||||
reinterpret_cast<ScriptRegister_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER)(frame, nullptr);
|
||||
return *reinterpret_cast<int *>(f + Offsets::OFF_COBJECT_LUA_REF);
|
||||
return Game::Read<int>(frame, Offsets::OFF_COBJECT_LUA_REF);
|
||||
}
|
||||
|
||||
// Writes "arg" + k (k in 1..19) into buf; buf must hold at least 6 bytes.
|
||||
@@ -193,9 +192,7 @@ bool RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr)
|
||||
// as the first positional to mirror retail's (self, event, arg1..argN).
|
||||
const bool isOnEvent =
|
||||
haveThis &&
|
||||
*reinterpret_cast<const int *>(reinterpret_cast<uint8_t *>(frame) +
|
||||
Offsets::OFF_FRAME_ONEVENT_SLOT) ==
|
||||
handlerRef;
|
||||
Game::Read<int>(frame, Offsets::OFF_FRAME_ONEVENT_SLOT) == handlerRef;
|
||||
const int selfRef = haveThis ? EnsureLuaRef(frame) : 0;
|
||||
|
||||
Arg args[kMaxArgs];
|
||||
@@ -230,7 +227,7 @@ bool RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr)
|
||||
|
||||
// --- call handler(self, [event,] arg1..argN) ---------------------------
|
||||
const int callBase = GetTop(L); // == base + saved-block size
|
||||
const int errRef = *reinterpret_cast<const int *>(
|
||||
const int errRef = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
int errIdx = 0;
|
||||
if (errRef > 0) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "Notes.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -41,14 +42,14 @@ std::string g_path;
|
||||
bool g_loaded = false;
|
||||
|
||||
const char *ReadAccountName() {
|
||||
return *reinterpret_cast<const char *const *>(Offsets::VAR_ACCOUNT_NAME_PTR);
|
||||
return Game::Read<const char *>(Offsets::VAR_ACCOUNT_NAME_PTR);
|
||||
}
|
||||
|
||||
const char *ReadRealmName() {
|
||||
auto *info = *reinterpret_cast<uint8_t **>(Offsets::VAR_REALM_INFO_PTR);
|
||||
auto *info = Game::Read<uint8_t *>(Offsets::VAR_REALM_INFO_PTR);
|
||||
if (info == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(info + Offsets::OFF_REALM_INFO_NAME);
|
||||
return Game::Read<const char *>(info, Offsets::OFF_REALM_INFO_NAME);
|
||||
}
|
||||
|
||||
const char *ReadCharacterName() {
|
||||
|
||||
@@ -34,8 +34,7 @@ namespace FriendList::WhoInfo {
|
||||
namespace {
|
||||
|
||||
int WhoResultCount() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_WHO_RESULT_COUNT));
|
||||
return Game::Read<int>(Offsets::VAR_WHO_RESULT_COUNT);
|
||||
}
|
||||
|
||||
// `C_FriendList.GetNumWhoResults()` → `numWhos, totalCount`. The first is the
|
||||
@@ -44,7 +43,7 @@ int WhoResultCount() {
|
||||
// global's two returns.
|
||||
int __fastcall Script_GetNumWhoResults(void *L) {
|
||||
const int displayed = WhoResultCount();
|
||||
const int total = *reinterpret_cast<const int *>(
|
||||
const int total = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_WHO_RESULT_COUNT) + 4);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(displayed));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
@@ -67,15 +66,15 @@ int __fastcall Script_GetWhoInfo(void *L) {
|
||||
static_cast<uintptr_t>(Offsets::VAR_WHO_RESULTS) +
|
||||
static_cast<size_t>(index - 1) * Offsets::WHO_RESULT_STRIDE);
|
||||
|
||||
const char *name = reinterpret_cast<const char *>(entry + Offsets::OFF_WHO_NAME);
|
||||
const char *guild = reinterpret_cast<const char *>(entry + Offsets::OFF_WHO_GUILD);
|
||||
const int level = *reinterpret_cast<const int *>(entry + Offsets::OFF_WHO_LEVEL);
|
||||
const char *name = Game::Ptr<const char>(entry, Offsets::OFF_WHO_NAME);
|
||||
const char *guild = Game::Ptr<const char>(entry, Offsets::OFF_WHO_GUILD);
|
||||
const int level = Game::Read<int>(entry, Offsets::OFF_WHO_LEVEL);
|
||||
const uint32_t raceID =
|
||||
*reinterpret_cast<const uint32_t *>(entry + Offsets::OFF_WHO_RACE);
|
||||
Game::Read<uint32_t>(entry, Offsets::OFF_WHO_RACE);
|
||||
const uint32_t classID =
|
||||
*reinterpret_cast<const uint32_t *>(entry + Offsets::OFF_WHO_CLASS);
|
||||
Game::Read<uint32_t>(entry, Offsets::OFF_WHO_CLASS);
|
||||
const uint32_t zoneID =
|
||||
*reinterpret_cast<const uint32_t *>(entry + Offsets::OFF_WHO_ZONE);
|
||||
Game::Read<uint32_t>(entry, Offsets::OFF_WHO_ZONE);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "fullName", name); // inline; "" if empty
|
||||
|
||||
@@ -72,10 +72,10 @@ int __fastcall Script_GetGameObjectInfoByID(void *L) {
|
||||
if (rec == nullptr)
|
||||
return 0; // not cached -> nil
|
||||
|
||||
const uint32_t type = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_GAMEOBJECT_TYPE);
|
||||
const uint32_t type = Game::Read<uint32_t>(rec, Offsets::OFF_GAMEOBJECT_TYPE);
|
||||
const uint32_t displayID =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_GAMEOBJECT_DISPLAYID);
|
||||
const char *name = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_GAMEOBJECT_NAME);
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_GAMEOBJECT_DISPLAYID);
|
||||
const char *name = Game::Read<const char *>(rec, Offsets::OFF_GAMEOBJECT_NAME);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "gameObjectID", goID);
|
||||
|
||||
@@ -63,10 +63,10 @@ static int __fastcall Script_GameTooltipGetGameObject(void *L) {
|
||||
return 0;
|
||||
|
||||
const auto *base = static_cast<const uint8_t *>(tooltipObj);
|
||||
const uint32_t guidLo = *reinterpret_cast<const uint32_t *>(
|
||||
base + Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_LO);
|
||||
const uint32_t guidHi = *reinterpret_cast<const uint32_t *>(
|
||||
base + Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_HI);
|
||||
const uint32_t guidLo =
|
||||
Game::Read<uint32_t>(base, Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_LO);
|
||||
const uint32_t guidHi =
|
||||
Game::Read<uint32_t>(base, Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_HI);
|
||||
if (guidLo == 0 && guidHi == 0)
|
||||
return 0;
|
||||
|
||||
@@ -111,10 +111,10 @@ static int __fastcall Script_GameTooltipHasGameObject(void *L) {
|
||||
return 1;
|
||||
}
|
||||
const auto *base = static_cast<const uint8_t *>(tooltipObj);
|
||||
const uint32_t guidLo = *reinterpret_cast<const uint32_t *>(
|
||||
base + Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_LO);
|
||||
const uint32_t guidHi = *reinterpret_cast<const uint32_t *>(
|
||||
base + Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_HI);
|
||||
const uint32_t guidLo =
|
||||
Game::Read<uint32_t>(base, Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_LO);
|
||||
const uint32_t guidHi =
|
||||
Game::Read<uint32_t>(base, Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_HI);
|
||||
Game::Lua::PushBool(L, guidLo != 0 || guidHi != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "group/MemberStats.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Group::MemberStats {
|
||||
@@ -23,7 +24,7 @@ namespace {
|
||||
using Lookup_t = const uint8_t *(__fastcall *)(uint64_t *guid);
|
||||
|
||||
uint16_t U16(const uint8_t *p, int off) {
|
||||
return *reinterpret_cast<const uint16_t *>(p + off);
|
||||
return Game::Read<uint16_t>(p, off);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -67,13 +68,13 @@ Stats Lookup(uint64_t guid) {
|
||||
s.level = U16(e, Offsets::OFF_RAID_SLOT_LEVEL);
|
||||
s.areaId = U16(e, Offsets::OFF_RAID_SLOT_AREA_ID);
|
||||
const uint32_t flags =
|
||||
*reinterpret_cast<const uint32_t *>(e + Offsets::OFF_RAID_SLOT_FLAGS);
|
||||
Game::Read<uint32_t>(e, Offsets::OFF_RAID_SLOT_FLAGS);
|
||||
s.online = (flags & Offsets::RAID_SLOT_FLAG_ONLINE) != 0;
|
||||
s.dead = (flags & Offsets::RAID_SLOT_FLAG_DEAD) != 0;
|
||||
s.subgroup = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
e + Offsets::OFF_RAID_SLOT_SUBGROUP)) + 1;
|
||||
s.subgroup = static_cast<int>(Game::Read<uint32_t>(
|
||||
e, Offsets::OFF_RAID_SLOT_SUBGROUP)) + 1;
|
||||
s.rank = static_cast<int>(
|
||||
*reinterpret_cast<const uint32_t *>(e + Offsets::OFF_RAID_SLOT_RANK));
|
||||
Game::Read<uint32_t>(e, Offsets::OFF_RAID_SLOT_RANK));
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -93,8 +94,8 @@ const uint16_t *AuraArray(uint64_t guid) {
|
||||
if ((e[Offsets::OFF_GROUP_MEMBER_STATUS_FLAGS] &
|
||||
Offsets::GROUP_MEMBER_STATUS_ONLINE) == 0)
|
||||
return nullptr;
|
||||
return reinterpret_cast<const uint16_t *>(
|
||||
e + Offsets::OFF_GROUP_MEMBER_STATS_AURAS);
|
||||
return Game::Ptr<const uint16_t>(
|
||||
e, Offsets::OFF_GROUP_MEMBER_STATS_AURAS);
|
||||
}
|
||||
|
||||
// Raid slot — auras at +0x64, no online gate (mirrors the engine's raid
|
||||
@@ -102,8 +103,8 @@ const uint16_t *AuraArray(uint64_t guid) {
|
||||
auto slotFn = reinterpret_cast<Lookup_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GROUP_MEMBER_SLOT_LOOKUP));
|
||||
if (const uint8_t *e = slotFn(&guid)) {
|
||||
return reinterpret_cast<const uint16_t *>(
|
||||
e + Offsets::OFF_RAID_SLOT_AURAS);
|
||||
return Game::Ptr<const uint16_t>(
|
||||
e, Offsets::OFF_RAID_SLOT_AURAS);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -46,12 +46,9 @@ int __fastcall Script_GetCreatureID(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const Type type = Classify(guid);
|
||||
if (type != Type::Creature && type != Type::Pet) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t entryID = static_cast<uint32_t>((guid >> 24) & 0xFFFFFFu);
|
||||
// `Guid::CreatureEntry` handles the creature/pet type gate + the bits-24-47
|
||||
// extraction, and returns 0 for players, non-creature types, and entry 0.
|
||||
const uint32_t entryID = CreatureEntry(guid);
|
||||
if (entryID == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
|
||||
@@ -51,6 +51,13 @@ const char *FormatAsString(uint64_t guid, char *buf, std::size_t cap) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
uint32_t CreatureEntry(uint64_t guid) {
|
||||
const Type type = Classify(guid);
|
||||
if (type != Type::Creature && type != Type::Pet)
|
||||
return 0;
|
||||
return static_cast<uint32_t>((guid >> 24) & 0xFFFFFFu);
|
||||
}
|
||||
|
||||
Type Classify(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return Type::Unknown;
|
||||
|
||||
@@ -76,6 +76,14 @@ inline bool IsGameObject(uint64_t guid) { return Classify(guid) == Type::Game
|
||||
inline bool IsDynamicObject(uint64_t guid) { return Classify(guid) == Type::DynamicObject; }
|
||||
inline bool IsCorpse(uint64_t guid) { return Classify(guid) == Type::Corpse; }
|
||||
|
||||
// The creature-template / NPC entry ID packed into a creature or pet GUID
|
||||
// (bits 24-47). Vanilla puts the per-spawn counter in the low 24 bits and the
|
||||
// template entry above it. Returns 0 for any other GUID type (players,
|
||||
// gameobjects, items, the 0 sentinel) and when the field itself is 0 — the
|
||||
// engine never assigns entry 0, so 0 uniformly means "no creature template".
|
||||
// Shared by `C_CreatureInfo.GetCreatureID` and `UnitCreatureID`.
|
||||
uint32_t CreatureEntry(uint64_t guid);
|
||||
|
||||
// Minimum buffer size for `FormatAsString`'s output — 16 hex digits +
|
||||
// the `"0x"` prefix + a NUL terminator.
|
||||
constexpr std::size_t STRING_SIZE = 19;
|
||||
|
||||
@@ -138,8 +138,8 @@ ItemInfo ReadItemInfo(const uint8_t *cgItem) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return {0, 0};
|
||||
}
|
||||
return {*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_ITEM_LEVEL),
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE)};
|
||||
return {Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_ITEM_LEVEL),
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE)};
|
||||
}
|
||||
|
||||
// Bank-walk helpers — bypass the bank-window gate on `GetItemBySlot`
|
||||
@@ -184,8 +184,8 @@ void WalkGuidArrayRange(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
std::vector<Candidate> &candidates) {
|
||||
if (invMgr == nullptr)
|
||||
return;
|
||||
auto *guidArray = *reinterpret_cast<const uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
auto *guidArray = Game::Read<const uint64_t *>(
|
||||
invMgr, Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guidArray == nullptr)
|
||||
return;
|
||||
for (int slot = firstSlot; slot <= lastSlot; ++slot) {
|
||||
@@ -203,8 +203,8 @@ void WalkBankBags(std::vector<Candidate> &candidates) {
|
||||
auto *playerInvMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (playerInvMgr == nullptr)
|
||||
return;
|
||||
auto *playerGuidArray = *reinterpret_cast<const uint64_t *const *>(
|
||||
playerInvMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
auto *playerGuidArray = Game::Read<const uint64_t *>(
|
||||
playerInvMgr, Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (playerGuidArray == nullptr)
|
||||
return;
|
||||
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
|
||||
|
||||
+10
-4
@@ -42,16 +42,22 @@ namespace {
|
||||
// `uint *` (the ItemStats cache lookup uses the integer both as a
|
||||
// hash key and as the field-zero match target, which is how it
|
||||
// inherits the pointer typing in the decomp).
|
||||
//
|
||||
// `outDuration`/`outStart` are *unsigned* millisecond ticks (same OS counter
|
||||
// and epoch as `GetTime()`). Reading `outStart` into a signed `int` makes
|
||||
// every timestamp negative once the machine has been up past 2^31 ms
|
||||
// (~24.9 days), so `startTime + duration` lands in the past and every item
|
||||
// reads as off cooldown. Keep them unsigned; see `Time::Clock`.
|
||||
using QueryItemCooldown_t = bool(__fastcall *)(uint32_t itemID,
|
||||
int *outDuration,
|
||||
int *outStart,
|
||||
uint32_t *outDuration,
|
||||
uint32_t *outStart,
|
||||
uint32_t *outEnable);
|
||||
|
||||
} // namespace
|
||||
|
||||
void PushCooldown(void *L, int itemID) {
|
||||
int durationMs = 0;
|
||||
int startMs = 0;
|
||||
uint32_t durationMs = 0;
|
||||
uint32_t startMs = 0;
|
||||
uint32_t enable = 0;
|
||||
if (itemID > 0) {
|
||||
auto fn = reinterpret_cast<QueryItemCooldown_t>(
|
||||
|
||||
+3
-3
@@ -39,7 +39,7 @@ constexpr int OFF_CONTAINER_GUID_LO = 0x08;
|
||||
constexpr int OFF_CONTAINER_GUID_HI = 0x0C;
|
||||
|
||||
bool ItemLocked(const uint8_t *item) {
|
||||
return (*reinterpret_cast<const uint32_t *>(item + Offsets::OFF_ITEM_CLIENT_LOCK) & 1) != 0;
|
||||
return (Game::Read<uint32_t>(item, Offsets::OFF_ITEM_CLIENT_LOCK) & 1) != 0;
|
||||
}
|
||||
|
||||
// Reads a CGItem's 64-bit instance GUID into lo/hi. False if the instance
|
||||
@@ -48,8 +48,8 @@ bool ReadItemGuid(const uint8_t *item, uint32_t *lo, uint32_t *hi) {
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return false;
|
||||
*lo = *reinterpret_cast<const uint32_t *>(instance + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
*hi = *reinterpret_cast<const uint32_t *>(instance + Offsets::OFF_INSTANCE_BLOCK_GUID + 4);
|
||||
*lo = Game::Read<uint32_t>(instance, Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
*hi = Game::Read<uint32_t>(instance, Offsets::OFF_INSTANCE_BLOCK_GUID + 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+185
-60
@@ -17,8 +17,11 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -64,21 +67,10 @@ static void FireItemDataLoadResult(int itemID, bool success) {
|
||||
itemID, success);
|
||||
}
|
||||
|
||||
// Engine callback for **implicit** cache fills (transparent warmup
|
||||
// triggered by our `Script_GetItemInfo` hook or `SetItemByID`). Fires
|
||||
// only the broad `GET_ITEM_INFO_RECEIVED` event — `ITEM_DATA_LOAD_RESULT`
|
||||
// is reserved for explicit `RequestLoadItemData(ByID)` paths to match
|
||||
// modern API semantics.
|
||||
//
|
||||
// Calling convention is __stdcall, 2 args / 8 bytes — matching the
|
||||
// Item-load callbacks are __stdcall, 2 args / 8 bytes — matching the
|
||||
// callback at 0x004FDC30 (used by 4 sites in the binary) which ends in
|
||||
// `ret 8`. If the engine called us with a different convention, that
|
||||
// callback would crash the engine, so stdcall is verified by induction.
|
||||
static void __stdcall ItemLoadCallback_Implicit(void *userData, int success) {
|
||||
const auto itemID = static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
FireGetItemInfoReceived(itemID, success != 0);
|
||||
}
|
||||
|
||||
// `ret 8`. If the engine called us with a different convention it would
|
||||
// crash, so stdcall is verified by induction.
|
||||
using ItemLoadCallback_t = void(__stdcall *)(void *userData, int success);
|
||||
|
||||
// Calls DBCache_ItemStats_C_GetRecord. With `callback=nullptr`,
|
||||
@@ -123,8 +115,8 @@ static int ResolveLocationToItemID(void *L, int idx) {
|
||||
auto *instance = Item::InstanceBlock(item);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_ITEM_ID));
|
||||
return static_cast<int>(Game::Read<uint32_t>(
|
||||
instance, Offsets::OFF_INSTANCE_BLOCK_ITEM_ID));
|
||||
}
|
||||
|
||||
static int __fastcall Script_IsItemDataCachedByID(void *L) {
|
||||
@@ -191,10 +183,42 @@ constexpr int kMaxWaitTicks = 1200;
|
||||
// One-way latch; see the relog note in `WarmCache`.
|
||||
constexpr int kSettleTicks = 60;
|
||||
|
||||
// Minimum spacing between outgoing *background* SMSG_ITEM_QUERY_SINGLE
|
||||
// requests. A single /reload can make addons (pfQuest / Questie / Bagnon)
|
||||
// look up hundreds of items in one frame. Unthrottled, every cache miss
|
||||
// fired its own packet at once: a reported burst of 1,325 requests in
|
||||
// ~14 ms exhausted a consumer router's NAT table and dropped the
|
||||
// machine's internet connectivity (issue #25). 50 ms caps the sustained
|
||||
// background rate at 20 requests / second. OnWorldTick is the single
|
||||
// point that issues queries, so this one floor governs the bulk path.
|
||||
constexpr uint32_t kMinRequestSpacingMs = 50;
|
||||
|
||||
// Priority tier. The player's carried items (equipment + backpack +
|
||||
// equipped bags) are what the bag / paperdoll UI shows immediately on
|
||||
// login; behind the background floor they showed as "unknown" for a
|
||||
// second or two while a bulk addon scan drained ahead of them. These
|
||||
// are issued on a separate, more generous budget so they resolve fast.
|
||||
// The set is bounded (~120 items) and the engine prefetches inventory in
|
||||
// parallel, so a small per-tick cap keeps them near-instant without
|
||||
// re-introducing a burst: worst case is kOwnedPerTick + 1 packets per
|
||||
// frame (~7), far below the flood that broke connectivity.
|
||||
constexpr int kOwnedPerTick = 6;
|
||||
constexpr int kMaxOwned = 256;
|
||||
constexpr uint32_t kOwnedRefreshMs = 1000; // carried set changes slowly
|
||||
|
||||
static Pending g_pending[kMaxPending];
|
||||
static int g_pendingCount = 0;
|
||||
static bool g_settled = false;
|
||||
static int g_worldTicks = 0;
|
||||
static uint32_t g_lastRequestMs = 0;
|
||||
|
||||
// Snapshot of the itemIDs the player currently carries. Rebuilt by a
|
||||
// pure-C inventory walk (no Lua stack — safe at world-tick time) at most
|
||||
// once per kOwnedRefreshMs, and only while there is queued work.
|
||||
static uint32_t g_owned[kMaxOwned];
|
||||
static int g_ownedCount = 0;
|
||||
static uint32_t g_ownedBuiltMs = 0;
|
||||
static bool g_ownedEverBuilt = false;
|
||||
|
||||
static int FindPending(uint32_t itemID) {
|
||||
for (int i = 0; i < g_pendingCount; ++i) {
|
||||
@@ -335,69 +359,170 @@ static const Game::HookAutoRegister _hookCacheResponse{
|
||||
reinterpret_cast<void *>(&ItemStatsCacheResponse_h),
|
||||
reinterpret_cast<void **>(&ItemStatsCacheResponse_o)};
|
||||
|
||||
// Escalation + timeout. The hook covers the happy path (engine fills
|
||||
// cache → we fire). This tick is only responsible for:
|
||||
// --- Owned-item priority set --------------------------------------------
|
||||
//
|
||||
// Determines which pending items are the player's carried inventory so
|
||||
// OnWorldTick can issue them ahead of background bulk lookups. The walk
|
||||
// reads the engine's GUID arrays directly (no Lua stack, no GetItemBySlot
|
||||
// bank gate) — the same pure-C pattern `item/Count` uses — so it is safe
|
||||
// to run at world-tick time.
|
||||
|
||||
static void AddOwned(uint32_t itemID) {
|
||||
if (itemID == 0)
|
||||
return;
|
||||
for (int i = 0; i < g_ownedCount; ++i)
|
||||
if (g_owned[i] == itemID)
|
||||
return; // dedup — stacks / duplicates collapse to one entry
|
||||
if (g_ownedCount < kMaxOwned)
|
||||
g_owned[g_ownedCount++] = itemID;
|
||||
}
|
||||
|
||||
// Collects itemIDs from linear slots [first, last] of an inventory
|
||||
// manager's GUID array (invMgr+OFF_INVMGR_GUID_ARRAY). Empty slots hold a
|
||||
// zero GUID and are skipped.
|
||||
static void CollectOwnedFrom(const uint8_t *invMgr, int first, int last) {
|
||||
if (invMgr == nullptr)
|
||||
return;
|
||||
auto *guids = Game::Read<const uint64_t *>(
|
||||
invMgr, Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr)
|
||||
return;
|
||||
for (int slot = first; slot <= last; ++slot) {
|
||||
const uint8_t *item = Item::Location::ResolveByGUID(guids[slot]);
|
||||
if (item != nullptr)
|
||||
AddOwned(static_cast<uint32_t>(Item::ID::FromCGItem(item)));
|
||||
}
|
||||
}
|
||||
|
||||
static void RebuildOwned() {
|
||||
g_ownedCount = 0;
|
||||
auto *invMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (invMgr != nullptr) {
|
||||
// Player invMgr linear slots 0..38: equipment + bag-slot items +
|
||||
// backpack (bank starts at INVMGR_BANK_MAIN_FIRST_SLOT = 39).
|
||||
CollectOwnedFrom(invMgr, 0,
|
||||
Offsets::BACKPACK_LINEAR_BASE + Offsets::BACKPACK_NUM_SLOTS - 1);
|
||||
// Contents of each equipped bag (its own invMgr, slot count at +0).
|
||||
for (int bagID = 1; bagID <= 4; ++bagID) {
|
||||
auto *bagInv =
|
||||
static_cast<const uint8_t *>(Item::Location::EquippedBagInventory(bagID));
|
||||
if (bagInv == nullptr)
|
||||
continue;
|
||||
const int count = static_cast<int>(
|
||||
Game::Read<uint32_t>(bagInv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (count > 0)
|
||||
CollectOwnedFrom(bagInv, 0, count - 1);
|
||||
}
|
||||
}
|
||||
g_ownedEverBuilt = true;
|
||||
}
|
||||
|
||||
static bool IsOwned(uint32_t itemID) {
|
||||
for (int i = 0; i < g_ownedCount; ++i)
|
||||
if (g_owned[i] == itemID)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Escalation + timeout — and the ONE place any item query is issued, so
|
||||
// the rate limiter here governs every path into the cache. This tick is
|
||||
// responsible for:
|
||||
// 1. Latching the startup gate via a tick-count fallback, in case no
|
||||
// engine response arrives early enough to latch it first.
|
||||
// 2. Issuing a query for items the engine doesn't naturally prefetch
|
||||
// (e.g. quest rewards) — but only once `g_settled`, so we don't
|
||||
// race the engine's startup-time state machine. The engine sends
|
||||
// the SMSG, the response handler hook fires the event when the
|
||||
// response lands.
|
||||
// 2. Issuing queries — for items the engine doesn't naturally prefetch
|
||||
// (quest rewards) AND for every `WarmCache` miss — but only once
|
||||
// `g_settled` (don't race the engine's startup state machine). The
|
||||
// player's carried items go out first, up to `kOwnedPerTick` per
|
||||
// tick; everything else is background, one per `kMinRequestSpacingMs`
|
||||
// (don't flood the network; issue #25). The engine sends the SMSG;
|
||||
// the response handler hook fires the event when the response lands.
|
||||
// 3. Firing failure immediately for items the server rejected (the
|
||||
// escalation callback marked them `failed`), and as a last resort
|
||||
// giving up after `kMaxWaitTicks` for items that never resolve at
|
||||
// all (lost query, server silent) — fire failure.
|
||||
// giving up after `kMaxWaitTicks` for items whose query was already
|
||||
// sent but never resolved (lost query, server silent).
|
||||
static void OnWorldTick() {
|
||||
if (!g_settled && ++g_worldTicks >= kSettleTicks)
|
||||
g_settled = true;
|
||||
|
||||
const uint32_t now = Time::Clock::NowMs();
|
||||
|
||||
// Refresh the carried-item set while there is queued work, at most
|
||||
// once per kOwnedRefreshMs — the set changes slowly and the walk is
|
||||
// pointer-chasing, not free.
|
||||
if (g_settled && g_pendingCount > 0 &&
|
||||
(!g_ownedEverBuilt || Time::Clock::Elapsed(g_ownedBuiltMs, now) >= kOwnedRefreshMs)) {
|
||||
RebuildOwned();
|
||||
g_ownedBuiltMs = now;
|
||||
}
|
||||
|
||||
// Two independent issue budgets this tick:
|
||||
// - owned (carried) items: up to kOwnedPerTick, no spacing floor —
|
||||
// bounded and player-visible, so they resolve fast.
|
||||
// - background items: one, and only once the spacing floor elapsed.
|
||||
// World ticks fire once per frame (~10-16 ms), so the background floor
|
||||
// is wall-clock, not a tick count, to bound its rate regardless of FPS.
|
||||
int ownedBudget = g_settled ? kOwnedPerTick : 0;
|
||||
bool bgAvailable =
|
||||
g_settled && Time::Clock::Elapsed(g_lastRequestMs, now) >= kMinRequestSpacingMs;
|
||||
|
||||
for (int i = 0; i < g_pendingCount;) {
|
||||
if (!g_pending[i].requestIssued && g_settled) {
|
||||
CacheFetch(g_pending[i].itemID, &ItemLoadCallback_Escalated);
|
||||
g_pending[i].requestIssued = true;
|
||||
if (g_settled && !g_pending[i].requestIssued) {
|
||||
const bool owned = IsOwned(g_pending[i].itemID);
|
||||
bool issue = false;
|
||||
if (owned && ownedBudget > 0) {
|
||||
issue = true;
|
||||
--ownedBudget;
|
||||
} else if (!owned && bgAvailable) {
|
||||
issue = true;
|
||||
bgAvailable = false;
|
||||
g_lastRequestMs = now;
|
||||
}
|
||||
if (issue) {
|
||||
CacheFetch(g_pending[i].itemID, &ItemLoadCallback_Escalated);
|
||||
g_pending[i].requestIssued = true;
|
||||
}
|
||||
}
|
||||
// A server rejection (marked by the escalation callback) resolves
|
||||
// the item as failure without waiting out the timeout.
|
||||
// A fill (engine response) or a server rejection (escalation
|
||||
// callback set `failed`) resolves the item either way.
|
||||
if (TryCompletePending(i))
|
||||
continue;
|
||||
if (g_pending[i].ticksWaiting >= kMaxWaitTicks) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
const bool implicit = g_pending[i].implicit;
|
||||
RemovePendingAt(i);
|
||||
FireByTag(itemID, false, implicit);
|
||||
continue;
|
||||
// Only time out items whose query is actually in flight. Counting
|
||||
// from track-time would fire spurious failures for items still
|
||||
// waiting their turn behind the rate limiter.
|
||||
if (g_pending[i].requestIssued) {
|
||||
if (g_pending[i].ticksWaiting >= kMaxWaitTicks) {
|
||||
const uint32_t itemID = g_pending[i].itemID;
|
||||
const bool implicit = g_pending[i].implicit;
|
||||
RemovePendingAt(i);
|
||||
FireByTag(itemID, false, implicit);
|
||||
continue;
|
||||
}
|
||||
++g_pending[i].ticksWaiting;
|
||||
}
|
||||
++g_pending[i].ticksWaiting;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// Public C++ API — see Item/Data.h. Implicit (transparent) warmup
|
||||
// path used by the `Script_GetItemInfo` hook and `SetItemByID`. Fires
|
||||
// the cache request only if the item isn't already cached; uses the
|
||||
// implicit callback which fires only `GET_ITEM_INFO_RECEIVED` (not
|
||||
// `ITEM_DATA_LOAD_RESULT`), matching modern semantics for ambient
|
||||
// cache fills.
|
||||
// Public C++ API — see Item/Data.h. Implicit (transparent) warmup path
|
||||
// used by the `Script_GetItemInfo` hook and `SetItemByID`. Routes every
|
||||
// call through the pending queue so OnWorldTick's single rate-limited
|
||||
// issue point governs all outgoing item queries (issue #25). `Track`
|
||||
// resolves an already-cached item for free (a pure lookup, no SMSG) and
|
||||
// queues only a genuine miss; the response-handler sweep then fires
|
||||
// `GET_ITEM_INFO_RECEIVED` (implicit), never `ITEM_DATA_LOAD_RESULT`.
|
||||
void WarmCache(uint32_t itemID) {
|
||||
// Post-settle (steady state): query immediately for lowest latency.
|
||||
// The engine invokes our implicit callback when the data lands.
|
||||
if (g_settled) {
|
||||
CacheFetch(itemID, &ItemLoadCallback_Implicit);
|
||||
return;
|
||||
}
|
||||
// Startup window: do NOT eagerly create the cache entry — that
|
||||
// orphans it (the engine's prefetch skips already-existing entries,
|
||||
// and our SMSG goes into a hole while dispatch isn't ready), leaving
|
||||
// the item permanently nil for the session. This is the exact race
|
||||
// that broke pfQuest/Questie scanning at PLAYER_ENTERING_WORLD. Defer
|
||||
// through the pending set instead; OnWorldTick issues the query once
|
||||
// settled and the response-handler sweep fires GET_ITEM_INFO_RECEIVED.
|
||||
//
|
||||
// NOTE: g_settled is a one-way latch per process. The race this
|
||||
// guards is the initial-login cold-WDB window. On relog the WDB is
|
||||
// already warm so cold-fetch pressure is minimal; we don't re-arm it.
|
||||
// Do NOT issue the query here. Two reasons converge on the queue:
|
||||
// - Network: a post-settle immediate CacheFetch let a reload-time
|
||||
// lookup burst fire one SMSG per item in a single frame and flood
|
||||
// the network (issue #25). The queue's rate limiter prevents it.
|
||||
// - Startup race: eagerly creating the cache entry before the engine
|
||||
// settles orphans it — the engine's prefetch skips already-existing
|
||||
// entries and our SMSG goes into a hole while dispatch isn't ready,
|
||||
// leaving the item permanently nil. This broke pfQuest/Questie
|
||||
// scanning at PLAYER_ENTERING_WORLD. OnWorldTick issues only once
|
||||
// `g_settled`, so deferring here fixes that too.
|
||||
Track(itemID, /*implicit=*/true);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ int __fastcall Script_C_Item_GetEnchantInfo(void *L) {
|
||||
// Localized display name (8-slot locale array at +0x34). Clamp the
|
||||
// locale index to the array bounds — index 8 would read the locale
|
||||
// mask column that follows. enUS (0) fallback for empty slots.
|
||||
auto names = reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELLITEMENCHANT_NAME);
|
||||
int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
auto names = Game::Ptr<const char *const>(rec, Offsets::OFF_SPELLITEMENCHANT_NAME);
|
||||
int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (locale < 0 || locale >= kNumNameLocales)
|
||||
locale = 0;
|
||||
const char *name = names[locale];
|
||||
@@ -93,9 +93,9 @@ int __fastcall Script_C_Item_GetEnchantInfo(void *L) {
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return 0; // unnamed record -> nil
|
||||
|
||||
auto type = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
auto type = Game::Ptr<const int32_t>(rec, Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = Game::Ptr<const int32_t>(rec, Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = Game::Ptr<const int32_t>(rec, Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "enchantID", enchantID);
|
||||
|
||||
+41
-41
@@ -38,7 +38,7 @@ namespace Item::GetData {
|
||||
namespace {
|
||||
|
||||
int CurrentLocaleIndex() {
|
||||
return *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
return Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
}
|
||||
|
||||
// Mirrors the lookup in `Item::Info`. ItemClass.dbc record `+0x0C` is
|
||||
@@ -53,10 +53,10 @@ const char *LookupClassName(uint32_t classID) {
|
||||
// ItemSubClass.dbc walker — same fallback chain as Script_GetItemInfo
|
||||
// (verbose name first, short name on miss).
|
||||
const char *LookupSubClassName(uint32_t classID, uint32_t subClassID) {
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_ITEMSUBCLASS_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_ITEMSUBCLASS_COUNT);
|
||||
if (count <= 0)
|
||||
return "";
|
||||
auto *records = *reinterpret_cast<const uint8_t *const *>(Offsets::VAR_ITEMSUBCLASS_RECORDS);
|
||||
auto *records = Game::Read<const uint8_t *>(Offsets::VAR_ITEMSUBCLASS_RECORDS);
|
||||
if (records == nullptr)
|
||||
return "";
|
||||
const int locale = CurrentLocaleIndex();
|
||||
@@ -66,12 +66,12 @@ const char *LookupSubClassName(uint32_t classID, uint32_t subClassID) {
|
||||
continue;
|
||||
if (*reinterpret_cast<const uint32_t *>(record + 0x04) != subClassID)
|
||||
continue;
|
||||
const char *verbose = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_DISPLAY_NAME + locale * 4);
|
||||
const char *verbose = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSUBCLASS_DISPLAY_NAME + locale * 4);
|
||||
if (verbose != nullptr && verbose[0] != '\0')
|
||||
return verbose;
|
||||
const char *shortName = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_NAME + locale * 4);
|
||||
const char *shortName = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSUBCLASS_NAME + locale * 4);
|
||||
return (shortName != nullptr && shortName[0] != '\0') ? shortName : "";
|
||||
}
|
||||
return "";
|
||||
@@ -172,10 +172,10 @@ void SetF32Array(void *L, const uint8_t *record, const char *key,
|
||||
void SetStatsMap(void *L, const uint8_t *record) {
|
||||
Game::Lua::PushString(L, "stats");
|
||||
Game::Lua::NewTable(L);
|
||||
auto *types = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
auto *types = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = Game::Ptr<const int32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_STAT_SLOT_COUNT; ++i) {
|
||||
if (types[i] == 0 && values[i] == 0)
|
||||
continue;
|
||||
@@ -193,18 +193,18 @@ void SetStatsMap(void *L, const uint8_t *record) {
|
||||
void SetSpellsArray(void *L, const uint8_t *record) {
|
||||
Game::Lua::PushString(L, "spells");
|
||||
Game::Lua::NewTable(L);
|
||||
auto *ids = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto *triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
auto *charges = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CHARGES);
|
||||
auto *cooldowns = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_COOLDOWN);
|
||||
auto *categories = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CATEGORY);
|
||||
auto *catCooldowns = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CATEGORY_CD);
|
||||
auto *ids = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto *triggers = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
auto *charges = Game::Ptr<const int32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_CHARGES);
|
||||
auto *cooldowns = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_COOLDOWN);
|
||||
auto *categories = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_CATEGORY);
|
||||
auto *catCooldowns = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_CATEGORY_CD);
|
||||
int written = 0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
if (ids[i] == 0)
|
||||
@@ -234,38 +234,38 @@ void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
Game::Lua::SetFieldNumber(L, "itemID", static_cast<double>(itemID));
|
||||
|
||||
// Identity strings
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *name = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSTATS_NAME);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
const char *description = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
if (description != nullptr && description[0] != '\0')
|
||||
Game::Lua::SetFieldString(L, "description", description);
|
||||
|
||||
// Display
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
const uint32_t displayInfoID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
Game::Lua::SetFieldNumber(L, "displayInfoID", static_cast<double>(displayInfoID));
|
||||
char iconPath[260];
|
||||
if (BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
Game::Lua::SetFieldString(L, "icon", iconPath);
|
||||
|
||||
// Quality / classification
|
||||
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
const uint32_t quality = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
Game::Lua::SetFieldNumber(L, "quality", static_cast<double>(quality));
|
||||
|
||||
const uint32_t classID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClassID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
const uint32_t classID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClassID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
Game::Lua::SetFieldNumber(L, "classID", static_cast<double>(classID));
|
||||
Game::Lua::SetFieldNumber(L, "subclassID", static_cast<double>(subClassID));
|
||||
Game::Lua::SetFieldString(L, "className", LookupClassName(classID));
|
||||
Game::Lua::SetFieldString(L, "subclassName", LookupSubClassName(classID, subClassID));
|
||||
|
||||
const uint32_t invType = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
const uint32_t invType = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
Game::Lua::SetFieldNumber(L, "inventoryType", static_cast<double>(invType));
|
||||
Game::Lua::SetFieldString(L, "equipLoc", LookupInvType(invType));
|
||||
|
||||
@@ -273,8 +273,8 @@ void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
SetU32(L, record, "bindType", Offsets::OFF_ITEMSTATS_BONDING);
|
||||
|
||||
// Flags: raw + decoded bits.
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
const uint32_t flags = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
Game::Lua::SetFieldNumber(L, "flags", static_cast<double>(flags));
|
||||
Game::Lua::SetFieldBool(L, "isConjured",
|
||||
(flags & Offsets::ITEM_FLAG_CONJURED) != 0);
|
||||
@@ -289,8 +289,8 @@ void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
SetI32(L, record, "maxStackSize", Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
SetI32(L, record, "maxCount", Offsets::OFF_ITEMSTATS_MAX_COUNT);
|
||||
SetU32(L, record, "containerSlots", Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS);
|
||||
const uint32_t rawBagFamily = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
const uint32_t rawBagFamily = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
Game::Lua::SetFieldNumber(L, "bagFamily",
|
||||
static_cast<double>(BagFamilyIdToBitmask(rawBagFamily)));
|
||||
|
||||
|
||||
+17
-17
@@ -30,7 +30,7 @@ namespace Item::Info {
|
||||
|
||||
static const char *PushedOrEmpty(const char *s) { return (s != nullptr && s[0] != 0) ? s : ""; }
|
||||
|
||||
static int CurrentLocaleIndex() { return *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX); }
|
||||
static int CurrentLocaleIndex() { return Game::Read<int>(Offsets::VAR_LOCALE_INDEX); }
|
||||
|
||||
static const char *LookupItemClassName(uint32_t classID) {
|
||||
return PushedOrEmpty(DBC::LocalizedField(
|
||||
@@ -39,10 +39,10 @@ static const char *LookupItemClassName(uint32_t classID) {
|
||||
}
|
||||
|
||||
static const char *LookupItemSubClassName(uint32_t classID, uint32_t subClassID) {
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_ITEMSUBCLASS_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_ITEMSUBCLASS_COUNT);
|
||||
if (count <= 0)
|
||||
return "";
|
||||
auto *records = *reinterpret_cast<const uint8_t *const *>(Offsets::VAR_ITEMSUBCLASS_RECORDS);
|
||||
auto *records = Game::Read<const uint8_t *>(Offsets::VAR_ITEMSUBCLASS_RECORDS);
|
||||
if (records == nullptr)
|
||||
return "";
|
||||
const int locale = CurrentLocaleIndex();
|
||||
@@ -54,12 +54,12 @@ static const char *LookupItemSubClassName(uint32_t classID, uint32_t subClassID)
|
||||
continue;
|
||||
// Mirror Script_GetItemInfo's fallback chain at 0x48E311..0x48E32C:
|
||||
// try the verbose name first, fall back to the short name.
|
||||
const char *verbose = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_DISPLAY_NAME + locale * 4);
|
||||
const char *verbose = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSUBCLASS_DISPLAY_NAME + locale * 4);
|
||||
if (verbose != nullptr && verbose[0] != 0)
|
||||
return verbose;
|
||||
const char *shortName = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_NAME + locale * 4);
|
||||
const char *shortName = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSUBCLASS_NAME + locale * 4);
|
||||
return PushedOrEmpty(shortName);
|
||||
}
|
||||
return "";
|
||||
@@ -112,13 +112,13 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
}
|
||||
|
||||
const uint32_t classID =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClassID =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
const uint32_t displayInfoID =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
const uint32_t invType =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
@@ -143,8 +143,8 @@ static int PushIconForItemID(void *L, int itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
const uint32_t displayInfoID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
return 0;
|
||||
@@ -274,10 +274,10 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
const uint32_t sellPrice = u32(Offsets::OFF_ITEMSTATS_SELL_PRICE);
|
||||
const uint32_t bindType = u32(Offsets::OFF_ITEMSTATS_BONDING);
|
||||
const uint32_t setID = u32(Offsets::OFF_ITEMSTATS_ITEM_SET);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
const char *name = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *description = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
|
||||
// Apply the link's random suffix ("... of the Owl") to both the name and
|
||||
// the reconstructed link when the input carried one (arg.suffix); with no
|
||||
|
||||
+4
-4
@@ -58,8 +58,8 @@ bool ReadItemGuid(const uint8_t *item, uint32_t *lo, uint32_t *hi) {
|
||||
auto *inst = Item::InstanceBlock(item);
|
||||
if (inst == nullptr)
|
||||
return false;
|
||||
*lo = *reinterpret_cast<const uint32_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
*hi = *reinterpret_cast<const uint32_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID + 4);
|
||||
*lo = Game::Read<uint32_t>(inst, Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
*hi = Game::Read<uint32_t>(inst, Offsets::OFF_INSTANCE_BLOCK_GUID + 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ int __fastcall Script_C_Item_IsLocked(void *L) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
item + Offsets::OFF_ITEM_CLIENT_LOCK);
|
||||
const uint32_t flags = Game::Read<uint32_t>(
|
||||
item, Offsets::OFF_ITEM_CLIENT_LOCK);
|
||||
Game::Lua::PushBool(L, (flags & Offsets::ITEM_CLIENT_LOCK_BIT) != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
#include "item/Location.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -128,10 +129,7 @@ bool g_seeded = false; // true once the login baseline has been taken
|
||||
uint64_t g_playerGuid = 0; // re-arm the baseline when this changes (relog)
|
||||
uint32_t g_loginMs = 0; // engine ms at which the current player resolved
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(Offsets::FUN_OS_TICKCOUNT_MS)();
|
||||
}
|
||||
using Time::Clock::NowMs;
|
||||
|
||||
bool Contains(const uint64_t *arr, int count, uint64_t guid) {
|
||||
for (int i = 0; i < count; ++i)
|
||||
|
||||
+27
-27
@@ -155,14 +155,14 @@ void AddSpellStatAuras(Accum *acc, int spellID, long sign) {
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (sp == nullptr)
|
||||
return;
|
||||
auto aura = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto misc = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
|
||||
auto base = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
auto dice = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
|
||||
auto aura = Game::Ptr<const int32_t>(
|
||||
sp, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto misc = Game::Ptr<const int32_t>(
|
||||
sp, Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
|
||||
auto base = Game::Ptr<const int32_t>(
|
||||
sp, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
auto dice = Game::Ptr<const int32_t>(
|
||||
sp, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
|
||||
|
||||
// Vanilla "+N Attack Power" spells carry a melee (99) AND a ranged
|
||||
// (124) aura with the same value; the generic melee key subsumes the
|
||||
@@ -248,10 +248,10 @@ void AddSpellStatAuras(Accum *acc, int spellID, long sign) {
|
||||
// mp5 / defense on base items (vanilla stores them as equip spells, not
|
||||
// stat slots).
|
||||
void AddEquipSpellStats(Accum *acc, const uint8_t *record, long sign) {
|
||||
auto ids = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
auto ids = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto triggers = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i)
|
||||
if (ids[i] != 0 && triggers[i] == 1) // ON_EQUIP
|
||||
AddSpellStatAuras(acc, static_cast<int>(ids[i]), sign);
|
||||
@@ -288,14 +288,14 @@ long Value(const Accum *acc, const char *key) {
|
||||
double ComputeDPS(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return 0.0;
|
||||
const uint32_t delayMs = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DELAY);
|
||||
const uint32_t delayMs = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_DELAY);
|
||||
if (delayMs == 0)
|
||||
return 0.0;
|
||||
auto *dmgMin = reinterpret_cast<const float *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DAMAGE_MIN);
|
||||
auto *dmgMax = reinterpret_cast<const float *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DAMAGE_MAX);
|
||||
auto *dmgMin = Game::Ptr<const float>(
|
||||
record, Offsets::OFF_ITEMSTATS_DAMAGE_MIN);
|
||||
auto *dmgMax = Game::Ptr<const float>(
|
||||
record, Offsets::OFF_ITEMSTATS_DAMAGE_MAX);
|
||||
double avgTotal = 0.0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT; ++i)
|
||||
avgTotal += (static_cast<double>(dmgMin[i]) +
|
||||
@@ -312,9 +312,9 @@ void ApplyEnchant(Accum *acc, uint32_t enchantID, long sign) {
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
if (en == nullptr)
|
||||
return;
|
||||
auto type = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
auto type = Game::Ptr<const int32_t>(en, Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = Game::Ptr<const int32_t>(en, Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = Game::Ptr<const int32_t>(en, Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
for (int i = 0; i < 3; ++i) { // SpellItemEnchantment has 3 effect slots
|
||||
switch (type[i]) {
|
||||
case 3: // equip spell — stat/resist value lives in the spell
|
||||
@@ -341,8 +341,8 @@ void ApplyRandomSuffix(Accum *acc, int suffixID, long sign) {
|
||||
static_cast<uint32_t>(suffixID));
|
||||
if (rp == nullptr)
|
||||
return;
|
||||
auto enchants = reinterpret_cast<const uint32_t *>(
|
||||
rp + Offsets::OFF_ITEMRANDOMPROP_ENCHANT);
|
||||
auto enchants = Game::Ptr<const uint32_t>(
|
||||
rp, Offsets::OFF_ITEMRANDOMPROP_ENCHANT);
|
||||
for (int s = 0; s < Offsets::ITEMRANDOMPROP_ENCHANT_SLOT_COUNT; ++s)
|
||||
ApplyEnchant(acc, enchants[s], sign);
|
||||
}
|
||||
@@ -350,10 +350,10 @@ void ApplyRandomSuffix(Accum *acc, int suffixID, long sign) {
|
||||
void AccumulateRecord(Accum *acc, const uint8_t *record, long sign) {
|
||||
if (record == nullptr)
|
||||
return;
|
||||
auto *types = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
auto *types = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = Game::Ptr<const int32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_STAT_SLOT_COUNT; ++i)
|
||||
AddByKey(acc, StatKeyForItemModType(types[i]),
|
||||
sign * static_cast<long>(values[i]));
|
||||
|
||||
+4
-4
@@ -44,8 +44,8 @@ bool ReadGuid(const void *cgObject, uint32_t *lo, uint32_t *hi) {
|
||||
auto *inst = Item::InstanceBlock(base);
|
||||
if (inst == nullptr)
|
||||
return false;
|
||||
*lo = *reinterpret_cast<const uint32_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
*hi = *reinterpret_cast<const uint32_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID + 4);
|
||||
*lo = Game::Read<uint32_t>(inst, Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
*hi = Game::Read<uint32_t>(inst, Offsets::OFF_INSTANCE_BLOCK_GUID + 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -259,8 +259,8 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
// have to special-case "move everything".
|
||||
auto *srcDescriptor = Item::ObjectFields(srcItem);
|
||||
const int srcStack = srcDescriptor == nullptr ? 0 :
|
||||
static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
srcDescriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
static_cast<int>(Game::Read<uint32_t>(
|
||||
srcDescriptor, Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
if (count > srcStack)
|
||||
return false; // server would reject anyway; fail fast and locally
|
||||
if (count == srcStack)
|
||||
|
||||
@@ -241,16 +241,16 @@ static int __fastcall Script_GameTooltipGetItem(void *L) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
}
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
const char *name = Game::Read<const char *>(
|
||||
record, Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return 0;
|
||||
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
const uint32_t quality = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
|
||||
int suffix = 0;
|
||||
if (*reinterpret_cast<const int *>(base + Offsets::OFF_TOOLTIP_COMPARE_FLAG) != 0)
|
||||
suffix = *reinterpret_cast<const int *>(base + Offsets::OFF_TOOLTIP_COMPARE_SUFFIX);
|
||||
if (Game::Read<int>(base, Offsets::OFF_TOOLTIP_COMPARE_FLAG) != 0)
|
||||
suffix = Game::Read<int>(base, Offsets::OFF_TOOLTIP_COMPARE_SUFFIX);
|
||||
|
||||
char link[256];
|
||||
std::snprintf(link, sizeof(link),
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "item/TooltipItem.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
@@ -27,10 +28,10 @@ int CurrentID(const void *tooltipObj, uint64_t *outGuid) {
|
||||
auto *c = static_cast<const uint8_t *>(tooltipObj);
|
||||
|
||||
const uint64_t itemGuid =
|
||||
*reinterpret_cast<const uint64_t *>(c + Offsets::OFF_TOOLTIP_ITEM_GUID_LO);
|
||||
Game::Read<uint64_t>(c, Offsets::OFF_TOOLTIP_ITEM_GUID_LO);
|
||||
|
||||
// Link item: the itemID field is authoritative (and reliably cleared).
|
||||
const int linkItemID = *reinterpret_cast<const int *>(c + Offsets::OFF_TOOLTIP_ITEM_ID);
|
||||
const int linkItemID = Game::Read<int>(c, Offsets::OFF_TOOLTIP_ITEM_ID);
|
||||
if (linkItemID > 0) {
|
||||
if (outGuid != nullptr)
|
||||
*outGuid = itemGuid;
|
||||
@@ -44,11 +45,11 @@ int CurrentID(const void *tooltipObj, uint64_t *outGuid) {
|
||||
// it to the itemID (this also yields the dressed link for callers).
|
||||
if (itemGuid == 0)
|
||||
return 0;
|
||||
if (*reinterpret_cast<const uint64_t *>(c + Offsets::OFF_TOOLTIP_UNIT_GUID_LO) != 0)
|
||||
if (Game::Read<uint64_t>(c, Offsets::OFF_TOOLTIP_UNIT_GUID_LO) != 0)
|
||||
return 0;
|
||||
if (*reinterpret_cast<const uint64_t *>(c + Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_LO) != 0)
|
||||
if (Game::Read<uint64_t>(c, Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_LO) != 0)
|
||||
return 0;
|
||||
if (*reinterpret_cast<const int *>(c + Offsets::OFF_TOOLTIP_SPELL_ID) > 0)
|
||||
if (Game::Read<int>(c, Offsets::OFF_TOOLTIP_SPELL_ID) > 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *cg = Item::Location::ResolveByGUID(itemGuid);
|
||||
|
||||
@@ -31,7 +31,7 @@ static int __fastcall Script_GetInboxItemID(void *L) {
|
||||
if (mailID < 0)
|
||||
return 0;
|
||||
|
||||
const int count = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
const int count = static_cast<int>(Game::Read<uint32_t>(
|
||||
Offsets::VAR_INBOX_COUNT));
|
||||
if (mailID >= count)
|
||||
return 0;
|
||||
@@ -39,15 +39,15 @@ static int __fastcall Script_GetInboxItemID(void *L) {
|
||||
// `VAR_INBOX_ENTRIES` is a pointer slot, not the array — the
|
||||
// engine reads it via `MOV ECX, [imm32]; MOV EDI, [ECX + idx*4]`
|
||||
// (two derefs total before the entry pointer is in hand).
|
||||
auto **entries = *reinterpret_cast<const uint8_t ***>(Offsets::VAR_INBOX_ENTRIES);
|
||||
auto **entries = Game::Read<const uint8_t **>(Offsets::VAR_INBOX_ENTRIES);
|
||||
if (entries == nullptr)
|
||||
return 0;
|
||||
auto *entry = entries[mailID];
|
||||
if (entry == nullptr)
|
||||
return 0;
|
||||
|
||||
const int itemID = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
entry + Offsets::OFF_INBOX_ENTRY_ITEM_ID));
|
||||
const int itemID = static_cast<int>(Game::Read<uint32_t>(
|
||||
entry, Offsets::OFF_INBOX_ENTRY_ITEM_ID));
|
||||
if (itemID == 0)
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ static int __fastcall Script_GetLootSlotItemID(void *L) {
|
||||
}
|
||||
int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const uint32_t guidLo = *reinterpret_cast<const uint32_t *>(Offsets::VAR_LOOT_GUID_LO);
|
||||
const uint32_t guidHi = *reinterpret_cast<const uint32_t *>(Offsets::VAR_LOOT_GUID_HI);
|
||||
const uint32_t guidLo = Game::Read<uint32_t>(Offsets::VAR_LOOT_GUID_LO);
|
||||
const uint32_t guidHi = Game::Read<uint32_t>(Offsets::VAR_LOOT_GUID_HI);
|
||||
if ((guidLo | guidHi) == 0)
|
||||
return 0;
|
||||
|
||||
slot -= 1;
|
||||
if (*reinterpret_cast<const uint32_t *>(Offsets::VAR_LOOT_LOOTABLE) != 0) {
|
||||
if (Game::Read<uint32_t>(Offsets::VAR_LOOT_LOOTABLE) != 0) {
|
||||
if (slot <= 0)
|
||||
return 0;
|
||||
slot -= 1;
|
||||
|
||||
@@ -30,20 +30,20 @@ static int __fastcall Script_GetMerchantItemID(void *L) {
|
||||
if (index < 0)
|
||||
return 0;
|
||||
|
||||
const uint32_t guidLo = *reinterpret_cast<const uint32_t *>(Offsets::VAR_MERCHANT_NPC_GUID_LO);
|
||||
const uint32_t guidHi = *reinterpret_cast<const uint32_t *>(Offsets::VAR_MERCHANT_NPC_GUID_HI);
|
||||
const uint32_t guidLo = Game::Read<uint32_t>(Offsets::VAR_MERCHANT_NPC_GUID_LO);
|
||||
const uint32_t guidHi = Game::Read<uint32_t>(Offsets::VAR_MERCHANT_NPC_GUID_HI);
|
||||
if ((guidLo | guidHi) == 0)
|
||||
return 0;
|
||||
|
||||
const int count = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
const int count = static_cast<int>(Game::Read<uint32_t>(
|
||||
Offsets::VAR_MERCHANT_COUNT));
|
||||
if (index >= count)
|
||||
return 0;
|
||||
|
||||
auto *entry = reinterpret_cast<const uint8_t *>(
|
||||
Offsets::VAR_MERCHANT_ITEMS + index * Offsets::MERCHANT_STRIDE);
|
||||
const int itemID = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
entry + Offsets::OFF_MERCHANT_ITEM_ID));
|
||||
const int itemID = static_cast<int>(Game::Read<uint32_t>(
|
||||
entry, Offsets::OFF_MERCHANT_ITEM_ID));
|
||||
if (itemID == 0)
|
||||
return 0;
|
||||
|
||||
|
||||
+5
-11
@@ -49,6 +49,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Names.h"
|
||||
#include "event/Custom.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -63,14 +64,7 @@ T Read(const void *base, int off) {
|
||||
return *reinterpret_cast<const T *>(static_cast<const uint8_t *>(base) + off);
|
||||
}
|
||||
|
||||
// Engine millisecond clock — same source (and epoch) as Lua's `GetTime()`
|
||||
// (which reads this scaled by 0.001), so a value stored here is directly
|
||||
// comparable to `GetTime()` once divided by 1000.
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
using Time::Clock::NowMs;
|
||||
|
||||
// Modern rollType constants (what C_LootHistory.GetPlayerInfo returns).
|
||||
constexpr int ROLL_PASS = 0;
|
||||
@@ -98,13 +92,13 @@ const uint8_t *LookupPlayerRecord(uint64_t guid) {
|
||||
|
||||
const char *NameFromRecord(const uint8_t *rec) {
|
||||
return rec != nullptr
|
||||
? reinterpret_cast<const char *>(rec + Offsets::OFF_PLAYER_INFO_NAME)
|
||||
? Game::Ptr<const char>(rec, Offsets::OFF_PLAYER_INFO_NAME)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
uint32_t ClassIdFromRecord(const uint8_t *rec) {
|
||||
return rec != nullptr
|
||||
? *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_PLAYER_INFO_CLASS)
|
||||
? Game::Read<uint32_t>(rec, Offsets::OFF_PLAYER_INFO_CLASS)
|
||||
: 0;
|
||||
}
|
||||
|
||||
@@ -318,7 +312,7 @@ int RecordAllPassed(const void *msg) {
|
||||
// the same field offsets as the message. This is the only place the random
|
||||
// fields are populated, so the result recorders leave them alone.
|
||||
void RecordStart() {
|
||||
const uint8_t *node = *reinterpret_cast<const uint8_t *const *>(
|
||||
const uint8_t *node = Game::Read<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOOTROLL_STORE_HEAD));
|
||||
if (node == nullptr)
|
||||
return;
|
||||
|
||||
+5
-8
@@ -76,10 +76,9 @@ constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
// 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 = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -87,10 +86,8 @@ bool IsLootable(const void *unit) {
|
||||
// engine uses this as the contribution from both sides (player + target)
|
||||
// in its interact-range computation.
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return *reinterpret_cast<const float *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
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
|
||||
|
||||
+7
-12
@@ -143,18 +143,15 @@ constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
// === Range / lootability helpers (identical to Loot::Nearby) ===
|
||||
|
||||
bool IsLootableUnit(const void *unit) {
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
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 = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return *reinterpret_cast<const float *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
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) {
|
||||
@@ -190,8 +187,7 @@ bool InInteractRange(const void *player, const void *target) {
|
||||
// `FUN_LOOT_CONTROLLER` run, so the slot table is fully filled in
|
||||
// and the link builder works against it.
|
||||
void ScrapeCurrentLoot(LootEntry *out) {
|
||||
const uint32_t coinRaw = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_LOOT_LOOTABLE);
|
||||
const uint32_t coinRaw = Game::Read<uint32_t>(Offsets::VAR_LOOT_LOOTABLE);
|
||||
out->coin = (coinRaw == 0xFFFFFFFFu) ? 0u : coinRaw;
|
||||
|
||||
const bool hasCoin = coinRaw != 0;
|
||||
@@ -239,8 +235,7 @@ void ScrapeCurrentLoot(LootEntry *out) {
|
||||
void LootCurrentWindow() {
|
||||
// Coin present when `VAR_LOOT_LOOTABLE` holds a real amount (0 = none;
|
||||
// 0xFFFFFFFF = already-looted sentinel, never seen on a fresh open).
|
||||
const uint32_t coinRaw = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_LOOT_LOOTABLE);
|
||||
const uint32_t coinRaw = Game::Read<uint32_t>(Offsets::VAR_LOOT_LOOTABLE);
|
||||
if (coinRaw != 0 && coinRaw != 0xFFFFFFFFu) {
|
||||
auto LootMoney = reinterpret_cast<LootMoney_t>(Offsets::FUN_CMSG_LOOT_MONEY);
|
||||
LootMoney(s_scan.player);
|
||||
|
||||
@@ -57,8 +57,8 @@ uint32_t s_desiredItemID = 0;
|
||||
int s_ticksSinceTransition = 0;
|
||||
|
||||
uint64_t ReadLootTargetGuid() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(Offsets::VAR_LOOT_GUID_LO);
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(Offsets::VAR_LOOT_GUID_HI);
|
||||
const uint32_t lo = Game::Read<uint32_t>(Offsets::VAR_LOOT_GUID_LO);
|
||||
const uint32_t hi = Game::Read<uint32_t>(Offsets::VAR_LOOT_GUID_HI);
|
||||
return static_cast<uint64_t>(hi) << 32 | lo;
|
||||
}
|
||||
|
||||
|
||||
+31
-28
@@ -48,6 +48,7 @@
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -56,21 +57,18 @@ namespace LossOfControl {
|
||||
|
||||
namespace {
|
||||
|
||||
using TickMs_t = uint32_t(__fastcall *)();
|
||||
int NowMs() {
|
||||
return static_cast<int>(
|
||||
reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)());
|
||||
}
|
||||
using Time::Clock::NowMs;
|
||||
using Time::Clock::Reached;
|
||||
|
||||
const char *SpellName(const uint8_t *rec) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *n =
|
||||
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
Game::Read<const char *>(rec, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
return (n != nullptr) ? n : "";
|
||||
}
|
||||
|
||||
const char *SpellIconPath(const uint8_t *rec) {
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const int iconID = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const uint8_t *iconRec = DBC::Record(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID));
|
||||
@@ -81,7 +79,7 @@ const char *SpellIconPath(const uint8_t *rec) {
|
||||
}
|
||||
|
||||
int SpellSchoolIndex(const uint8_t *rec) {
|
||||
return *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_SCHOOL);
|
||||
return Game::Read<int>(rec, Offsets::OFF_SPELL_SCHOOL);
|
||||
}
|
||||
|
||||
// The control-loss classifier lives in `Spell::CrowdControl::Classify` so the
|
||||
@@ -90,12 +88,14 @@ int SpellSchoolIndex(const uint8_t *rec) {
|
||||
|
||||
// ---- School-interrupt lockout state (fed by SMSG_SPELL_COOLDOWN) -----------
|
||||
|
||||
// One active lockout per school index (0..6); active while now < endMs.
|
||||
// One active lockout per school index (0..6); active until `Reached(endMs)`.
|
||||
// Self-expiring, per-process — a stale entry from before a relog is already
|
||||
// in the past on the shared uptime clock, so no reset is needed.
|
||||
// in the past on the shared uptime clock, so no reset is needed. Unsigned,
|
||||
// like every engine-tick store: the OS counter wraps at 2^32 ms and a signed
|
||||
// `int` goes negative past 2^31 ms (~24.9 days uptime). See `Time::Clock`.
|
||||
struct SchoolLock {
|
||||
int startMs;
|
||||
int endMs;
|
||||
uint32_t startMs;
|
||||
uint32_t endMs;
|
||||
};
|
||||
SchoolLock g_schoolLock[Offsets::SPELL_SCHOOL_COUNT] = {};
|
||||
|
||||
@@ -137,9 +137,9 @@ void CooldownSub(uint32_t opcode, Net::CDataStore *packet) {
|
||||
// (ProhibitSpellSchool); single/varied packets are normal cooldowns and
|
||||
// ignored.
|
||||
if (count >= 2 && uniform && firstDur > 0 && schoolIdx >= 0) {
|
||||
const int now = NowMs();
|
||||
const uint32_t now = NowMs();
|
||||
g_schoolLock[schoolIdx].startMs = now;
|
||||
g_schoolLock[schoolIdx].endMs = now + firstDur;
|
||||
g_schoolLock[schoolIdx].endMs = now + static_cast<uint32_t>(firstDur);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,17 +152,17 @@ struct LocEntry {
|
||||
int spellID; // 0 = unknown (school interrupt)
|
||||
const uint8_t *rec; // spell record for name/icon; null for school interrupt
|
||||
int schoolMask; // lockoutSchool
|
||||
int startMs; // 0 = unknown
|
||||
int endMs; // 0 = unknown / no timing
|
||||
uint32_t startMs; // 0 = unknown
|
||||
uint32_t endMs; // 0 = unknown / no timing
|
||||
};
|
||||
|
||||
int BuildList(LocEntry *out, int maxOut) {
|
||||
const int now = NowMs();
|
||||
const uint32_t now = NowMs();
|
||||
int n = 0;
|
||||
|
||||
// School-interrupt lockouts first.
|
||||
for (int s = 0; s < Offsets::SPELL_SCHOOL_COUNT && n < maxOut; ++s) {
|
||||
if (g_schoolLock[s].endMs != 0 && now < g_schoolLock[s].endMs) {
|
||||
if (g_schoolLock[s].endMs != 0 && !Reached(now, g_schoolLock[s].endMs)) {
|
||||
LocEntry &e = out[n++];
|
||||
e.locType = "SCHOOL_INTERRUPT";
|
||||
e.spellID = 0;
|
||||
@@ -202,11 +202,12 @@ int BuildList(LocEntry *out, int maxOut) {
|
||||
e.endMs = 0;
|
||||
uint64_t caster = 0;
|
||||
uint32_t expMs = 0, durMs = 0;
|
||||
if (Aura::Source::Get(playerGuid, spellID, &caster, &expMs, &durMs) &&
|
||||
if (Aura::Source::Get(playerGuid, spellID, slot, &caster, &expMs,
|
||||
&durMs) &&
|
||||
expMs != 0) {
|
||||
e.endMs = static_cast<int>(expMs);
|
||||
e.endMs = expMs;
|
||||
if (durMs != 0)
|
||||
e.startMs = static_cast<int>(expMs - durMs);
|
||||
e.startMs = expMs - durMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,7 +303,7 @@ int __fastcall Script_GetActiveLossOfControlData(void *L) {
|
||||
return 0; // nil
|
||||
|
||||
const LocEntry &e = entries[index - 1];
|
||||
const int now = NowMs();
|
||||
const uint32_t now = NowMs();
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "locType", e.locType);
|
||||
@@ -314,12 +315,14 @@ int __fastcall Script_GetActiveLossOfControlData(void *L) {
|
||||
|
||||
// Timing (seconds, GetTime() epoch). Nullable — leave a field unset (nil)
|
||||
// when we don't know it, per the modern contract.
|
||||
if (e.endMs != 0 && now < e.endMs) {
|
||||
Game::Lua::SetFieldNumber(L, "timeRemaining", (e.endMs - now) * 0.001);
|
||||
if (e.endMs != 0 && !Reached(now, e.endMs)) {
|
||||
Game::Lua::SetFieldNumber(L, "timeRemaining",
|
||||
static_cast<double>(e.endMs - now) * 0.001);
|
||||
if (e.startMs != 0) {
|
||||
Game::Lua::SetFieldNumber(L, "startTime", e.startMs * 0.001);
|
||||
Game::Lua::SetFieldNumber(L, "duration",
|
||||
(e.endMs - e.startMs) * 0.001);
|
||||
Game::Lua::SetFieldNumber(L, "startTime",
|
||||
static_cast<double>(e.startMs) * 0.001);
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "duration", static_cast<double>(e.endMs - e.startMs) * 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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/>.
|
||||
|
||||
// Vanilla 1.12 has no RunMacro or RunMacroText global. Raw macro text is
|
||||
// dispatched one line at a time through FrameXML's ChatEdit_ParseText using a
|
||||
// small throwaway edit-box table. Saved macros are resolved with the stock
|
||||
// GetMacroIndexByName/GetMacroInfo globals before entering that same path.
|
||||
|
||||
#include "macro/Execute.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace Macro::Execute {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_GetText(void *L) {
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_Noop(void *) { return 0; }
|
||||
|
||||
int __fastcall Script_Index(void *L) {
|
||||
Game::Lua::PushCClosure(L, &Script_Noop, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Line(void *L, const char *line, size_t length) {
|
||||
if (length == 0)
|
||||
return;
|
||||
if (line[length - 1] == '\r')
|
||||
--length;
|
||||
if (length == 0)
|
||||
return;
|
||||
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
const int objectIndex = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, "GetText");
|
||||
Game::Lua::PushLString(L, line, static_cast<unsigned int>(length));
|
||||
Game::Lua::PushCClosure(L, &Script_GetText, 1);
|
||||
Game::Lua::SetTable(L, objectIndex);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
const int metatableIndex = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, "__index");
|
||||
Game::Lua::PushCClosure(L, &Script_Index, 0);
|
||||
Game::Lua::SetTable(L, metatableIndex);
|
||||
|
||||
if (Game::Lua::PushGlobalFunction(L, "setmetatable")) {
|
||||
Game::Lua::PushValue(L, objectIndex);
|
||||
Game::Lua::PushValue(L, metatableIndex);
|
||||
Game::Lua::PCall(L, 2, 0, 0);
|
||||
}
|
||||
|
||||
if (Game::Lua::PushGlobalFunction(L, "ChatEdit_ParseText")) {
|
||||
Game::Lua::PushValue(L, objectIndex);
|
||||
Game::Lua::PushNumber(L, 1);
|
||||
Game::Lua::PCall(L, 2, 0, 0);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
bool ParseIndex(const char *value, int *index) {
|
||||
if (value == nullptr || *value == '\0')
|
||||
return false;
|
||||
|
||||
int result = 0;
|
||||
for (const char *p = value; *p != '\0'; ++p) {
|
||||
if (*p < '0' || *p > '9')
|
||||
return false;
|
||||
|
||||
const int digit = *p - '0';
|
||||
if (result > (0x7FFFFFFF - digit) / 10)
|
||||
return false;
|
||||
|
||||
result = result * 10 + digit;
|
||||
}
|
||||
|
||||
if (result <= 0)
|
||||
return false;
|
||||
|
||||
*index = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CallRunMacro(void *L, const char *macro) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (!Game::Lua::PushGlobalFunction(L, "RunMacro")) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
if (ParseIndex(macro, &index))
|
||||
Game::Lua::PushNumber(L, index);
|
||||
else
|
||||
Game::Lua::PushString(L, macro);
|
||||
|
||||
const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0;
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
int ResolveIndex(void *L, const char *macro) {
|
||||
int index = 0;
|
||||
if (ParseIndex(macro, &index))
|
||||
return index;
|
||||
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (!Game::Lua::PushGlobalFunction(L, "GetMacroIndexByName")) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, macro);
|
||||
if (Game::Lua::PCall(L, 1, 1, 0) == 0 && Game::Lua::IsNumber(L, -1))
|
||||
index = static_cast<int>(Game::Lua::ToNumber(L, -1));
|
||||
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
bool GetBody(void *L, int index, std::string *body) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (!Game::Lua::PushGlobalFunction(L, "GetMacroInfo")) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, index);
|
||||
const bool called = Game::Lua::PCall(L, 1, 3, 0) == 0;
|
||||
const bool found = called && Game::Lua::IsString(L, -1);
|
||||
if (found)
|
||||
*body = Game::Lua::ToString(L, -1);
|
||||
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Text(void *L, const char *text) {
|
||||
if (L == nullptr || text == nullptr)
|
||||
return;
|
||||
|
||||
for (const char *line = text; *line != '\0';) {
|
||||
const char *end = line;
|
||||
while (*end != '\0' && *end != '\n')
|
||||
++end;
|
||||
Line(L, line, static_cast<size_t>(end - line));
|
||||
line = *end == '\n' ? end + 1 : end;
|
||||
}
|
||||
}
|
||||
|
||||
bool Saved(void *L, const char *macro) {
|
||||
if (L == nullptr || macro == nullptr || *macro == '\0')
|
||||
return false;
|
||||
|
||||
if (CallRunMacro(L, macro))
|
||||
return true;
|
||||
|
||||
const int index = ResolveIndex(L, macro);
|
||||
if (index <= 0)
|
||||
return false;
|
||||
|
||||
std::string body;
|
||||
if (!GetBody(L, index, &body))
|
||||
return false;
|
||||
|
||||
Text(L, body.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Macro::Execute
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 Macro::Execute {
|
||||
|
||||
// Runs raw macro text through vanilla FrameXML's ChatEdit_ParseText dispatcher.
|
||||
void Text(void *L, const char *text);
|
||||
|
||||
// Runs a saved macro by name or decimal index. Prefers an addon-provided
|
||||
// RunMacro implementation when present, then falls back to GetMacroInfo plus
|
||||
// the stock text dispatcher.
|
||||
bool Saved(void *L, const char *macro);
|
||||
|
||||
} // namespace Macro::Execute
|
||||
+22
-30
@@ -13,21 +13,20 @@
|
||||
|
||||
#include "map/Area.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
namespace Map::Area {
|
||||
|
||||
int RowForAreaID(uint32_t areaID) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_WMA_AREA_ID) ==
|
||||
areaID)
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_WMA_AREA_ID) == areaID)
|
||||
return id;
|
||||
}
|
||||
return -1;
|
||||
@@ -35,17 +34,15 @@ int RowForAreaID(uint32_t areaID) {
|
||||
|
||||
int CurrentViewRow() {
|
||||
const int continent =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_CONTINENT_INDEX);
|
||||
Game::Read<int>(Offsets::VAR_WORLDMAP_CONTINENT_INDEX);
|
||||
if (continent < 0)
|
||||
return *reinterpret_cast<const int *>(
|
||||
Offsets::VAR_WORLDMAP_DEFAULT_AREA_ROW);
|
||||
const uint8_t *blob = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_WORLDMAP_CONTINENT_DATA);
|
||||
return Game::Read<int>(Offsets::VAR_WORLDMAP_DEFAULT_AREA_ROW);
|
||||
const uint8_t *blob =
|
||||
Game::Read<const uint8_t *>(Offsets::VAR_WORLDMAP_CONTINENT_DATA);
|
||||
if (blob == nullptr)
|
||||
return -1;
|
||||
const uint8_t *entry = blob + continent * Offsets::WORLDMAP_CONTINENT_STRIDE;
|
||||
const int zone =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_ZONE_INDEX);
|
||||
const int zone = Game::Read<int>(Offsets::VAR_WORLDMAP_ZONE_INDEX);
|
||||
if (zone < 0)
|
||||
return *reinterpret_cast<const int *>(entry + 0x04);
|
||||
const int *zoneRows = *reinterpret_cast<const int *const *>(entry + 0x10);
|
||||
@@ -80,20 +77,19 @@ double OverlayMargin(int wmaRow, double mapXpct, double mapYpct) {
|
||||
const double cx = mapXpct / 100.0 * kZoneCanvasW;
|
||||
const double cy = mapYpct / 100.0 * kZoneCanvasH;
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
Game::Read<int>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
double best = -1.0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_OVERLAY_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_OVERLAY_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr ||
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_WORLDMAP_AREA) !=
|
||||
wmaRow)
|
||||
Game::Read<int>(rec, Offsets::OFF_WMO_WORLDMAP_AREA) != wmaRow)
|
||||
continue;
|
||||
const int hl = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_LEFT);
|
||||
const int hr = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_RIGHT);
|
||||
const int ht = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_TOP);
|
||||
const int hb = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_BOTTOM);
|
||||
const int hl = Game::Read<int>(rec, Offsets::OFF_WMO_HITRECT_LEFT);
|
||||
const int hr = Game::Read<int>(rec, Offsets::OFF_WMO_HITRECT_RIGHT);
|
||||
const int ht = Game::Read<int>(rec, Offsets::OFF_WMO_HITRECT_TOP);
|
||||
const int hb = Game::Read<int>(rec, Offsets::OFF_WMO_HITRECT_BOTTOM);
|
||||
const double sw = hr - hl, sh = hb - ht;
|
||||
if (sw <= 0.0 || sh <= 0.0)
|
||||
continue; // no / degenerate hit rect
|
||||
@@ -111,8 +107,7 @@ double OverlayMargin(int wmaRow, double mapXpct, double mapYpct) {
|
||||
|
||||
bool ZonePercent(int mapID, float x, float y, int *outAreaID, double *outMapX,
|
||||
double *outMapY) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
// Two candidate tracks over the zones whose rect contains the point:
|
||||
// land* — the point is on the zone's drawn landmass (an overlay hit rect
|
||||
// encloses it): authoritative, wins whenever any zone qualifies.
|
||||
@@ -132,10 +127,9 @@ bool ZonePercent(int mapID, float x, float y, int *outAreaID, double *outMapX,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_MAP_ID) != mapID)
|
||||
if (Game::Read<int>(rec, Offsets::OFF_WMA_MAP_ID) != mapID)
|
||||
continue;
|
||||
const int areaID =
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID);
|
||||
const int areaID = Game::Read<int>(rec, Offsets::OFF_WMA_AREA_ID);
|
||||
if (areaID == 0)
|
||||
continue; // continent-spanning row, not a zone
|
||||
|
||||
@@ -191,15 +185,14 @@ bool ZonePercent(int mapID, float x, float y, int *outAreaID, double *outMapX,
|
||||
}
|
||||
|
||||
bool PercentInZone(int areaID, float x, float y, double *outMapX, double *outMapY) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID) != areaID)
|
||||
if (Game::Read<int>(rec, Offsets::OFF_WMA_AREA_ID) != areaID)
|
||||
continue;
|
||||
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
|
||||
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
|
||||
@@ -219,17 +212,16 @@ bool PercentInZone(int areaID, float x, float y, double *outMapX, double *outMap
|
||||
}
|
||||
|
||||
bool ContinentPercent(int mapID, float x, float y, double *outPx, double *outPy) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_MAP_ID) != mapID)
|
||||
if (Game::Read<int>(rec, Offsets::OFF_WMA_MAP_ID) != mapID)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID) != 0)
|
||||
if (Game::Read<int>(rec, Offsets::OFF_WMA_AREA_ID) != 0)
|
||||
continue; // want the continent-spanning row
|
||||
|
||||
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
|
||||
|
||||
@@ -80,7 +80,7 @@ bool PushTriggerInfo(void *L, int id) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
|
||||
const int mapID = *reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID);
|
||||
const int mapID = Game::Read<int>(rec, Offsets::OFF_AT_MAP_ID);
|
||||
const float x = FloatField(rec, Offsets::OFF_AT_X);
|
||||
const float y = FloatField(rec, Offsets::OFF_AT_Y);
|
||||
const float z = FloatField(rec, Offsets::OFF_AT_Z);
|
||||
@@ -134,8 +134,7 @@ int __fastcall Script_GetAreaTriggers(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_AREATRIGGER_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_AREATRIGGER_COUNT);
|
||||
int outIdx = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATRIGGER_RECORDS,
|
||||
@@ -144,7 +143,7 @@ int __fastcall Script_GetAreaTriggers(void *L) {
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (filter &&
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID) != wantMap)
|
||||
Game::Read<int>(rec, Offsets::OFF_AT_MAP_ID) != wantMap)
|
||||
continue;
|
||||
|
||||
outIdx += 1;
|
||||
|
||||
+4
-6
@@ -67,8 +67,7 @@ int __fastcall Script_GetAreas(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_AREATABLE_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_AREATABLE_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name =
|
||||
DBC::AreaName(static_cast<uint32_t>(id), /*resolveToParent=*/false);
|
||||
@@ -98,18 +97,17 @@ int __fastcall Script_GetMapAreaIDs(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
const int count = Game::Read<int>(Offsets::VAR_WORLDMAP_AREA_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_AREA_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const int areaID = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID);
|
||||
const int areaID = Game::Read<int>(rec, Offsets::OFF_WMA_AREA_ID);
|
||||
if (areaID == 0)
|
||||
continue; // continent-spanning row, no zone id
|
||||
const char *dir = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_WMA_NAME);
|
||||
const char *dir = Game::Read<const char *>(rec, Offsets::OFF_WMA_NAME);
|
||||
if (dir == nullptr || dir[0] == '\0')
|
||||
continue;
|
||||
Game::Lua::SetFieldNumber(L, dir, static_cast<double>(areaID));
|
||||
|
||||
+12
-15
@@ -274,7 +274,7 @@ void ResolveTiles(ResolvedOverlay *out, const char *basePath, int dbcW,
|
||||
const ResolvedOverlay *CachedResolve(int overlayId, const char *basePath,
|
||||
int dbcW, int dbcH) {
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
Game::Read<int>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
if (g_cache == nullptr || g_cacheSize < count + 1) {
|
||||
auto **fresh = new ResolvedOverlay *[count + 1]();
|
||||
for (int i = 0; i < g_cacheSize; ++i)
|
||||
@@ -388,8 +388,7 @@ const uint8_t *ExplorationBits() {
|
||||
auto *player = static_cast<const uint8_t *>(fn("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
auto *sub = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
auto *sub = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
if (sub == nullptr)
|
||||
return nullptr;
|
||||
return sub + Offsets::OFF_PLAYER_EXPLORED_BITS;
|
||||
@@ -404,8 +403,8 @@ bool IsOverlayExplored(const uint8_t *overlayRec, const uint8_t *bits) {
|
||||
if (bits == nullptr)
|
||||
return false;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const int areaID = *reinterpret_cast<const int *>(
|
||||
overlayRec + Offsets::OFF_WMO_AREA_ID + k * 4);
|
||||
const int areaID =
|
||||
Game::Read<int>(overlayRec, Offsets::OFF_WMO_AREA_ID + k * 4);
|
||||
if (areaID <= 0)
|
||||
continue;
|
||||
const uint8_t *areaRec =
|
||||
@@ -413,10 +412,10 @@ bool IsOverlayExplored(const uint8_t *overlayRec, const uint8_t *bits) {
|
||||
Offsets::VAR_AREATABLE_COUNT, static_cast<uint32_t>(areaID));
|
||||
if (areaRec == nullptr)
|
||||
continue;
|
||||
if (*reinterpret_cast<const int *>(areaRec + Offsets::OFF_AREATABLE_EXPLORE_GATE) < 0)
|
||||
if (Game::Read<int>(areaRec, Offsets::OFF_AREATABLE_EXPLORE_GATE) < 0)
|
||||
return true; // area needs no exploration → always counts
|
||||
const int areaBit = *reinterpret_cast<const int *>(
|
||||
areaRec + Offsets::OFF_AREATABLE_AREA_BIT);
|
||||
const int areaBit =
|
||||
Game::Read<int>(areaRec, Offsets::OFF_AREATABLE_AREA_BIT);
|
||||
if (areaBit < 0)
|
||||
continue;
|
||||
const int byteIdx = areaBit >> 3;
|
||||
@@ -455,8 +454,7 @@ void PushZoneOverlays(void *L, int wmaRow, Filter filter) {
|
||||
static_cast<uint32_t>(wmaRow));
|
||||
const char *mapDir =
|
||||
(wmaRec != nullptr)
|
||||
? *reinterpret_cast<const char *const *>(wmaRec +
|
||||
Offsets::OFF_WMA_NAME)
|
||||
? Game::Read<const char *>(wmaRec, Offsets::OFF_WMA_NAME)
|
||||
: nullptr;
|
||||
|
||||
// Exploration bitfield only needed for the filtered variants.
|
||||
@@ -464,15 +462,14 @@ void PushZoneOverlays(void *L, int wmaRow, Filter filter) {
|
||||
(filter == Filter::All) ? nullptr : ExplorationBits();
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
Game::Read<int>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
|
||||
int outIdx = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_OVERLAY_RECORDS,
|
||||
Offsets::VAR_WORLDMAP_OVERLAY_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr ||
|
||||
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_WORLDMAP_AREA) !=
|
||||
wmaRow)
|
||||
Game::Read<int>(rec, Offsets::OFF_WMO_WORLDMAP_AREA) != wmaRow)
|
||||
continue;
|
||||
|
||||
if (filter != Filter::All) {
|
||||
@@ -483,8 +480,8 @@ void PushZoneOverlays(void *L, int wmaRow, Filter filter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *texName = *reinterpret_cast<const char *const *>(
|
||||
rec + Offsets::OFF_WMO_TEXTURE_NAME);
|
||||
const char *texName =
|
||||
Game::Read<const char *>(rec, Offsets::OFF_WMO_TEXTURE_NAME);
|
||||
const bool hasTexture = (texName != nullptr && texName[0] != '\0');
|
||||
|
||||
const auto fieldInt = [rec](uintptr_t off) {
|
||||
|
||||
@@ -60,10 +60,10 @@ int __fastcall Script_GetMapWorldSize(void *L) {
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
|
||||
const float left = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_LEFT);
|
||||
const float right = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_RIGHT);
|
||||
const float top = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_TOP);
|
||||
const float bottom = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_BOTTOM);
|
||||
const float left = Game::Read<float>(rec, Offsets::OFF_WMA_LOC_LEFT);
|
||||
const float right = Game::Read<float>(rec, Offsets::OFF_WMA_LOC_RIGHT);
|
||||
const float top = Game::Read<float>(rec, Offsets::OFF_WMA_LOC_TOP);
|
||||
const float bottom = Game::Read<float>(rec, Offsets::OFF_WMA_LOC_BOTTOM);
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(left) - right); // width (world Y span)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(top) - bottom); // height (world X span)
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
// 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/>.
|
||||
|
||||
// `Model:SetDisplayInfo(creatureDisplayID)` — backport of the modern Model
|
||||
// method that points a Model frame at a creature by its display ID, instead of
|
||||
// requiring a `.mdx` file path (`Model:SetModel`). Vanilla already loads models
|
||||
// by path; this just prepends the DBC resolution modern clients do internally:
|
||||
//
|
||||
// creatureDisplayID → CreatureDisplayInfo[id] +0x04 ModelID
|
||||
// → CreatureModelData[ModelID] +0x08 ModelPath
|
||||
// → Model:SetModel's worker (vtable +0x94)
|
||||
// → apply the display's TextureVariation skins
|
||||
// → character displays: dress via the engine compositor
|
||||
// (baked skin, hair, geosets, equipment — see the
|
||||
// "Character-display dressing" section below)
|
||||
//
|
||||
// The load path is the engine's own (`Script_SetModel` FUN_0076d950 calls the
|
||||
// same vtable +0x94 worker); we add the display-ID front end and the same
|
||||
// dressing a spawned unit gets. It is the foundational "display ID → loaded
|
||||
// model" step — a rendered portrait-by-display-ID
|
||||
// (SetPortraitTextureFromCreatureDisplayID) would build render-to-texture on
|
||||
// top of this, but a live Model frame needs neither.
|
||||
//
|
||||
// SetCreature(creatureID) is the entry-ID cousin: it resolves through the client
|
||||
// creature cache to a display ID, then runs the same load — see its note below.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "creature/Info.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Model::DisplayInfo {
|
||||
namespace {
|
||||
|
||||
// The CFrameScriptObject "is-a" vmethod, at vtable +0x10 for every frame type,
|
||||
// and the Model model-load worker at vtable +0x94 (the one Script_SetModel
|
||||
// invokes after resolving self). Both single-use here, so kept local.
|
||||
constexpr int kVmtIsA = 0x10;
|
||||
constexpr int kVmtLoadModel = 0x94;
|
||||
|
||||
using IsA_t = char(__thiscall *)(void *self, int typeId);
|
||||
using LoadModel_t = void(__thiscall *)(void *self, const char *path);
|
||||
using SetReplaceableTexture_t = void(__thiscall *)(void *model, int type, const char *path);
|
||||
|
||||
template <typename Fn>
|
||||
Fn Vmethod(void *obj, int byteOffset) {
|
||||
auto *vtbl = *reinterpret_cast<uint8_t **>(obj);
|
||||
return *reinterpret_cast<Fn *>(vtbl + byteOffset);
|
||||
}
|
||||
|
||||
// The Model frame-script type id. The engine assigns it lazily on the first
|
||||
// Model method call; mirror that assignment (FUN_0076d950's prologue) so a
|
||||
// type-check is valid even if no stock Model method has run yet.
|
||||
int ModelTypeId() {
|
||||
auto &typeId = Game::Ref<int>(Offsets::VAR_MODEL_LUA_TYPE_ID);
|
||||
if (typeId == 0) {
|
||||
auto &counter = Game::Ref<int>(Offsets::VAR_FRAMESCRIPT_TYPE_ID_COUNTER);
|
||||
counter += 1;
|
||||
typeId = counter;
|
||||
}
|
||||
return typeId;
|
||||
}
|
||||
|
||||
// Resolve arg1 to a Model object, or nullptr if it is not a Model. Mirrors the
|
||||
// resolve-then-typecheck prologue every Model method uses: generic object
|
||||
// resolve, then the IsA vmethod. The type-check matters here because we call a
|
||||
// Model vtable slot directly — a non-Model region would dispatch a wrong method.
|
||||
void *ResolveModel(void *L) {
|
||||
void *obj = Game::Lua::ResolveObject(L, 1);
|
||||
if (obj == nullptr)
|
||||
return nullptr;
|
||||
if (!Vmethod<IsA_t>(obj, kVmtIsA)(obj, ModelTypeId()))
|
||||
return nullptr;
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Apply a creature display's TextureVariation skins to the just-loaded model.
|
||||
// The variation columns are bare filenames that live in the model's own
|
||||
// directory; they fill the model's MONSTER_1/2/3 replaceable-texture slots
|
||||
// (M2/MDX texture types 11/12/13). FUN_0076cfe0 no-ops when no model is loaded,
|
||||
// so this is safe to call unconditionally after the load.
|
||||
void ApplySkins(void *model, const char *modelPath, const uint8_t *displayRec) {
|
||||
std::string dir(modelPath);
|
||||
const auto slash = dir.find_last_of("\\/");
|
||||
dir = (slash == std::string::npos) ? std::string() : dir.substr(0, slash + 1);
|
||||
|
||||
auto setTexture = reinterpret_cast<SetReplaceableTexture_t>(
|
||||
Offsets::FUN_MODEL_SET_REPLACEABLE_TEXTURE);
|
||||
constexpr int kMonsterSlot[3] = {11, 12, 13};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const char *variation = Game::Read<const char *>(
|
||||
displayRec, Offsets::OFF_CREATUREDISPLAYINFO_TEXTURE_VARIATION + i * 4);
|
||||
if (variation == nullptr || variation[0] == '\0')
|
||||
continue;
|
||||
const std::string skin = dir + variation;
|
||||
setTexture(model, kMonsterSlot[i], skin.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Character-display dressing via the engine compositor -------------------
|
||||
//
|
||||
// Character-based displays (CreatureDisplayInfo.extendedDisplayInfoID != 0) use
|
||||
// a shared Character\Race\Sex base model that the engine "dresses" per display:
|
||||
// baked body texture, hair texture, hair / facial-hair geosets (beards,
|
||||
// earrings, teeth), and equipment geosets (sleeves, robe→trousers, boots,
|
||||
// tabard, cape). Units run this through a compositor object
|
||||
// (CGUnit_C::UpdateCharacterCustomization, FUN_005fb200) once their model
|
||||
// loads; SetPortraitTexture waits on the same compositor, which is why
|
||||
// portraits always look right. We drive the identical compositor for the
|
||||
// Model frame, mirroring the unit flow field for field — see the
|
||||
// FUN_CHARCOMP_* notes in Offsets.h.
|
||||
//
|
||||
// Still not shown: weapons. NPC weapons come from the server-pushed
|
||||
// UNIT_VIRTUAL_ITEM_SLOT_DISPLAY fields on live units, not from any client
|
||||
// DBC, so a display ID alone can't produce them.
|
||||
|
||||
// The compositor info struct FUN_CHARCOMP_SET_INFO copies (0x5B dwords).
|
||||
// Layout read straight out of FUN_005fb200's NPC branch.
|
||||
struct CharCompInfo {
|
||||
uint32_t race; // +0x00 Extra +0x04
|
||||
uint32_t sex; // +0x04 Extra +0x08
|
||||
uint32_t hairColor; // +0x08 Extra +0x18
|
||||
uint32_t skin; // +0x0C Extra +0x0C
|
||||
uint32_t zero; // +0x10 engine passes 0
|
||||
uint32_t face; // +0x14 Extra +0x10
|
||||
uint32_t facialHair; // +0x18 Extra +0x1C
|
||||
uint32_t hairStyle; // +0x1C Extra +0x14
|
||||
void *model; // +0x20 AddRef'd by caller; builder owns the ref
|
||||
uint8_t useBake; // +0x24 1 = load bakePath instead of compositing
|
||||
char bakePath[263]; // +0x25 "Textures\BakedNpcTextures\<bake>"
|
||||
uint32_t geosetDefaults[16]; // +0x12C per-group defaults (see kGeosetGroupDefaults)
|
||||
};
|
||||
static_assert(sizeof(CharCompInfo) == 0x16C, "must match the engine's 0x5B-dword copy");
|
||||
|
||||
// The engine's geoset-group defaults from FUN_005fb200 — group N shows
|
||||
// N*100+1, except ears (group 7) whose "shown" variant is 702. The hair and
|
||||
// facial-hair groups get overwritten by the compositor's CharHairGeosets /
|
||||
// CharacterFacialHairStyles resolution.
|
||||
constexpr uint32_t kGeosetGroupDefaults[16] = {1, 101, 201, 301, 401, 501,
|
||||
601, 702, 801, 901, 1001, 1101,
|
||||
1201, 1301, 1401, 1501};
|
||||
|
||||
using CharCompCreate_t = void *(__cdecl *)();
|
||||
using CharCompDestroy_t = void(__fastcall *)(void *builder);
|
||||
using CharCompSetInfo_t = char(__thiscall *)(void *builder, const CharCompInfo *info);
|
||||
using CharCompSetItem_t = void(__thiscall *)(void *builder, int slot, int itemDisplayID);
|
||||
using CharCompPump_t = char(__thiscall *)(void *builder, int *unused);
|
||||
using ModelAddRef_t = void(__fastcall *)(void *instance);
|
||||
using ModelLoaded_t = int(__thiscall *)(void *instance, int tryLoad, int recurseChildren);
|
||||
|
||||
// A pending / finished dress for one Model frame. The model loads async, and
|
||||
// the engine only dresses a LOADED model (its poll gate FUN_00607da0 checks
|
||||
// FUN_MODEL_INSTANCE_LOADED first), so each job waits on WorldTick for the
|
||||
// load, configures the compositor once, then pumps it to completion. The
|
||||
// builder stays alive after that — units keep theirs for the model's whole
|
||||
// lifetime, and it holds the ref on the model instance — until the frame
|
||||
// loads another display or the reload teardown.
|
||||
struct DressJob {
|
||||
void *frame = nullptr; // CSimpleModelFFX that requested the dress
|
||||
void *instance = nullptr; // model instance the job is bound to
|
||||
const uint8_t *extraRec = nullptr; // CreatureDisplayInfoExtra (DBC memory, stable)
|
||||
void *builder = nullptr; // engine compositor (owned)
|
||||
bool configured = false;
|
||||
bool done = false;
|
||||
int ticks = 0;
|
||||
};
|
||||
|
||||
// Model frames are few and permanent; SetDisplayInfo users fewer still.
|
||||
DressJob g_jobs[8];
|
||||
// Give up on a model that never finishes loading (bad path, missing file).
|
||||
constexpr int kMaxDressTicks = 600;
|
||||
|
||||
void CancelJob(DressJob &job) {
|
||||
if (job.builder != nullptr)
|
||||
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(job.builder);
|
||||
job = DressJob{};
|
||||
}
|
||||
|
||||
DressJob *FindJob(void *frame) {
|
||||
for (auto &job : g_jobs)
|
||||
if (job.frame == frame)
|
||||
return &job;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Claim a slot for `frame`: its own existing slot, a free one, or — all busy —
|
||||
// a finished one (the dressed model keeps its geosets and texture refs; only
|
||||
// the kept-alive builder is dropped early).
|
||||
DressJob *ClaimJob(void *frame) {
|
||||
DressJob *slot = FindJob(frame);
|
||||
if (slot == nullptr)
|
||||
for (auto &job : g_jobs)
|
||||
if (job.frame == nullptr) {
|
||||
slot = &job;
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
for (auto &job : g_jobs)
|
||||
if (job.done) {
|
||||
slot = &job;
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_jobs[0];
|
||||
CancelJob(*slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Queue a dress for a character display just loaded into `frame`. 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).
|
||||
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,
|
||||
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
job->extraRec = extraRec;
|
||||
}
|
||||
|
||||
// One WorldTick step for one job: wait for the load, then configure + feed
|
||||
// equipment (the engine's order: SetInfo, then the 10 Extra equipment slots),
|
||||
// then pump until done. Field-for-field mirror of FUN_005fb200's NPC branch.
|
||||
void StepJob(DressJob &job) {
|
||||
void *instance = Game::Read<void *>(job.frame,
|
||||
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
if (job.instance == nullptr)
|
||||
job.instance = instance;
|
||||
if (instance == nullptr || instance != job.instance || ++job.ticks > kMaxDressTicks) {
|
||||
CancelJob(job); // model swapped out from under us / never loaded
|
||||
return;
|
||||
}
|
||||
|
||||
if (!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) {
|
||||
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);
|
||||
|
||||
job.configured = true;
|
||||
}
|
||||
|
||||
if (reinterpret_cast<CharCompPump_t>(Offsets::FUN_CHARCOMP_PUMP)(job.builder, nullptr))
|
||||
job.done = true; // builder kept — it owns the model ref for the dressed model
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
for (auto &job : g_jobs)
|
||||
if (job.frame != nullptr && !job.done)
|
||||
StepJob(job);
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe g_tick{&OnWorldTick};
|
||||
|
||||
// Resolve a creature display ID to its model file and skins, and load it into
|
||||
// the Model frame. Shared by SetDisplayInfo (display ID direct) and SetCreature
|
||||
// (creature entry ID -> cache -> display ID). No-op for an unknown display ID.
|
||||
void LoadDisplay(void *model, int displayID) {
|
||||
if (displayID <= 0)
|
||||
return;
|
||||
const uint8_t *displayRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_COUNT,
|
||||
static_cast<uint32_t>(displayID));
|
||||
if (displayRec == nullptr)
|
||||
return;
|
||||
const uint32_t modelID = Game::Read<uint32_t>(
|
||||
displayRec, Offsets::OFF_CREATUREDISPLAYINFO_MODEL_ID);
|
||||
|
||||
const char *modelPath = DBC::StringField(Offsets::VAR_CREATUREMODELDATA_RECORDS,
|
||||
Offsets::VAR_CREATUREMODELDATA_COUNT,
|
||||
modelID,
|
||||
Offsets::OFF_CREATUREMODELDATA_MODEL_PATH);
|
||||
if (modelPath == nullptr)
|
||||
return;
|
||||
|
||||
// Any pending or kept dress for this frame is about to point at the
|
||||
// wrong model — drop it before the load swaps the instance.
|
||||
if (DressJob *job = FindJob(model))
|
||||
CancelJob(*job);
|
||||
|
||||
Vmethod<LoadModel_t>(model, kVmtLoadModel)(model, modelPath);
|
||||
ApplySkins(model, modelPath, displayRec);
|
||||
|
||||
// Character-based display (Character\Race\Sex base): dress it through the
|
||||
// engine compositor, else body and hair render white with every geoset on.
|
||||
const uint32_t extID = Game::Read<uint32_t>(
|
||||
displayRec, Offsets::OFF_CREATUREDISPLAYINFO_EXTENDED_ID);
|
||||
if (extID != 0) {
|
||||
const uint8_t *extraRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFOEXTRA_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFOEXTRA_COUNT, extID);
|
||||
if (extraRec != nullptr)
|
||||
ScheduleDress(model, extraRec);
|
||||
}
|
||||
}
|
||||
|
||||
int __fastcall Script_SetDisplayInfo(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: Model:SetDisplayInfo(creatureDisplayID)");
|
||||
return 0;
|
||||
}
|
||||
void *model = ResolveModel(L);
|
||||
if (model == nullptr) {
|
||||
Game::Lua::Error(L, "Model:SetDisplayInfo(): self is not a Model");
|
||||
return 0;
|
||||
}
|
||||
LoadDisplay(model, static_cast<int>(Game::Lua::ToNumber(L, 2)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `Model:SetCreature(creatureID)` — the creature-entry cousin of SetDisplayInfo.
|
||||
// Vanilla has no client-side creature-entry -> display table in a DBC; that
|
||||
// mapping lives in the creature cache (creaturecache.wdb / SMSG_CREATURE_QUERY_
|
||||
// RESPONSE), keyed by entry ID and carrying the display ID. So this resolves the
|
||||
// entry through the cache, then loads exactly as SetDisplayInfo does.
|
||||
//
|
||||
// Limitation: only creatures already cached (queried this session or loaded from
|
||||
// the WDB at login) resolve; an uncached entry is a no-op. A caller that needs an
|
||||
// uncached creature can `C_CreatureInfo.RequestLoadCreatureByID` first, then call
|
||||
// this after `CREATURE_DATA_LOAD_RESULT`.
|
||||
int __fastcall Script_SetCreature(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: Model:SetCreature(creatureID)");
|
||||
return 0;
|
||||
}
|
||||
void *model = ResolveModel(L);
|
||||
if (model == nullptr) {
|
||||
Game::Lua::Error(L, "Model:SetCreature(): self is not a Model");
|
||||
return 0;
|
||||
}
|
||||
const int creatureID = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (creatureID <= 0)
|
||||
return 0;
|
||||
const uint32_t displayID = Creature::Info::DisplayID(static_cast<uint32_t>(creatureID));
|
||||
if (displayID == 0)
|
||||
return 0; // not cached -> no-op (see limitation above)
|
||||
LoadDisplay(model, static_cast<int>(displayID));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetDisplayInfo", &Script_SetDisplayInfo},
|
||||
{"SetCreature", &Script_SetCreature},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_MODEL_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
void PrepareForReload() {
|
||||
for (auto &job : g_jobs)
|
||||
CancelJob(job);
|
||||
}
|
||||
|
||||
} // namespace Model::DisplayInfo
|
||||
@@ -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
|
||||
|
||||
namespace Model::DisplayInfo {
|
||||
|
||||
// The reload teardown destroys every Model frame (and with it the model
|
||||
// instances our pending character-dress jobs point at). Drop all jobs and
|
||||
// destroy their engine compositors before the engine starts tearing down.
|
||||
void PrepareForReload();
|
||||
|
||||
} // namespace Model::DisplayInfo
|
||||
+35
-39
@@ -86,10 +86,10 @@ bool RestrictedOut(uint32_t mask, uint32_t index1Based) {
|
||||
// item's class *has* a proficiency concept (mask != 0) and the item's
|
||||
// subclass bit is unset.
|
||||
bool ProficiencyMet(const uint8_t *record) {
|
||||
const uint32_t itemClass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
const uint32_t itemClass = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClass = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
if (itemClass > 16) // table is 17 entries (m_class 0..16)
|
||||
return true;
|
||||
auto *table = reinterpret_cast<const uint32_t *>(
|
||||
@@ -105,27 +105,26 @@ bool ProficiencyMet(const uint8_t *record) {
|
||||
// item's SkillLine.dbc row to the player's skill slot; the player must
|
||||
// have the line at an effective rank (current + temp bonus) >= required.
|
||||
bool RequiredSkillMet(const uint8_t *player, const uint8_t *record) {
|
||||
const uint32_t reqSkill = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_SKILL);
|
||||
const uint32_t reqSkill = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_SKILL);
|
||||
if (reqSkill == 0)
|
||||
return true; // no skill requirement
|
||||
const uint32_t reqRank = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_SKILL_RANK);
|
||||
const uint32_t reqRank = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_SKILL_RANK);
|
||||
|
||||
auto toSlot = reinterpret_cast<SkillLineToSlot_t>(Offsets::FUN_SKILL_LINE_TO_SLOT);
|
||||
const int slot = toSlot(const_cast<uint8_t *>(player), reqSkill);
|
||||
if (slot < 0)
|
||||
return false; // player doesn't have the skill line at all
|
||||
|
||||
auto *sub = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
auto *sub = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
if (sub == nullptr)
|
||||
return false;
|
||||
const uint8_t *rec = sub + Offsets::OFF_SKILL_INFO_TABLE +
|
||||
slot * Offsets::SKILL_INFO_STRIDE;
|
||||
uint32_t rank = *reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_CUR);
|
||||
uint32_t rank = Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_CUR);
|
||||
if (rank != 0) // add the temp bonus only when there's a base value
|
||||
rank += *reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_BONUS);
|
||||
rank += Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_BONUS);
|
||||
return rank >= reqRank;
|
||||
}
|
||||
|
||||
@@ -138,11 +137,11 @@ bool RequiredSkillMet(const uint8_t *player, const uint8_t *record) {
|
||||
bool PlayerKnowsSpell(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
auto *bitmap = *reinterpret_cast<const uint32_t *const *>(
|
||||
auto *bitmap = Game::Read<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
const int spellCount = *reinterpret_cast<const int *>(
|
||||
const int spellCount = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount)
|
||||
return false;
|
||||
@@ -163,8 +162,8 @@ bool PlayerKnowsSpell(int spellID) {
|
||||
// specialization/proficiency in practice, so the chain walk is
|
||||
// belt-and-suspenders, but it makes the check exact.
|
||||
bool RequiredSpellMet(const uint8_t *player, const uint8_t *record) {
|
||||
const int reqSpell = *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_SPELL);
|
||||
const int reqSpell = Game::Read<int32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_SPELL);
|
||||
if (reqSpell <= 0)
|
||||
return true; // no requirement
|
||||
if (::Spell::Lookup::RecordForID(reqSpell) == nullptr)
|
||||
@@ -180,8 +179,8 @@ bool RequiredSpellMet(const uint8_t *player, const uint8_t *record) {
|
||||
// RequiredHonorRank — the player's PvP honor rank must be >= the item's.
|
||||
// `sub` is the CGPlayer +0xE68 sub-struct (may be null pre-world).
|
||||
bool RequiredHonorRankMet(const uint8_t *sub, const uint8_t *record) {
|
||||
const uint32_t req = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_HONOR_RANK);
|
||||
const uint32_t req = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_HONOR_RANK);
|
||||
if (req == 0)
|
||||
return true;
|
||||
if (sub == nullptr)
|
||||
@@ -193,14 +192,14 @@ bool RequiredHonorRankMet(const uint8_t *sub, const uint8_t *record) {
|
||||
// RequiredCityRank — a PvP-medal bitmask gate: the item's rank R passes
|
||||
// when the player has earned bit (R-1) in the medal mask.
|
||||
bool RequiredCityRankMet(const uint8_t *sub, const uint8_t *record) {
|
||||
const uint32_t req = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_CITY_RANK);
|
||||
const uint32_t req = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_CITY_RANK);
|
||||
if (req == 0)
|
||||
return true;
|
||||
if (sub == nullptr)
|
||||
return false;
|
||||
const uint32_t medals = *reinterpret_cast<const uint32_t *>(
|
||||
sub + Offsets::OFF_CGPLAYER_PVP_MEDALS);
|
||||
const uint32_t medals = Game::Read<uint32_t>(
|
||||
sub, Offsets::OFF_CGPLAYER_PVP_MEDALS);
|
||||
return (medals & (1u << ((req - 1) & 31))) != 0;
|
||||
}
|
||||
|
||||
@@ -209,17 +208,17 @@ bool RequiredCityRankMet(const uint8_t *sub, const uint8_t *record) {
|
||||
// indexes the engine's reaction-min threshold table; the player's total
|
||||
// rep with the faction must be >= that threshold.
|
||||
bool RequiredReputationMet(const uint8_t *record) {
|
||||
const int faction = *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_FACTION);
|
||||
const int faction = Game::Read<int32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_FACTION);
|
||||
if (faction == 0)
|
||||
return true;
|
||||
const uint32_t band = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_FACTION_RANK);
|
||||
const uint32_t band = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_FACTION_RANK);
|
||||
if (band > 7) // reaction-min table is 8 bands (0..7); guard OOB
|
||||
return true;
|
||||
auto getStanding = reinterpret_cast<RepStanding_t>(Offsets::FUN_REPUTATION_GET_STANDING);
|
||||
const int current = getStanding(faction);
|
||||
const int threshold = *reinterpret_cast<const int *>(
|
||||
const int threshold = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_REACTION_MIN_TABLE) + band * 4);
|
||||
return current >= threshold;
|
||||
}
|
||||
@@ -233,29 +232,27 @@ bool ComputeCanUse(int itemID) {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return false; // pre-login / no player
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
auto *desc = Game::Read<const uint8_t *>(player, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
|
||||
if (!ProficiencyMet(record))
|
||||
return false;
|
||||
|
||||
const int playerLevel = *reinterpret_cast<const int *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
const uint32_t reqLevel = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
const int playerLevel = Game::Read<int>(desc, Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
const uint32_t reqLevel = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
if (static_cast<int>(reqLevel) > playerLevel)
|
||||
return false;
|
||||
|
||||
const uint32_t allowClass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_ALLOWABLE_CLASS);
|
||||
const uint32_t allowClass = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_ALLOWABLE_CLASS);
|
||||
const uint32_t playerClass = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
if (RestrictedOut(allowClass, playerClass))
|
||||
return false;
|
||||
|
||||
const uint32_t allowRace = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_ALLOWABLE_RACE);
|
||||
const uint32_t allowRace = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_ALLOWABLE_RACE);
|
||||
const uint32_t playerRace = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
|
||||
if (RestrictedOut(allowRace, playerRace))
|
||||
return false;
|
||||
@@ -267,8 +264,7 @@ bool ComputeCanUse(int itemID) {
|
||||
return false;
|
||||
|
||||
// PvP rank + reputation gates read the CGPlayer +0xE68 sub-struct.
|
||||
auto *sub = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
auto *sub = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
if (!RequiredHonorRankMet(sub, record))
|
||||
return false;
|
||||
if (!RequiredCityRankMet(sub, record))
|
||||
|
||||
+53
-45
@@ -35,6 +35,7 @@
|
||||
// them, or they appear in guild login spam), they will be.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Info.h"
|
||||
#include "NameCache.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Names.h"
|
||||
@@ -50,6 +51,17 @@ namespace Player::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Bounded, always-terminated copy. Returns true iff a non-empty name landed.
|
||||
bool CopyName(char *dst, const char *src, size_t n) {
|
||||
if (dst == nullptr || n == 0)
|
||||
return false;
|
||||
size_t i = 0;
|
||||
for (; src && src[i] && i + 1 < n; ++i)
|
||||
dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
return dst[0] != '\0';
|
||||
}
|
||||
|
||||
// Thin shim: parses a GUID string and splits to hi/lo dwords. Shared parser
|
||||
// lives in `Guid::Parse`.
|
||||
bool ParseGUID(const char *str, uint32_t &outHi, uint32_t &outLo) {
|
||||
@@ -194,54 +206,14 @@ int __fastcall Script_UnitNameFromGUID(void *L) {
|
||||
Game::Lua::Error(L, "Usage: UnitNameFromGUID(\"0x...\")");
|
||||
return 0;
|
||||
}
|
||||
const char *guidStr = Game::Lua::ToString(L, 1);
|
||||
uint32_t hi, lo;
|
||||
if (!ParseGUID(guidStr, hi, lo))
|
||||
if (!ParseGUID(Game::Lua::ToString(L, 1), hi, lo))
|
||||
return 0;
|
||||
if (hi == 0 && lo == 0)
|
||||
char name[64];
|
||||
if (!NameFromGuid((static_cast<uint64_t>(hi) << 32) | lo, name, sizeof name))
|
||||
return 0;
|
||||
|
||||
// Object-manager path — TYPEMASK_OBJECT (0x01) is permissive; the
|
||||
// name getter at `FUN_OBJECT_GET_NAME` does its own type check and
|
||||
// safely returns the "UNKNOWNOBJECT" sentinel for non-unit
|
||||
// objects (gameobjects etc.), so we just gate on the sentinel
|
||||
// rather than pre-filtering by typemask.
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_OBJECT,
|
||||
(static_cast<uint64_t>(hi) << 32) | lo, nullptr, 0);
|
||||
if (obj != nullptr) {
|
||||
auto getName = reinterpret_cast<GetName_t>(Offsets::FUN_OBJECT_GET_NAME);
|
||||
const char *name = getName(obj, nullptr);
|
||||
if (name != nullptr && *name != '\0' &&
|
||||
std::strcmp(name, "UNKNOWNOBJECT") != 0 &&
|
||||
std::strcmp(name, "Unknown Being") != 0) {
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::PushString(L, "");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Friends-list fallback: the friend list keeps name + GUID for online
|
||||
// AND offline friends, so a friend resolves even while unsynced and
|
||||
// uncached. It needs no race/sex (this API returns only name + realm).
|
||||
if (const char *fname = FriendList::NameForGuid(
|
||||
(static_cast<uint64_t>(hi) << 32) | lo)) {
|
||||
Game::Lua::PushString(L, fname);
|
||||
Game::Lua::PushString(L, "");
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Object-manager miss / sentinel — fall back to the persistent
|
||||
// NameCache (when enabled). Covers ex-units no longer in the
|
||||
// engine's sync window.
|
||||
const std::string *cachedName = nullptr;
|
||||
const NameCache::Entry *cached = NameCache::Lookup(
|
||||
(static_cast<uint64_t>(hi) << 32) | lo, &cachedName);
|
||||
if (cached == nullptr || cachedName == nullptr || cachedName->empty())
|
||||
return 0;
|
||||
Game::Lua::PushString(L, cachedName->c_str());
|
||||
Game::Lua::PushString(L, "");
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::PushString(L, ""); // realm — always "" in vanilla
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -272,6 +244,42 @@ int __fastcall Script_C_PlayerCache_GetPlayerInfoByName(void *L) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// Resolution order matches vanilla's own `Script_UnitName` (0x00517020):
|
||||
// 1. Object manager — TYPEMASK_OBJECT (0x01) is permissive; the name getter
|
||||
// at `FUN_OBJECT_GET_NAME` does its own type check and routes through the
|
||||
// player NameCache for players AND the creature cache for NPCs, so it
|
||||
// resolves both with a single call. It returns the "UNKNOWNOBJECT"
|
||||
// sentinel for non-unit objects (gameobjects etc.), so we gate on that
|
||||
// rather than pre-filtering by typemask.
|
||||
// 2. Friends list — keeps name + GUID for online AND offline friends, so a
|
||||
// friend resolves even while unsynced and never seen in chat.
|
||||
// 3. Persistent NameCache (when enabled) — ex-units no longer in the sync
|
||||
// window (an ex-target who logged off, a chat name no longer visible).
|
||||
bool NameFromGuid(uint64_t guid, char *buf, size_t bufSize) {
|
||||
if (guid == 0 || buf == nullptr || bufSize == 0)
|
||||
return false;
|
||||
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
if (void *obj = Object::ByGuid(Offsets::TYPEMASK_OBJECT, guid, nullptr, 0)) {
|
||||
auto getName = reinterpret_cast<GetName_t>(Offsets::FUN_OBJECT_GET_NAME);
|
||||
const char *name = getName(obj, nullptr);
|
||||
if (name != nullptr && *name != '\0' &&
|
||||
std::strcmp(name, "UNKNOWNOBJECT") != 0 &&
|
||||
std::strcmp(name, "Unknown Being") != 0)
|
||||
return CopyName(buf, name, bufSize);
|
||||
}
|
||||
|
||||
if (const char *fname = FriendList::NameForGuid(guid))
|
||||
return CopyName(buf, fname, bufSize);
|
||||
|
||||
const std::string *cachedName = nullptr;
|
||||
const NameCache::Entry *cached = NameCache::Lookup(guid, &cachedName);
|
||||
if (cached != nullptr && cachedName != nullptr && !cachedName->empty())
|
||||
return CopyName(buf, cachedName->c_str(), bufSize);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetPlayerInfoByGUID",
|
||||
&Script_GetPlayerInfoByGUID);
|
||||
|
||||
@@ -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
|
||||
|
||||
// Shared GUID -> name resolution, factored out of `UnitNameFromGUID` so other
|
||||
// modules (e.g. `Spell::Cast`'s `UnitSpellTargetName`) can turn a GUID into a
|
||||
// display name without a Lua round-trip.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Player::Info {
|
||||
|
||||
// Resolve any object GUID to a display name -- players AND creatures. Chain:
|
||||
// object manager (the engine's polymorphic name getter) -> friends list ->
|
||||
// persistent NameCache. Writes a NUL-terminated name into `buf` and returns
|
||||
// true iff resolved; returns false (buf untouched) for GUID 0, a non-unit
|
||||
// object, or an unknown / uncached GUID. Passive -- never triggers a name
|
||||
// query.
|
||||
bool NameFromGuid(uint64_t guid, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Player::Info
|
||||
@@ -31,6 +31,7 @@
|
||||
// the free-look bit is held AND the camera yaw changes; STOPPED
|
||||
// fires on LMB release.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
@@ -67,7 +68,7 @@ bool g_havePrevCameraYaw = false;
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *Controller() {
|
||||
return *reinterpret_cast<const uint8_t *const *>(Offsets::VAR_UI_INPUT_CONTROLLER);
|
||||
return Game::Read<const uint8_t *>(Offsets::VAR_UI_INPUT_CONTROLLER);
|
||||
}
|
||||
|
||||
const uint8_t *Player() {
|
||||
@@ -76,12 +77,12 @@ const uint8_t *Player() {
|
||||
}
|
||||
|
||||
const uint8_t *Camera() {
|
||||
const uint8_t *gameState = *reinterpret_cast<const uint8_t *const *>(
|
||||
const uint8_t *gameState = Game::Read<const uint8_t *>(
|
||||
Offsets::VAR_GAME_STATE_PTR);
|
||||
if (gameState == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
gameState + Offsets::OFF_GAME_STATE_CAMERA_PTR);
|
||||
return Game::Read<const uint8_t *>(
|
||||
gameState, Offsets::OFF_GAME_STATE_CAMERA_PTR);
|
||||
}
|
||||
|
||||
void FireTransition(bool now, bool &prev, const char *startedName,
|
||||
@@ -109,8 +110,7 @@ void OnWorldTick() {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
ctrl + Offsets::OFF_UI_INPUT_FLAGS);
|
||||
const uint32_t flags = Game::Read<uint32_t>(ctrl, Offsets::OFF_UI_INPUT_FLAGS);
|
||||
const bool freeLookHeld = (flags & Offsets::INPUT_FLAG_FREE_LOOK) != 0;
|
||||
const bool mouselookHeld = (flags & Offsets::INPUT_FLAG_MOUSELOOK) != 0;
|
||||
|
||||
@@ -133,8 +133,7 @@ void OnWorldTick() {
|
||||
// Once STARTED has fired during a hold, the latch stays on
|
||||
// until the bit clears — so a drag-stop-drag motion within
|
||||
// the same RMB hold doesn't flap STARTED/STOPPED.
|
||||
const float bodyYaw = *reinterpret_cast<const float *>(
|
||||
player + Offsets::OFF_PLAYER_BODY_YAW);
|
||||
const float bodyYaw = Game::Read<float>(player, Offsets::OFF_PLAYER_BODY_YAW);
|
||||
bool turning;
|
||||
if (!mouselookHeld) {
|
||||
turning = false;
|
||||
@@ -159,8 +158,8 @@ void OnWorldTick() {
|
||||
} else if (camera == nullptr) {
|
||||
looking = false; // can't read camera yaw — bail
|
||||
} else {
|
||||
const float cameraYaw = *reinterpret_cast<const float *>(
|
||||
camera + Offsets::OFF_CAMERA_RELATIVE_YAW);
|
||||
const float cameraYaw = Game::Read<float>(
|
||||
camera, Offsets::OFF_CAMERA_RELATIVE_YAW);
|
||||
if (g_wasLooking) {
|
||||
looking = true;
|
||||
} else if (g_havePrevCameraYaw &&
|
||||
|
||||
@@ -52,7 +52,7 @@ const char *LookupQuestType(uint32_t row) {
|
||||
if (!record)
|
||||
return nullptr;
|
||||
const int localeIdx =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (localeIdx < 0 || localeIdx > 8)
|
||||
return nullptr;
|
||||
auto *names = reinterpret_cast<const char *const *>(record + 4);
|
||||
|
||||
+27
-27
@@ -83,8 +83,8 @@ const uint8_t *PlayerFields() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
return Game::Read<const uint8_t *>(
|
||||
player, Offsets::OFF_CGPLAYER_INFO);
|
||||
}
|
||||
|
||||
// Net spell damage for a 0-based school (0=physical .. 6=arcane). POS is the
|
||||
@@ -94,10 +94,10 @@ long SpellDamageForSchool(int school0) {
|
||||
const uint8_t *pf = PlayerFields();
|
||||
if (pf == nullptr)
|
||||
return 0;
|
||||
const int32_t pos = *reinterpret_cast<const int32_t *>(
|
||||
pf + Offsets::OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_POS + school0 * 4);
|
||||
const int32_t neg = *reinterpret_cast<const int32_t *>(
|
||||
pf + Offsets::OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_NEG + school0 * 4);
|
||||
const int32_t pos = Game::Read<int32_t>(
|
||||
pf, Offsets::OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_POS + school0 * 4);
|
||||
const int32_t neg = Game::Read<int32_t>(
|
||||
pf, Offsets::OFF_PLAYER_FIELD_MOD_DAMAGE_DONE_NEG + school0 * 4);
|
||||
return static_cast<long>(pos) - static_cast<long>(neg);
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@ long ItemFlatHealing(const uint8_t *cgItem) {
|
||||
// Random suffix ("… of Restoration") + applied permanent (slot 0) and
|
||||
// temporary (slot 1) enchants (Healing Power, Mana Oil, …).
|
||||
Item::StatAccum::ApplyRandomSuffix(
|
||||
acc, *reinterpret_cast<const int32_t *>(
|
||||
desc + Offsets::OFF_DESCRIPTOR_RANDOM_PROPERTY), +1);
|
||||
acc, Game::Read<int32_t>(
|
||||
desc, Offsets::OFF_DESCRIPTOR_RANDOM_PROPERTY), +1);
|
||||
for (int slot = 0; slot <= 1; ++slot)
|
||||
Item::StatAccum::ApplyEnchant(
|
||||
acc, *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_DESCRIPTOR_ENCHANTMENT_ID +
|
||||
acc, Game::Read<uint32_t>(
|
||||
desc, Offsets::OFF_DESCRIPTOR_ENCHANTMENT_ID +
|
||||
slot * Offsets::DESCRIPTOR_ENCHANTMENT_SLOT_STRIDE), +1);
|
||||
}
|
||||
return Item::StatAccum::Value(acc, "ITEM_MOD_SPELL_HEALING_DONE_SHORT");
|
||||
@@ -165,15 +165,15 @@ void AddSpellHealingAuras(long &total, int spellID, int32_t spirit,
|
||||
if (sp == nullptr)
|
||||
return;
|
||||
if (requirePassive &&
|
||||
!(*reinterpret_cast<const uint32_t *>(sp + Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
!(Game::Read<uint32_t>(sp, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
Offsets::SPELL_ATTR_PASSIVE))
|
||||
return;
|
||||
auto aura = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto base = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
auto dice = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
|
||||
auto aura = Game::Ptr<const int32_t>(
|
||||
sp, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto base = Game::Ptr<const int32_t>(
|
||||
sp, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
auto dice = Game::Ptr<const int32_t>(
|
||||
sp, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const long amount = static_cast<long>(base[i]) + static_cast<long>(dice[i]);
|
||||
if (aura[i] == kAuraModHealingDone)
|
||||
@@ -194,21 +194,21 @@ long ComputeHealing() {
|
||||
|
||||
// Player Spirit / Armor for the aura-175 / aura-199 conversions.
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
const int32_t spirit = desc ? *reinterpret_cast<const int32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_STAT_SPIRIT) : 0;
|
||||
const int32_t armor = desc ? *reinterpret_cast<const int32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_RESISTANCE_ARMOR) : 0;
|
||||
const int32_t spirit = desc ? Game::Read<int32_t>(
|
||||
desc, Offsets::OFF_UNIT_FIELD_STAT_SPIRIT) : 0;
|
||||
const int32_t armor = desc ? Game::Read<int32_t>(
|
||||
desc, Offsets::OFF_UNIT_FIELD_RESISTANCE_ARMOR) : 0;
|
||||
|
||||
// Active buffs — flat healing + Spirit/Armor conversions, one pass.
|
||||
auto *table = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_TABLE));
|
||||
for (int i = 0; i < Offsets::PLAYER_BUFF_TABLE_COUNT; ++i) {
|
||||
const uint8_t *entry = table + i * Offsets::PLAYER_BUFF_ENTRY_STRIDE;
|
||||
if (*reinterpret_cast<const int32_t *>(
|
||||
entry + Offsets::OFF_PLAYER_BUFF_SLOT_CODE) < 0)
|
||||
if (Game::Read<int32_t>(
|
||||
entry, Offsets::OFF_PLAYER_BUFF_SLOT_CODE) < 0)
|
||||
continue;
|
||||
AddSpellHealingAuras(total, *reinterpret_cast<const int32_t *>(
|
||||
entry + Offsets::OFF_PLAYER_BUFF_SPELL_ID),
|
||||
AddSpellHealingAuras(total, Game::Read<int32_t>(
|
||||
entry, Offsets::OFF_PLAYER_BUFF_SPELL_ID),
|
||||
spirit, armor, /*requirePassive=*/false);
|
||||
}
|
||||
|
||||
@@ -216,9 +216,9 @@ long ComputeHealing() {
|
||||
// and any passive flat healing, via the known-spell bitmap (bounded by
|
||||
// spellCount). The passive gate assumes ranks supersede (only the learned
|
||||
// rank is known), so no per-rank dedup is needed.
|
||||
auto *bitmap = *reinterpret_cast<const uint32_t *const *>(
|
||||
auto *bitmap = Game::Read<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
const int spellCount = *reinterpret_cast<const int *>(
|
||||
const int spellCount = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (bitmap != nullptr && spellCount > 0) {
|
||||
for (int id = 1; id <= spellCount; ++id)
|
||||
|
||||
+183
-35
@@ -73,9 +73,11 @@
|
||||
#include "dbc/Lookup.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "player/Info.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/CastEvents.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -85,7 +87,6 @@ namespace Spell::Cast {
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using TickMs_t = uint32_t(__fastcall *)();
|
||||
using GetCastTime_t = uint32_t(__fastcall *)(int spellID, int unit, int flag);
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord, int unit, int skipMod);
|
||||
|
||||
@@ -100,6 +101,13 @@ constexpr uint32_t SPELL_ATTR_TRADESPELL = 0x20;
|
||||
// shots (Auto Shot / Shoot, AttributesEx2 bit 0x20) are the one exclusion.
|
||||
constexpr uint32_t SPELL_ATTR_RANGED = 0x2;
|
||||
|
||||
// SMSG_SPELL_START target block: SpellCastTargets writes a u16 target mask,
|
||||
// then a packed GUID for the unit target when this bit is set (1.12 server
|
||||
// SpellCastTargets::write). Self / ground / item targets carry no unit GUID —
|
||||
// a cast with none reports no target. Per project convention TARGET_* flags
|
||||
// stay local (single-use, no drift).
|
||||
constexpr uint16_t TARGET_FLAG_UNIT = 0x2;
|
||||
|
||||
struct TrackedSpell {
|
||||
int spellID; // 0 = not casting / channeling
|
||||
int startMs;
|
||||
@@ -110,12 +118,27 @@ struct TrackedSpell {
|
||||
TrackedSpell g_cast{0, 0, 0, 0};
|
||||
TrackedSpell g_channel{0, 0, 0, 0};
|
||||
|
||||
// The unit GUID the player is currently casting / channeling AT (from the
|
||||
// SMSG_SPELL_START target block), or 0 for a self / ground / untargeted cast.
|
||||
// Reset on each fresh client-side stamp; filled when the confirming packet's
|
||||
// target lands. Read by `UnitSpellTargetName` and gated there on g_cast /
|
||||
// g_channel being active, so a stale value from a finished cast is never read.
|
||||
uint64_t g_castTargetGuid = 0;
|
||||
|
||||
// True when g_cast was stamped from SMSG_SPELL_START (a chained same-spell
|
||||
// recast the client cast path bailed on) rather than the client-side
|
||||
// FUN_CAST_START_SET hook. Such casts never set VAR_CURRENT_CAST_SPELL, so
|
||||
// the WorldTick VAR==0 clear must not touch them.
|
||||
bool g_castFromServer = false;
|
||||
|
||||
// Set when a movement-immune cast's engine cast-state was dropped by the player
|
||||
// MOVING (issue #23 — grenades). Such a cast isn't really interrupted; the
|
||||
// server completes it. So we keep tracking it instead of ending it, and clear
|
||||
// it on its SPELL_GO completion (→ STOP, no INTERRUPTED) or a grace past its end.
|
||||
// A real cancel / interrupt happens with the player stationary, so it clears
|
||||
// normally. Reset on every fresh cast stamp.
|
||||
bool g_castMoveDropped = false;
|
||||
|
||||
// Armed once the broadcast UNIT_FIELD_CHANNEL_SPELL (+0x228) has been seen to
|
||||
// reflect g_channel's spell — the enable for OnWorldTick's channel-stop poll.
|
||||
// StampChannel re-arms it (false) on every channel stamp so that field's
|
||||
@@ -132,7 +155,48 @@ void StampChannel(int spellID, int startMs, int endMs) {
|
||||
g_cast.spellID = 0;
|
||||
}
|
||||
|
||||
int NowMs() { return static_cast<int>(reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)()); }
|
||||
// Cast tracking keeps ticks in a signed int internally by design — see PushMs
|
||||
// for why that's safe (small deltas cancel; the Lua boundary re-casts to
|
||||
// uint32 so it matches GetTime()*1000 across the wrap). The reader itself
|
||||
// still comes from the one canonical source.
|
||||
int NowMs() { return static_cast<int>(Time::Clock::NowMs()); }
|
||||
|
||||
// A cast is interrupted by the caster moving only if its Spell.dbc InterruptFlags
|
||||
// carry SPELL_INTERRUPT_FLAG_MOVEMENT (the server's HandleMovementOpcodes and the
|
||||
// client's own CheckCast both gate the movement interrupt on this bit). Thrown
|
||||
// items — grenades — lack it. Unknown spell → assume interruptible (the safe
|
||||
// default: never keeps a phantom bar alive).
|
||||
bool MovementInterruptible(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
return true;
|
||||
return (Game::Read<uint32_t>(
|
||||
rec, Offsets::OFF_SPELL_RECORD_INTERRUPT_FLAGS) &
|
||||
Offsets::SPELL_INTERRUPT_FLAG_MOVEMENT) != 0;
|
||||
}
|
||||
|
||||
// Grace past a move-dropped cast's computed end before we give up on its
|
||||
// SPELL_GO and end it anyway (covers the SPELL_GO landing ~1 RTT after the
|
||||
// bar's computed end).
|
||||
constexpr int kMoveDroppedGraceMs = 750;
|
||||
|
||||
// Is the local player moving in a way that drops an in-progress cast? Mirrors
|
||||
// the engine's own cast-drop movement test (CheckCast masks the player movement
|
||||
// flags with MOVEFLAG_MASK_CAST_DROP). Used to tell a spurious movement drop of
|
||||
// a movement-immune cast (grenades) from a real cancel/interrupt (stationary).
|
||||
// False (the safe answer — clear normally) when the player object or its
|
||||
// movement block is missing.
|
||||
bool PlayerMoving() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
const uint8_t *move = Game::Read<const uint8_t *>(
|
||||
player, Offsets::OFF_UNIT_MOVEMENT_INFO_PTR);
|
||||
if (move == nullptr)
|
||||
return false;
|
||||
return (Game::Read<uint32_t>(move, Offsets::OFF_MOVEMENT_FLAGS) &
|
||||
Offsets::MOVEFLAG_MASK_CAST_DROP) != 0;
|
||||
}
|
||||
|
||||
// Pushes an engine-ms timestamp to Lua as an UNSIGNED 32-bit value.
|
||||
//
|
||||
@@ -180,9 +244,9 @@ int CastTimeMs(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec != nullptr) {
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
const uint32_t attrEx2 =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
if ((attr & SPELL_ATTR_RANGED) && !(attrEx2 & Offsets::SPELL_ATTR_EX2_AUTOREPEAT_FLAG))
|
||||
ms += 500;
|
||||
}
|
||||
@@ -203,12 +267,12 @@ int ChannelDurationMs(int spellID) {
|
||||
}
|
||||
|
||||
const char *SpellName(const uint8_t *rec) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
return *reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
return Game::Read<const char *>(rec, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
bool IsTradeskill(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
return (Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
SPELL_ATTR_TRADESPELL) != 0;
|
||||
}
|
||||
|
||||
@@ -216,13 +280,13 @@ bool IsTradeskill(const uint8_t *rec) {
|
||||
// already stores the full "Interface\Icons\..." path (unlike item icons,
|
||||
// which are bare filenames) — so it's used verbatim, no prefix.
|
||||
const char *SpellIconPath(const uint8_t *rec) {
|
||||
const int iconID = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const int iconID = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const uint8_t *iconRec = DBC::Record(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID));
|
||||
if (iconRec == nullptr)
|
||||
return "";
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
const char *path = Game::Read<const char *>(iconRec, Offsets::OFF_SPELLICON_PATH);
|
||||
return (path != nullptr) ? path : "";
|
||||
}
|
||||
|
||||
@@ -312,8 +376,10 @@ void __fastcall CastStartSet_h(int spellID, int targetState) {
|
||||
if (dur > 0) {
|
||||
const int now = NowMs();
|
||||
g_cast = TrackedSpell{spellID, now, now + dur, 0};
|
||||
g_castFromServer = false; // client-tracked; VAR==0 clears it
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
g_castFromServer = false; // client-tracked; VAR==0 clears it
|
||||
g_castMoveDropped = false; // fresh cast — clear the move-drop latch
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
g_castTargetGuid = 0; // target arrives with the confirming packet
|
||||
}
|
||||
// dur == 0: instant (no bar) or channel (handled via +0x228 poll);
|
||||
// leave g_cast — don't clobber an unrelated active cast.
|
||||
@@ -334,7 +400,7 @@ const Game::HookAutoRegister _castStartHook{
|
||||
// times to `UnitChannelInfo` (validated against the live +0x228 field).
|
||||
|
||||
bool IsChannelSpell(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
return (Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
@@ -348,6 +414,7 @@ int RemoteChannelDurationMs(const uint8_t *rec) {
|
||||
|
||||
struct RemoteCast {
|
||||
uint64_t casterGuid;
|
||||
uint64_t targetGuid; // unit the caster is casting at, 0 if none
|
||||
int spellID;
|
||||
int startMs;
|
||||
int endMs;
|
||||
@@ -359,9 +426,10 @@ constexpr int kRemoteCastSlots = 64;
|
||||
RemoteCast g_remoteCasts[kRemoteCastSlots];
|
||||
int g_remoteCursor = 0;
|
||||
|
||||
void StoreRemoteCast(uint64_t caster, int spellID, int startMs, int endMs,
|
||||
bool isChannel) {
|
||||
const RemoteCast entry{caster, spellID, startMs, endMs, isChannel, true};
|
||||
void StoreRemoteCast(uint64_t caster, uint64_t targetGuid, int spellID,
|
||||
int startMs, int endMs, bool isChannel) {
|
||||
const RemoteCast entry{caster, targetGuid, spellID,
|
||||
startMs, endMs, isChannel, true};
|
||||
// One active cast per unit — replace any existing entry for this caster.
|
||||
for (auto &e : g_remoteCasts) {
|
||||
if (e.used && e.casterGuid == caster) {
|
||||
@@ -397,7 +465,8 @@ const RemoteCast *FindRemoteCast(uint64_t caster) {
|
||||
// time so genuine chained casts aren't wrongly deduped.
|
||||
constexpr int kCastStartDedupMs = 500;
|
||||
|
||||
void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime,
|
||||
uint64_t targetGuid) {
|
||||
if (caster == 0 || spellID == 0)
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
@@ -419,8 +488,10 @@ void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
// Info) at endMs. (MSG_CHANNEL_START re-stamps moments later with
|
||||
// the server's duration — see ChannelStart_h.)
|
||||
const int dur = ChannelDurationMs(spellID);
|
||||
if (dur > 0)
|
||||
if (dur > 0) {
|
||||
StampChannel(spellID, now, now + dur);
|
||||
g_castTargetGuid = targetGuid;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// channel && castTime > 0 → a CAST-THEN-CHANNEL spell (Mind Control:
|
||||
@@ -453,11 +524,14 @@ void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
// pushback already accumulated). A no-op when client and server agree.
|
||||
g_cast.endMs =
|
||||
g_cast.startMs + static_cast<int>(castTime) + g_cast.delayMs;
|
||||
g_castTargetGuid = targetGuid; // confirming packet carries the target
|
||||
return;
|
||||
}
|
||||
g_cast = TrackedSpell{spellID, now, now + static_cast<int>(castTime), 0};
|
||||
g_castFromServer = true;
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
g_castMoveDropped = false; // fresh cast — clear the move-drop latch
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
g_castTargetGuid = targetGuid;
|
||||
return;
|
||||
}
|
||||
// A channeled spell with a cast time (Mind Control) is in its CAST phase
|
||||
@@ -467,7 +541,7 @@ void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
const bool instantChannel = channel && castTime == 0;
|
||||
const int endMs = instantChannel ? now + RemoteChannelDurationMs(rec)
|
||||
: now + static_cast<int>(castTime);
|
||||
StoreRemoteCast(caster, spellID, now, endMs, instantChannel);
|
||||
StoreRemoteCast(caster, targetGuid, spellID, now, endMs, instantChannel);
|
||||
// Phase 2 events: START (cast) or CHANNEL_START (channel). Skip pure
|
||||
// instants (non-channel, castTime 0 → no bar); their SUCCEEDED still fires
|
||||
// from the SPELL_GO hook.
|
||||
@@ -478,15 +552,23 @@ void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
|
||||
// SMSG_SPELL_START parse. Body (mirrored from nampower's SpellStartHandler):
|
||||
// itemGuid(packed), casterGuid(packed), spellId(u32), castFlags(u16),
|
||||
// castTime(u32). Runs from the Net::PacketDispatch funnel with the cursor
|
||||
// already positioned at the body.
|
||||
// castTime(u32), then the SpellCastTargets block: targetMask(u16) and — when
|
||||
// TARGET_FLAG_UNIT is set — a packed target GUID (the unit the cast is aimed
|
||||
// at; the field that backs UnitSpellTargetName). Runs from the
|
||||
// Net::PacketDispatch funnel with the cursor already positioned at the body.
|
||||
void ParseSpellStart(Net::CDataStore *packet) {
|
||||
Net::ReadPackedGuid(packet); // itemGuid (unused)
|
||||
const uint64_t caster = Net::ReadPackedGuid(packet);
|
||||
const int spellID = static_cast<int>(Net::Read<uint32_t>(packet));
|
||||
Net::Read<uint16_t>(packet); // castFlags (unused)
|
||||
const uint32_t castTime = Net::Read<uint32_t>(packet);
|
||||
HandleSpellStart(caster, spellID, castTime);
|
||||
// TARGET_FLAG_UNIT is the first field the server writes after the mask, so
|
||||
// the packed GUID sits immediately after it (SpellCastTargets::write). Other
|
||||
// target kinds (ground coords, item) carry no unit GUID → target is 0.
|
||||
const uint16_t targetMask = Net::Read<uint16_t>(packet);
|
||||
const uint64_t targetGuid =
|
||||
(targetMask & TARGET_FLAG_UNIT) ? Net::ReadPackedGuid(packet) : 0;
|
||||
HandleSpellStart(caster, spellID, castTime, targetGuid);
|
||||
}
|
||||
|
||||
// SMSG_SPELL_DELAYED — cast pushback. The server only sends it to the
|
||||
@@ -566,11 +648,20 @@ void HandleCastAborted(uint64_t guid, int spellID) {
|
||||
// caster receives their own broadcast is server-dependent; if it never
|
||||
// fires, the endMs self-expiry backstop still applies.
|
||||
if (guid == Unit::Identity::PlayerGuid()) {
|
||||
// Cast interrupts are surfaced by PollPlayer (g_castSucceeded). Player
|
||||
// Cast interrupts are surfaced by PollPlayer / SpellFailed_h. Player
|
||||
// CHANNELS never fire INTERRUPTED (retail only fires CHANNEL_STOP for
|
||||
// them, interrupted or not), so there's nothing to do for a channel.
|
||||
if (g_cast.spellID == spellID)
|
||||
g_cast.spellID = 0;
|
||||
if (g_cast.spellID == spellID) {
|
||||
// A movement-immune cast (grenade) whose cast-state the movement
|
||||
// path just cleared isn't really interrupted — the server completes
|
||||
// it (issue #23). Keep tracking it (mark it move-dropped); OnWorldTick
|
||||
// ends it on its SPELL_GO or a grace past its end. A real cancel /
|
||||
// interrupt happens with the player NOT moving, so it clears here.
|
||||
if (!MovementInterruptible(spellID) && PlayerMoving())
|
||||
g_castMoveDropped = true;
|
||||
else
|
||||
g_cast.spellID = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Phase 2: remote unit — the poll fires INTERRUPTED + STOP for it.
|
||||
@@ -611,8 +702,7 @@ ClearCastingSpell_t g_origClearCastingSpell = nullptr;
|
||||
void __fastcall ClearCastingSpell_h(void *unit, void *edx, int spellID,
|
||||
char notify, char cleanup) {
|
||||
if (unit != nullptr && spellID != 0) {
|
||||
const int current = *reinterpret_cast<const int *>(
|
||||
static_cast<const uint8_t *>(unit) + Offsets::OFF_UNIT_CAST_SPELL);
|
||||
const int current = Game::Read<int>(unit, Offsets::OFF_UNIT_CAST_SPELL);
|
||||
if (current != 0 && current == spellID)
|
||||
HandleCastAborted(Unit::Identity::GuidForObject(unit), spellID);
|
||||
}
|
||||
@@ -662,9 +752,21 @@ void OnWorldTick() {
|
||||
// so they're exempt — they expire on their computed endMs (self-expiry in
|
||||
// PushCastInfo).
|
||||
if (g_cast.spellID != 0 && !g_castFromServer &&
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CURRENT_CAST_SPELL) == 0)
|
||||
Game::Read<int>(Offsets::VAR_CURRENT_CAST_SPELL) == 0)
|
||||
g_cast.spellID = 0;
|
||||
|
||||
// End a move-dropped cast (issue #23): the engine cleared its cast-state on
|
||||
// movement, but the server completes it. HandleCastAborted kept it tracked;
|
||||
// end it once its SPELL_GO has landed (→ PollPlayer STOP, no INTERRUPTED) or,
|
||||
// if none ever comes, a grace past its computed end (→ honest INTERRUPTED +
|
||||
// STOP). Gated on the latch, so normal casts are untouched.
|
||||
if (g_cast.spellID != 0 && g_castMoveDropped &&
|
||||
(Spell::CastEvents::PlayerCastSucceeded() ||
|
||||
Time::Clock::Reached(static_cast<uint32_t>(g_cast.endMs + kMoveDroppedGraceMs)))) {
|
||||
g_cast.spellID = 0;
|
||||
g_castMoveDropped = false;
|
||||
}
|
||||
|
||||
// A target-selection spell (Disenchant, ground-target AoE, …) stamps
|
||||
// g_cast at the green-cursor step, NOT when the cast actually begins:
|
||||
// Spell_C_CastSpell (0x006E4B60) calls the cast-start writer
|
||||
@@ -680,9 +782,9 @@ void OnWorldTick() {
|
||||
// SENT castGUID); on cancel it simply stays cleared. Gated on
|
||||
// !g_castFromServer so it can never touch that server re-stamp.
|
||||
if (g_cast.spellID != 0 && !g_castFromServer &&
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_SPELL_TARGETING_FLAGS) != 0 &&
|
||||
Game::Read<int>(Offsets::VAR_SPELL_TARGETING_FLAGS) != 0 &&
|
||||
g_cast.spellID ==
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_PENDING_CAST_SPELL))
|
||||
Game::Read<int>(Offsets::VAR_PENDING_CAST_SPELL))
|
||||
g_cast.spellID = 0;
|
||||
|
||||
// Detect a player CHANNEL that ended before its computed endMs — the
|
||||
@@ -703,8 +805,8 @@ void OnWorldTick() {
|
||||
if (g_channel.spellID != 0) {
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
if (desc != nullptr) {
|
||||
const int chan = *reinterpret_cast<const int *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
const int chan = Game::Read<int>(
|
||||
desc, Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
if (chan == g_channel.spellID)
|
||||
g_channelConfirmed = true;
|
||||
else if (chan == 0 && g_channelConfirmed)
|
||||
@@ -791,14 +893,14 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
|
||||
if (u == Resolve("player"))
|
||||
return PushChannelInfo(L, g_channel.spellID, g_channel.startMs, g_channel.endMs, true);
|
||||
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(u) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
auto *desc = Game::Read<const uint8_t *>(
|
||||
u, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
// The live +0x228 field is authoritative for "is this unit channeling
|
||||
// right now"; the SMSG_SPELL_START cache adds real start/end times when
|
||||
// we observed the channel begin (and still matches the current spell).
|
||||
const int spellID = *reinterpret_cast<const int *>(desc + Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
const int spellID = Game::Read<int>(desc, Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
if (spellID == 0)
|
||||
return 0;
|
||||
const RemoteCast *rc = FindRemoteCast(Unit::Identity::GuidForObject(u));
|
||||
@@ -807,6 +909,49 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
|
||||
return PushChannelInfo(L, spellID, 0, 0, /*haveTimes*/ false);
|
||||
}
|
||||
|
||||
// The unit GUID `caster` is currently casting / channeling AT, or 0 when it
|
||||
// isn't casting or the spell has no unit target (self / ground / item). The
|
||||
// player comes from g_cast / g_channel + g_castTargetGuid, gated on an active
|
||||
// cast so a stale target from a finished cast is never returned; other units
|
||||
// come from the SMSG_SPELL_START cache while inside their cast window.
|
||||
static uint64_t TargetGuidForCaster(uint64_t caster) {
|
||||
if (caster == 0)
|
||||
return 0;
|
||||
if (caster == Unit::Identity::PlayerGuid()) {
|
||||
const int now = NowMs();
|
||||
const bool casting = g_cast.spellID != 0 && now < g_cast.endMs;
|
||||
const bool channeling =
|
||||
g_channel.spellID != 0 && (g_channel.endMs == 0 || now < g_channel.endMs);
|
||||
return (casting || channeling) ? g_castTargetGuid : 0;
|
||||
}
|
||||
const RemoteCast *rc = FindRemoteCast(caster);
|
||||
return (rc != nullptr && NowMs() < rc->endMs) ? rc->targetGuid : 0;
|
||||
}
|
||||
|
||||
// `UnitSpellTargetName(unit)` — name of the unit that `unit` is currently
|
||||
// casting / channeling a spell at, or nil when `unit` isn't casting, the spell
|
||||
// has no unit target (self / ground / item), or the target's name can't be
|
||||
// resolved (an off-screen stranger). ClassicAPI extension; the cast's target
|
||||
// comes from the SMSG_SPELL_START target block, captured for the player and
|
||||
// any remote unit whose cast we've observed.
|
||||
static int __fastcall Script_UnitSpellTargetName(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitSpellTargetName(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
const uint64_t caster =
|
||||
(token != nullptr) ? Unit::Identity::GuidForToken(token) : 0;
|
||||
const uint64_t targetGuid = TargetGuidForCaster(caster);
|
||||
char name[64];
|
||||
if (targetGuid == 0 || !Player::Info::NameFromGuid(targetGuid, name, sizeof name)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
// Registered under C_Spell rather than as globals to avoid clobbering
|
||||
// the global `UnitCastingInfo` / `UnitChannelInfo` names. Addons that
|
||||
@@ -820,6 +965,9 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "CastingInfo", &Script_CastingInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "UnitChannelInfo", &Script_UnitChannelInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "ChannelInfo", &Script_ChannelInfo);
|
||||
// A novel ClassicAPI name (no addon ships its own), so it's safe as a
|
||||
// global — and it matches the `UnitSpellTargetName(unit)` call shape.
|
||||
Game::Lua::RegisterGlobalFunction("UnitSpellTargetName", &Script_UnitSpellTargetName);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -44,9 +44,9 @@ static const char *LocaleNameForID(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return nullptr;
|
||||
const int locale = *reinterpret_cast<const int *>(
|
||||
const int locale = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
return *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
return Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
// Hook on the single name → spellbook-slot resolver
|
||||
|
||||
+32
-11
@@ -19,6 +19,7 @@
|
||||
#include "net/PacketReader.h"
|
||||
#include "net/SendObserver.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdio>
|
||||
@@ -66,8 +67,8 @@ constexpr uint32_t kAutoShotSpellId = 75;
|
||||
constexpr uint16_t kTargetFlagUnit = 0x0002;
|
||||
|
||||
const char *LocalizedField(const uint8_t *rec, int fieldOffset) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
return *reinterpret_cast<const char *const *>(rec + fieldOffset + locale * 4);
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
return Game::Read<const char *>(rec, fieldOffset + locale * 4);
|
||||
}
|
||||
|
||||
// Cast UID for a REAL (Type-3) cast — player and remote alike. Per the retail
|
||||
@@ -178,6 +179,17 @@ int g_castGuid = 0; // castGUID counter value for the tracked cast
|
||||
int g_castDelay = 0; // last-seen accumulated pushback (ms)
|
||||
bool g_castSucceeded = false; // SPELL_GO seen for the tracked cast this cast
|
||||
|
||||
// UNIT_SPELLCAST_INTERRUPTED de-dup. SpellFailed_h and PollPlayer can both
|
||||
// surface the same interrupt: the client fires Spell_C_SpellFailed AND drops the
|
||||
// tracked cast (e.g. an Esc cancel of a normal spell, or a movement interrupt).
|
||||
// SpellFailed_h stamps here when it fires INTERRUPTED; PollPlayer skips its own
|
||||
// for the same spell within the window, so one interrupt = one event. A
|
||||
// SuperWoW-style kick clears the cast WITHOUT a Spell_C_SpellFailed, so no stamp
|
||||
// is made and PollPlayer still owns those.
|
||||
int g_lastInterruptSpell = 0;
|
||||
int g_lastInterruptTMs = 0;
|
||||
constexpr int kInterruptDedupMs = 1000;
|
||||
|
||||
int g_chanSpell = 0;
|
||||
int g_chanGuid = 0;
|
||||
|
||||
@@ -257,15 +269,14 @@ constexpr int kChanSuccDeferMs = 500;
|
||||
bool IsChanneledSpell(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
return rec != nullptr &&
|
||||
(*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
(Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
int NowMs() {
|
||||
using TickMs_t = uint32_t(__fastcall *)();
|
||||
return static_cast<int>(reinterpret_cast<TickMs_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))());
|
||||
}
|
||||
// Only used for relative windows (`now - stampMs < window`), where the int
|
||||
// deltas cancel and stay correct across the wrap. Reader from the one
|
||||
// canonical source; see `Time::Clock`.
|
||||
int NowMs() { return static_cast<int>(Time::Clock::NowMs()); }
|
||||
|
||||
// ---- UNIT_SPELLCAST_FAILED (client-side cast rejection) ----------------
|
||||
// Co-hook `Spell_C_SpellFailed` — the local player's own cast failures
|
||||
@@ -305,6 +316,8 @@ void __fastcall SpellFailed_h(uint32_t spellId, int result, int unk1, int unk2,
|
||||
int guid = g_castSpell == sid ? g_castGuid
|
||||
: recentCast ? g_endedGuid
|
||||
: NextCastGuid(sid);
|
||||
g_lastInterruptSpell = sid;
|
||||
g_lastInterruptTMs = now;
|
||||
Fire(kInterrupted, sid, guid);
|
||||
return;
|
||||
}
|
||||
@@ -426,6 +439,8 @@ RemoteEvt *AllocRemoteEvt(uint64_t guid) {
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PlayerCastSucceeded() { return g_castSucceeded; }
|
||||
|
||||
void PollPlayer(int castSpellID, int castStartMs, int castDelayMs,
|
||||
int channelSpellID, int channelStartMs) {
|
||||
// ---- Regular cast -------------------------------------------------
|
||||
@@ -441,7 +456,13 @@ void PollPlayer(int castSpellID, int castStartMs, int castDelayMs,
|
||||
// completion's SPELL_GO (and thus OnPlayerSucceeded) has already run
|
||||
// a frame earlier, so the flag reliably tells the two apart even
|
||||
// though the engine routes both through the same abort choke point.
|
||||
if (!g_castSucceeded)
|
||||
// Skip if SpellFailed_h already fired INTERRUPTED for this cast (de-dup
|
||||
// — see g_lastInterruptSpell); still fire STOP.
|
||||
const bool alreadyInterrupted =
|
||||
g_lastInterruptSpell == g_castSpell &&
|
||||
Time::Clock::Elapsed(static_cast<uint32_t>(g_lastInterruptTMs)) <
|
||||
static_cast<uint32_t>(kInterruptDedupMs);
|
||||
if (!g_castSucceeded && !alreadyInterrupted)
|
||||
Fire(kInterrupted, g_castSpell, g_castGuid);
|
||||
Fire(kStop, g_castSpell, g_castGuid);
|
||||
g_endedSpell = g_castSpell; // for SpellFailed_h's interrupt-vs-fail gate
|
||||
@@ -531,14 +552,14 @@ void OnPlayerSucceeded(int spellID) {
|
||||
|
||||
void PollReticle() {
|
||||
const bool active =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_SPELL_TARGETING_FLAGS) != 0;
|
||||
Game::Read<int>(Offsets::VAR_SPELL_TARGETING_FLAGS) != 0;
|
||||
if (active) {
|
||||
if (s_reticleSpell == 0) {
|
||||
// Reticle just came up — report the pending spell. Read the spellID
|
||||
// now (it's set by Spell_C_CastSpell before targeting begins); if
|
||||
// it's transiently 0 this frame, retry next tick.
|
||||
const int spellID =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_PENDING_CAST_SPELL);
|
||||
Game::Read<int>(Offsets::VAR_PENDING_CAST_SPELL);
|
||||
if (spellID != 0) {
|
||||
s_reticleSpell = spellID;
|
||||
s_reticlePlaced = false;
|
||||
|
||||
@@ -69,6 +69,13 @@ void PollReticle();
|
||||
// order matches modern (CHANNEL_START → SUCCEEDED).
|
||||
void OnPlayerSucceeded(int spellID);
|
||||
|
||||
// True once a SPELL_GO has been seen for the currently-tracked player cast
|
||||
// (set by OnPlayerSucceeded, reset when a new cast starts). `Spell::Cast` reads
|
||||
// it to tell a completed cast from an interrupted one when deciding whether to
|
||||
// keep a movement-immune cast alive through a spurious cast-global drop (the
|
||||
// grenade case, issue #23).
|
||||
bool PlayerCastSucceeded();
|
||||
|
||||
// Fire `UNIT_SPELLCAST_CHANNEL_UPDATE` for the local player. Called from the
|
||||
// MSG_CHANNEL_UPDATE hook (`Spell::Cast`) when a pushback re-anchors the
|
||||
// player's active channel end. Reuses the tracked channel's castGUID (set by
|
||||
|
||||
+44
-13
@@ -11,10 +11,14 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `CastSpellNoToggle(name | spellID)` — spam-safe variant of
|
||||
// `CastSpellNoToggle(name | spellID [, unit])` — spam-safe variant of
|
||||
// `CastSpellByName` that won't toggle off an already-active self-aura
|
||||
// or auto-repeat. Covers everything the modern `/cast !Name` syntax
|
||||
// covers in vanilla terms:
|
||||
// or auto-repeat. The optional second argument is a unit token to cast
|
||||
// at (issue #22), e.g. `CastSpellNoToggle("Auto Shot", "focus")` or
|
||||
// `CastSpellNoToggle("Shoot", "targettarget")`: the spell fires at that
|
||||
// unit without changing your current target. When it is omitted the call
|
||||
// behaves exactly as before. Covers everything the modern `/cast !Name`
|
||||
// syntax covers in vanilla terms:
|
||||
//
|
||||
// - Auto-repeat: Shoot, Auto-Shot, Wand. Engine tracks via the
|
||||
// `VAR_ACTIVE_AUTO_REPEAT_SPELL` global.
|
||||
@@ -41,6 +45,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/Arg.h"
|
||||
#include "spell/AtUnit.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
|
||||
@@ -61,7 +66,7 @@ const CastSpellByName_t Script_CastSpellByName_Engine =
|
||||
reinterpret_cast<CastSpellByName_t>(Offsets::FUN_SCRIPT_CAST_SPELL_BY_NAME);
|
||||
|
||||
int ReadActiveSpellID() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
return Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTIVE_AUTO_REPEAT_SPELL));
|
||||
}
|
||||
|
||||
@@ -69,9 +74,9 @@ const char *LocaleName(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return nullptr;
|
||||
const int locale = *reinterpret_cast<const int *>(
|
||||
const int locale = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
return *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
return Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
}
|
||||
|
||||
// Case-insensitive name match for the auto-repeat-name gate. Walks
|
||||
@@ -150,10 +155,25 @@ int __fastcall Script_CastSpellNoToggle(void *L) {
|
||||
// could shift it and invalidate `s`.
|
||||
std::snprintf(nameBuf, sizeof(nameBuf), "%s", s);
|
||||
} else {
|
||||
Game::Lua::Error(L, "Usage: CastSpellNoToggle(\"name\" | spellID)");
|
||||
Game::Lua::Error(L, "Usage: CastSpellNoToggle(\"name\" | spellID [, \"unit\"])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Optional arg2 — a unit token to cast at (issue #22), e.g.
|
||||
// CastSpellNoToggle("Auto Shot", "focus"). Copy it off Lua's string
|
||||
// heap now, before the toggle gates and the SetTop below can shift the
|
||||
// stack. A non-string arg2 (nil / absent) leaves unitToken null and the
|
||||
// call keeps its original single-arg behavior.
|
||||
char unitBuf[64];
|
||||
const char *unitToken = nullptr;
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_STRING) {
|
||||
const char *u = Game::Lua::ToString(L, 2);
|
||||
if (u != nullptr && *u != '\0') {
|
||||
std::snprintf(unitBuf, sizeof(unitBuf), "%s", u);
|
||||
unitToken = unitBuf;
|
||||
}
|
||||
}
|
||||
|
||||
// Check 1 — auto-repeat (Shoot / Auto-Shot / Wand). Cheap global
|
||||
// read; the spell-cast subsystem owns this state separately from
|
||||
// the aura system.
|
||||
@@ -181,12 +201,23 @@ int __fastcall Script_CastSpellNoToggle(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Neither toggle would fire — safe to cast. Delegate to the engine
|
||||
// with a fresh, one-arg stack so `lua_toboolean(L, 2)` (the
|
||||
// `onSelf` flag inside Script_CastSpellByName) sees nil → false.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, nameBuf);
|
||||
Script_CastSpellByName_Engine(L);
|
||||
// Neither toggle would fire — safe to cast.
|
||||
if (unitToken != nullptr) {
|
||||
// Cast at the given unit without disturbing the current target —
|
||||
// reuses AtUnit's resolve→GUID→dispatch core. A unit-target /
|
||||
// auto-repeat spell fires straight at the unit; a ground spell lands
|
||||
// at its feet. Casts by name so numeric and string input match the
|
||||
// no-unit path (highest known rank). A genuinely unknown token raises
|
||||
// the engine's standard "Unknown unit" error, same as UnitHealth.
|
||||
Spell::AtUnit::CastByName(nameBuf, unitToken);
|
||||
} else {
|
||||
// Delegate to the engine with a fresh, one-arg stack so
|
||||
// `lua_toboolean(L, 2)` (the `onSelf` flag inside
|
||||
// Script_CastSpellByName) sees nil → false.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, nameBuf);
|
||||
Script_CastSpellByName_Engine(L);
|
||||
}
|
||||
|
||||
// Reflect the post-cast state: true if either toggle is now active.
|
||||
const bool autoRepeatNow = ReadActiveSpellID() != 0;
|
||||
|
||||
+10
-4
@@ -31,10 +31,16 @@ namespace Spell::Cooldown {
|
||||
|
||||
namespace {
|
||||
|
||||
// The engine writes *unsigned* millisecond ticks into `outStart`/`outDuration`
|
||||
// (same OS counter, same epoch as `GetTime()`). Reading `outStart` into a
|
||||
// signed `int` makes every timestamp negative once the machine has been up
|
||||
// past 2^31 ms (~24.9 days) — `startTime + duration` then lands a month in the
|
||||
// past and callers see every cooldown as already elapsed. Keep them unsigned;
|
||||
// see `Time::Clock` for the full rationale.
|
||||
using QueryCooldown_t = void(__fastcall *)(int spellID, int bookType,
|
||||
int *outDuration,
|
||||
int *outStart,
|
||||
int *outEnable);
|
||||
uint32_t *outDuration,
|
||||
uint32_t *outStart,
|
||||
uint32_t *outEnable);
|
||||
|
||||
int __fastcall Script_C_Spell_GetSpellCooldown(void *L) {
|
||||
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
|
||||
@@ -43,7 +49,7 @@ int __fastcall Script_C_Spell_GetSpellCooldown(void *L) {
|
||||
|
||||
auto fn = reinterpret_cast<QueryCooldown_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SPELL_QUERY_COOLDOWN));
|
||||
int durationMs = 0, startMs = 0, enable = 0;
|
||||
uint32_t durationMs = 0, startMs = 0, enable = 0;
|
||||
fn(spellID, 0 /* bookType=player */, &durationMs, &startMs, &enable);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
+91
-22
@@ -129,26 +129,26 @@ static bool ReadSpellInfo(int spellID, SpellInfoData &out) {
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
|
||||
out.spellID = spellID;
|
||||
out.name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
out.rank = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
out.name = Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
out.rank = Game::Read<const char *>(record, Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
|
||||
out.iconPath = nullptr;
|
||||
const int iconID = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const int iconID = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
if (auto *iconRec = LookupSubRecord(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT, iconID)) {
|
||||
out.iconPath = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
out.iconPath = Game::Read<const char *>(iconRec, Offsets::OFF_SPELLICON_PATH);
|
||||
}
|
||||
|
||||
out.cost = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_MANA_COST);
|
||||
out.cost = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_MANA_COST);
|
||||
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
const uint32_t attrEx2 = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
out.isFunnel = (attrEx2 & SPELL_ATTR_EX2_HEALTH_FUNNEL) != 0;
|
||||
|
||||
out.powerType = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
out.powerType = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
|
||||
out.castTimeMs = 0;
|
||||
const int castIndex = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_CASTING_TIME_INDEX);
|
||||
const int castIndex = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_CASTING_TIME_INDEX);
|
||||
if (auto *castRec = LookupSubRecord(Offsets::VAR_SPELL_CAST_TIMES_RECORDS,
|
||||
Offsets::VAR_SPELL_CAST_TIMES_COUNT, castIndex)) {
|
||||
out.castTimeMs = *reinterpret_cast<const int *>(castRec + 4);
|
||||
@@ -156,7 +156,7 @@ static bool ReadSpellInfo(int spellID, SpellInfoData &out) {
|
||||
|
||||
out.minRange = 0.0f;
|
||||
out.maxRange = 0.0f;
|
||||
const int rangeIndex = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
const int rangeIndex = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (auto *rangeRec = LookupSubRecord(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
Offsets::VAR_SPELL_RANGE_COUNT, rangeIndex)) {
|
||||
out.minRange = *reinterpret_cast<const float *>(rangeRec + 4);
|
||||
@@ -276,7 +276,7 @@ static int __fastcall Script_C_GetSpellName(void *L) {
|
||||
return 0; // nil for unknown spellID
|
||||
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
const char *name = Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return 0; // empty / no name in current locale → nil
|
||||
Game::Lua::PushString(L, name);
|
||||
@@ -289,12 +289,12 @@ static int __fastcall Script_C_GetSpellTexture(void *L) {
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
|
||||
const int iconID = *reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
const int iconID = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
auto *iconRec = LookupSubRecord(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT, iconID);
|
||||
if (iconRec == nullptr)
|
||||
return 0;
|
||||
const char *path = *reinterpret_cast<const char *const *>(iconRec + Offsets::OFF_SPELLICON_PATH);
|
||||
const char *path = Game::Read<const char *>(iconRec, Offsets::OFF_SPELLICON_PATH);
|
||||
if (path == nullptr || *path == '\0')
|
||||
return 0;
|
||||
Game::Lua::PushString(L, path);
|
||||
@@ -315,7 +315,7 @@ static bool BuildSpellLink(int spellID, char *out, size_t outLen) {
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const int locale = ReadGlobal<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
const char *name = Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
const int n = std::snprintf(out, outLen, "|cff71d5ff|Hspell:%d:0|h[%s]|h|r",
|
||||
@@ -392,8 +392,8 @@ static int PushIsPassive(void *L, int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
const uint32_t attr = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
const uint32_t attr = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
Game::Lua::PushBool(L, (attr & Offsets::SPELL_ATTR_PASSIVE) != 0);
|
||||
return 1;
|
||||
}
|
||||
@@ -438,11 +438,11 @@ static int __fastcall Script_C_IsSpellPassive(void *L) {
|
||||
static bool PlayerKnowsSpell(int spellID) {
|
||||
if (spellID < 1)
|
||||
return false;
|
||||
const int spellCount = *reinterpret_cast<const int *>(
|
||||
const int spellCount = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount)
|
||||
return false;
|
||||
auto *bitmap = *reinterpret_cast<const uint32_t *const *>(
|
||||
auto *bitmap = Game::Read<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
@@ -522,10 +522,10 @@ template <typename Pred>
|
||||
static bool AnyEffectTarget(const uint8_t *record, Pred pred) {
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
auto *targetsA = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
auto *targetsB = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B);
|
||||
auto *targetsA = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
auto *targetsB = Game::Ptr<const uint32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_B);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (pred(targetsA[i]) || pred(targetsB[i]))
|
||||
return true;
|
||||
@@ -617,6 +617,69 @@ static const Game::HookAutoRegister _hookUnlearnSpell{
|
||||
Offsets::FUN_UNLEARN_SPELL, reinterpret_cast<void *>(&UnlearnSpell_h),
|
||||
reinterpret_cast<void **>(&g_origUnlearnSpell)};
|
||||
|
||||
// Enum.SpellBookItemType values (retail). Vanilla's spellbook only ever
|
||||
// produces real spells, so we emit Spell (player book) or PetAction (pet
|
||||
// book); None/FutureSpell/Flyout don't occur in 1.12.
|
||||
static constexpr int kSpellBookItemTypeSpell = 1;
|
||||
static constexpr int kSpellBookItemTypePetAction = 3;
|
||||
|
||||
static const Game::Lua::EnumIntegerEntry kSpellBookSpellBankEntries[] = {
|
||||
{"Player", 0}, {"Pet", 1},
|
||||
};
|
||||
static const Game::Lua::EnumIntegerEntry kSpellBookItemTypeEntries[] = {
|
||||
{"None", 0}, {"Spell", 1}, {"FutureSpell", 2}, {"PetAction", 3},
|
||||
{"Flyout", 4},
|
||||
};
|
||||
|
||||
// `C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)` -> table.
|
||||
// `slotIndex` is 1-based across the whole book; `spellBank` is
|
||||
// Enum.SpellBookSpellBank (0 = Player, 1 = Pet). Returns nil for an empty or
|
||||
// out-of-range slot. Vanilla's spellbook holds only real spells, so `itemType`
|
||||
// is always Spell (player) or PetAction (pet).
|
||||
//
|
||||
// Field deviations from retail, all forced by 1.12 lacking the data:
|
||||
// - `iconID` is the icon PATH string (vanilla has no fileID), same as
|
||||
// C_Spell.GetSpellInfo.iconID -- feed it straight to texture:SetTexture.
|
||||
// - `isOffSpec` is always false (vanilla has no specializations).
|
||||
// - `skillLineIndex` is omitted (nil) -- spellbook tabs aren't SkillLines.
|
||||
static int __fastcall Script_C_SpellBook_GetSpellBookItemInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // modern returns nil (not an error) for a bad index
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int bank = Game::Lua::IsNumber(L, 2)
|
||||
? static_cast<int>(Game::Lua::ToNumber(L, 2))
|
||||
: 0;
|
||||
const int bookType = (bank == 1) ? 1 : 0; // 1 = pet book, else player
|
||||
|
||||
const int spellID = Spell::Lookup::SpellbookSlotToID(slot, bookType);
|
||||
if (spellID <= 0)
|
||||
return 0; // empty slot / out of range -> nil
|
||||
|
||||
SpellInfoData info;
|
||||
if (!ReadSpellInfo(spellID, info))
|
||||
return 0;
|
||||
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
const uint32_t attr =
|
||||
record ? Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES)
|
||||
: 0u;
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
SetField(L, "itemType",
|
||||
static_cast<double>(bookType == 1 ? kSpellBookItemTypePetAction
|
||||
: kSpellBookItemTypeSpell));
|
||||
SetField(L, "actionID", static_cast<double>(spellID));
|
||||
SetField(L, "spellID", static_cast<double>(spellID));
|
||||
SetField(L, "name", info.name);
|
||||
// subName carries the rank text ("Rank N"); modern uses "" when absent.
|
||||
SetField(L, "subName", info.rank ? info.rank : "");
|
||||
SetField(L, "iconID", info.iconPath); // path string -- see note above
|
||||
SetFieldBool(L, "isPassive", (attr & Offsets::SPELL_ATTR_PASSIVE) != 0);
|
||||
SetFieldBool(L, "isOffSpec", false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellInfo", &Script_GetSpellInfo);
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellLink", &Script_GetSpellLink);
|
||||
@@ -637,6 +700,12 @@ static void RegisterLuaFunctions() {
|
||||
&Script_C_Spell_IsSpellHarmful);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellHelpful",
|
||||
&Script_C_Spell_IsSpellHelpful);
|
||||
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSpellBookItemInfo",
|
||||
&Script_C_SpellBook_GetSpellBookItemInfo);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SpellBookSpellBank",
|
||||
kSpellBookSpellBankEntries, 2);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SpellBookItemType",
|
||||
kSpellBookItemTypeEntries, 5);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+34
-34
@@ -33,8 +33,8 @@ const uint8_t *PlayerDescriptor() {
|
||||
auto *player = static_cast<const uint8_t *>(fn("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<const uint8_t *>(
|
||||
player, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
}
|
||||
|
||||
// Builds (or returns the cached) set of every spellID that appears
|
||||
@@ -51,9 +51,9 @@ std::unordered_set<int> &TalentSpellSet() {
|
||||
static std::unordered_set<int> set;
|
||||
if (!set.empty())
|
||||
return set;
|
||||
const uint8_t *const *records = *reinterpret_cast<const uint8_t *const *const *>(
|
||||
const uint8_t *const *records = Game::Read<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_TALENT_DBC_RECORDS));
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
const int count = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_TALENT_DBC_COUNT));
|
||||
if (records == nullptr || count <= 0)
|
||||
return set;
|
||||
@@ -63,8 +63,8 @@ std::unordered_set<int> &TalentSpellSet() {
|
||||
const uint8_t *rec = records[i];
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
auto *ranks = reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_TALENT_SPELL_RANK);
|
||||
auto *ranks = Game::Ptr<const uint32_t>(
|
||||
rec, Offsets::OFF_TALENT_SPELL_RANK);
|
||||
for (int j = 0; j < 9; ++j) {
|
||||
const uint32_t spellID = ranks[j];
|
||||
if (spellID != 0)
|
||||
@@ -80,8 +80,8 @@ int SpellBaseLevel(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL);
|
||||
return Game::Read<int32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_BASE_LEVEL);
|
||||
}
|
||||
|
||||
// Spell.dbc attribute filter for the learnable-spell walk. Only the
|
||||
@@ -110,8 +110,8 @@ bool SpellHiddenFromSpellbook(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const uint32_t attributes = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
const uint32_t attributes = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
return (attributes & SPELL_ATTR_HIDDEN_CLIENTSIDE) != 0;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ constexpr uint32_t SPELL_EFFECT_APPLY_AURA = 6;
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 35;
|
||||
|
||||
int LocaleIndex() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
return Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
}
|
||||
|
||||
@@ -169,8 +169,8 @@ bool IsPositiveRankTarget(uint32_t t) {
|
||||
// single-rank buffs (Power Infusion / Innervate / Blessing of Kings, all
|
||||
// empty Rank and un-gated) vs ranked ones (Divine Spirit / Fortitude).
|
||||
bool HasRankString(const uint8_t *record) {
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + LocaleIndex() * 4);
|
||||
const char *rank = Game::Read<const char *>(
|
||||
record, Offsets::OFF_SPELL_RECORD_RANK + LocaleIndex() * 4);
|
||||
return rank != nullptr && *rank != '\0';
|
||||
}
|
||||
|
||||
@@ -178,19 +178,19 @@ bool HasRankString(const uint8_t *record) {
|
||||
// the spell isn't subject to the ranked-positive-aura target-level rule.
|
||||
int RequiredTargetLevel(const uint8_t *record) {
|
||||
const int spellLevel =
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_SPELL_LEVEL);
|
||||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_SPELL_LEVEL);
|
||||
if (spellLevel <= 10) // targetLevel + 10 >= spellLevel holds for any level
|
||||
return 0;
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & Offsets::SPELL_ATTR_PASSIVE)
|
||||
return 0;
|
||||
if (!HasRankString(record)) // single-rank spell → not gated
|
||||
return 0;
|
||||
|
||||
auto *effect = reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
auto *effect = Game::Ptr<const uint32_t>(record, Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
auto *targetA =
|
||||
reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
Game::Ptr<const uint32_t>(record, Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
bool positiveAura = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if ((effect[i] == SPELL_EFFECT_APPLY_AURA &&
|
||||
@@ -221,13 +221,13 @@ int __fastcall Script_GetSpellLevelInfo(void *L) {
|
||||
return 0;
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_SPELL_LEVEL)));
|
||||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_SPELL_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL)));
|
||||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_BASE_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_MAX_LEVEL)));
|
||||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_MAX_LEVEL)));
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -308,8 +308,8 @@ int __fastcall Script_GetCurrentLevelSpells(void *L) {
|
||||
|
||||
const uint8_t classByte = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
const uint8_t raceByte = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
|
||||
const int32_t playerLevel = *reinterpret_cast<const int32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
const int32_t playerLevel = Game::Read<int32_t>(
|
||||
desc, Offsets::OFF_UNIT_FIELD_LEVEL);
|
||||
if (classByte == 0 || raceByte == 0 || playerLevel <= 0)
|
||||
return 1;
|
||||
|
||||
@@ -337,9 +337,9 @@ int __fastcall Script_GetCurrentLevelSpells(void *L) {
|
||||
(1u << 0) | (1u << 1) | (1u << 2) | (1u << 3) | (1u << 4) |
|
||||
(1u << 6) | (1u << 7) | (1u << 8) | (1u << 10);
|
||||
|
||||
const uint8_t *const *records = *reinterpret_cast<const uint8_t *const *const *>(
|
||||
const uint8_t *const *records = Game::Read<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SKILL_LINE_ABILITY_RECORDS));
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
const int count = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SKILL_LINE_ABILITY_COUNT));
|
||||
if (records == nullptr || count <= 0)
|
||||
return 1;
|
||||
@@ -357,14 +357,14 @@ int __fastcall Script_GetCurrentLevelSpells(void *L) {
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
|
||||
const uint32_t recClassMask = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SLA_CLASS_MASK);
|
||||
const uint32_t recRaceMask = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SLA_RACE_MASK);
|
||||
const uint32_t recExcludeClass = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SLA_EXCLUDE_CLASS);
|
||||
const uint32_t recExcludeRace = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SLA_EXCLUDE_RACE);
|
||||
const uint32_t recClassMask = Game::Read<uint32_t>(
|
||||
rec, Offsets::OFF_SLA_CLASS_MASK);
|
||||
const uint32_t recRaceMask = Game::Read<uint32_t>(
|
||||
rec, Offsets::OFF_SLA_RACE_MASK);
|
||||
const uint32_t recExcludeClass = Game::Read<uint32_t>(
|
||||
rec, Offsets::OFF_SLA_EXCLUDE_CLASS);
|
||||
const uint32_t recExcludeRace = Game::Read<uint32_t>(
|
||||
rec, Offsets::OFF_SLA_EXCLUDE_RACE);
|
||||
|
||||
// Skip if explicitly excluded.
|
||||
if ((recExcludeClass & playerClassBit) != 0)
|
||||
@@ -406,8 +406,8 @@ int __fastcall Script_GetCurrentLevelSpells(void *L) {
|
||||
if (!isClassSpell && !isRacial)
|
||||
continue;
|
||||
|
||||
const int spellID = *reinterpret_cast<const int *>(
|
||||
rec + Offsets::OFF_SLA_SPELL_ID);
|
||||
const int spellID = Game::Read<int>(
|
||||
rec, Offsets::OFF_SLA_SPELL_ID);
|
||||
if (spellID <= 0)
|
||||
continue;
|
||||
if (SpellBaseLevel(spellID) != queryLevel)
|
||||
|
||||
@@ -53,7 +53,7 @@ static const char *MechanicName(uint32_t mechanicID) {
|
||||
Offsets::VAR_SPELLMECHANIC_COUNT, mechanicID);
|
||||
if (rec == nullptr)
|
||||
return nullptr;
|
||||
auto names = reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELLMECHANIC_NAME);
|
||||
auto names = Game::Ptr<const char *const>(rec, Offsets::OFF_SPELLMECHANIC_NAME);
|
||||
const char *s = names[0]; // enUS
|
||||
return (s != nullptr && s[0] != '\0') ? s : nullptr;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ static int __fastcall Script_GetSpellMechanicByID(void *L) {
|
||||
return 0; // nil for invalid / out-of-range spell IDs
|
||||
|
||||
const uint32_t mechanicID =
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_MECHANIC);
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_MECHANIC);
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(mechanicID));
|
||||
|
||||
|
||||
+8
-8
@@ -32,7 +32,7 @@ uint32_t PlayerSpellFamily() {
|
||||
auto *unit = static_cast<const uint8_t *>(resolve("player"));
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(unit + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
const uint8_t classByte = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
@@ -40,7 +40,7 @@ uint32_t PlayerSpellFamily() {
|
||||
Offsets::VAR_CHRCLASSES_COUNT, classByte);
|
||||
if (cls == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint32_t *>(cls + Offsets::OFF_CHRCLASSES_SPELL_FAMILY);
|
||||
return Game::Read<uint32_t>(cls, Offsets::OFF_CHRCLASSES_SPELL_FAMILY);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -49,18 +49,18 @@ float Apply(const uint8_t *spellRecord, int op, float base) {
|
||||
if (spellRecord == nullptr)
|
||||
return base;
|
||||
|
||||
const uint32_t familyName = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_FAMILY_NAME);
|
||||
const uint32_t attrEx3 = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX3);
|
||||
const uint32_t familyName = Game::Read<uint32_t>(
|
||||
spellRecord, Offsets::OFF_SPELL_RECORD_FAMILY_NAME);
|
||||
const uint32_t attrEx3 = Game::Read<uint32_t>(
|
||||
spellRecord, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX3);
|
||||
if (familyName == 0 ||
|
||||
(attrEx3 & Offsets::SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS) != 0)
|
||||
return base;
|
||||
if (familyName != PlayerSpellFamily())
|
||||
return base;
|
||||
|
||||
const uint64_t familyFlags = *reinterpret_cast<const uint64_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_FAMILY_FLAGS);
|
||||
const uint64_t familyFlags = Game::Read<uint64_t>(
|
||||
spellRecord, Offsets::OFF_SPELL_RECORD_FAMILY_FLAGS);
|
||||
if (familyFlags == 0)
|
||||
return base;
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ int __fastcall Script_GetSpellPowerCost(void *L) {
|
||||
if (cost == 0xFFFFFFFF)
|
||||
return 0; // engine error (no player context, etc.)
|
||||
|
||||
const int powerType = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
const int costPercent = *reinterpret_cast<const int *>(rec + Offsets::OFF_SPELL_RECORD_MANA_COST_PERCENT);
|
||||
const int powerType = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
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
|
||||
|
||||
|
||||
@@ -61,14 +61,14 @@ namespace Spell::Radius {
|
||||
static float MaxBaseRadius(const uint8_t *record) {
|
||||
float best = -1.0f;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int radiusIndex = *reinterpret_cast<const int *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_RADIUS_INDEX + i * 4);
|
||||
const int radiusIndex = Game::Read<int>(
|
||||
record, Offsets::OFF_SPELL_RECORD_EFFECT_RADIUS_INDEX + i * 4);
|
||||
const uint8_t *rad = DBC::Record(Offsets::VAR_SPELL_RADIUS_RECORDS,
|
||||
Offsets::VAR_SPELL_RADIUS_COUNT,
|
||||
static_cast<uint32_t>(radiusIndex));
|
||||
if (rad == nullptr)
|
||||
continue;
|
||||
const float r = *reinterpret_cast<const float *>(rad + Offsets::OFF_SPELL_RADIUS_VALUE);
|
||||
const float r = Game::Read<float>(rad, Offsets::OFF_SPELL_RADIUS_VALUE);
|
||||
if (r > best)
|
||||
best = r;
|
||||
}
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Range.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "unit/Position.h"
|
||||
@@ -41,7 +42,7 @@ bool SpellIDHasRange(int spellID) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
const uint32_t rangeIdx =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (rangeIdx == 0)
|
||||
return false;
|
||||
const uint8_t *rangeRec = DBC::Record(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user