mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 15:10:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555ea02447 | ||
|
|
e3f5318747 | ||
|
|
cd674aa115 | ||
|
|
f91ffd3189 | ||
|
|
8361ae01eb | ||
|
|
5d785ea0d3 | ||
|
|
e84673dde5 | ||
|
|
a2c5db7b76 | ||
|
|
46597f6587 | ||
|
|
4e1c1863e9 | ||
|
|
a4c57614bc | ||
|
|
e1a9d9b5c4 | ||
|
|
cd280ac307 | ||
|
|
564495248c | ||
|
|
1f3f3cb9f6 |
@@ -43,3 +43,4 @@ Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Server-side DoT-duration adjustments the 1.12 client is never told about.
|
||||
--
|
||||
-- ClassicAPI derives C_UnitAuras expirationTime for a debuff on another unit
|
||||
-- from the cast packet (SMSG_SPELL_GO). The vanilla server never retransmits a
|
||||
-- LATER server-side duration change on a non-self unit -- verified in the
|
||||
-- server source: the change runs through SetAuraDuration / RefreshHolder, and
|
||||
-- the only duration packet (SMSG_UPDATE_AURA_DURATION) is self-scoped to the
|
||||
-- aura-bearer, so an observing caster hears nothing (and on a mob the packet
|
||||
-- isn't even built). We can't hear the change, but we DO see the *triggering*
|
||||
-- cast, so C_UnitAuras.RegisterAuraDurationModifier mirrors the server's edit
|
||||
-- off that trigger.
|
||||
--
|
||||
-- A trigger is matched by exact spellID (stable across ranks -- these are the
|
||||
-- IDs the server binds its spell script to); the affected aura by
|
||||
-- SpellFamilyName + a family-flag overlap (rank-proof, exactly how the server
|
||||
-- scripts locate it). Flags below are from the client's Spell.dbc. When the
|
||||
-- trigger is a whole class of spells rather than a named ability,
|
||||
-- RegisterAuraDurationModifierByTrigger matches it by SpellFamilyName + school
|
||||
-- index instead (see Shadow Weaving below).
|
||||
--
|
||||
-- NOT included on purpose: Carnage's Rip/Rake refresh fires behind a
|
||||
-- server-side probability roll on Ferocious Bite. The client can't know the
|
||||
-- roll's outcome, so inferring it would show wrong timers. If you accept that,
|
||||
-- register it yourself, e.g. (DRUID = 7):
|
||||
-- C_UnitAuras.RegisterAuraDurationModifier(<ferociousBiteID>, 7, tonumber("800000",16), 108, "refresh") -- Rip
|
||||
-- C_UnitAuras.RegisterAuraDurationModifier(<ferociousBiteID>, 7, tonumber("1000",16), 494, "refresh") -- Rake
|
||||
-- Conflagrate's *full*-consume variant (other servers) removes Immolate
|
||||
-- outright; that clears the aura slot and ClassicAPI evicts it via the normal
|
||||
-- removal path, so the reduce rule below is harmless there and correct on
|
||||
-- Turtle (where Immolate keeps ticking with 3s shaved off).
|
||||
|
||||
if TURTLE_WOW_VERSION then
|
||||
local WARLOCK, SHAMAN = 5, 11
|
||||
-- Lua 5.0 has no 0x hex literals; tonumber(hex, 16) keeps the flags readable.
|
||||
local IMMOLATE_FLAG = tonumber("4", 16) -- CF_WARLOCK_IMMOLATE (bit 2)
|
||||
local FLAMESHOCK_FLAG = tonumber("10000000", 16) -- CF_SHAMAN_FLAME_SHOCK (bit 28)
|
||||
|
||||
-- Conflagrate (all ranks) -> shave 3s off the caster's Immolate.
|
||||
local CONFLAGRATE = { 17962, 18930, 18931, 18932 }
|
||||
for i = 1, table.getn(CONFLAGRATE) do
|
||||
C_UnitAuras.RegisterAuraDurationModifier(CONFLAGRATE[i], WARLOCK, IMMOLATE_FLAG, 0, "reduce", 3)
|
||||
end
|
||||
|
||||
-- Molten Blast (all ranks) -> refresh the caster's Flame Shock to full.
|
||||
local MOLTEN_BLAST = { 36916, 36917, 36918, 36919, 36920, 36921 }
|
||||
for i = 1, table.getn(MOLTEN_BLAST) do
|
||||
C_UnitAuras.RegisterAuraDurationModifier(MOLTEN_BLAST[i], SHAMAN, FLAMESHOCK_FLAG, 0, "refresh")
|
||||
end
|
||||
end
|
||||
-- Shadow Weaving (priest): any shadow-school priest cast refreshes the caster's
|
||||
-- Shadow Vulnerability (15258, 15s, stacks to 5). The 1->5 stack changes fire
|
||||
-- OnAuraStacksChanged (already handled); the 5->5 refresh is the silent blind
|
||||
-- spot this covers. Matched by family(6) + school(5) so it spans every priest
|
||||
-- shadow spell/rank incl. Turtle additions, excluding holy/discipline casts
|
||||
-- (Smite, heals) which are priest-family but not shadow-school.
|
||||
--
|
||||
-- Gated on the 5/5 talent (15334), where the proc is 100% -> deterministic;
|
||||
-- below 5/5 it's chance-based and inferring on every cast would over-refresh.
|
||||
-- Deferred to SPELLS_CHANGED because talent/spell state isn't ready at load,
|
||||
-- and re-checked there so a respec INTO 5/5 registers without a reload.
|
||||
-- Caveat: DoT *ticks* (SW:P / Devouring Plague) refresh it server-side too but
|
||||
-- emit no cast packet, so a pure DoT-only phase (no direct casts) under-counts.
|
||||
local PRIEST, SHADOW = 6, 5
|
||||
local SW_FLAG, SW_ICON = tonumber("4000000", 16), 9 -- 15258 SpellFamilyFlags + icon
|
||||
|
||||
EventUtil.RegisterOnceFrameEventAndCallback("SPELLS_CHANGED", function()
|
||||
if IsPlayerSpell(15334) then
|
||||
C_UnitAuras.RegisterAuraDurationModifierByTrigger(PRIEST, SHADOW, PRIEST, SW_FLAG, SW_ICON, "refresh")
|
||||
end
|
||||
end)
|
||||
@@ -547,7 +547,7 @@ function EventTraceFrame_Update()
|
||||
button:GetHighlightTexture():SetVertexColor(.8, .8, 1, .15);
|
||||
button:UnlockHighlight();
|
||||
end
|
||||
if (button:IsMouseOver()) then
|
||||
if MouseIsOver(button) then
|
||||
EventTraceFrameEvent_OnEnter(button);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -456,14 +456,11 @@
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<!-- Breadcrumb showing the active navigation path,
|
||||
e.g. "_G > MyFrame > backdrop". Updated each
|
||||
`TableInspectorFrame_Update` tick. -->
|
||||
<FontString name="$parentBreadcrumb" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Size x="380" y="16"/>
|
||||
<Size x="300" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="80" y="-34"/>
|
||||
<Offset x="156" y="-34"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
@@ -510,6 +507,17 @@
|
||||
<OnClick>TableInspector_Back();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentRefreshButton" inherits="UIPanelButtonTemplate" text="Refresh">
|
||||
<Size x="70" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentBackButton" relativePoint="RIGHT">
|
||||
<Offset x="4" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>TableInspector_Refresh();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Slider name="$parentScroll" orientation="VERTICAL">
|
||||
<Size x="16" y="0"/>
|
||||
<Anchors>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
-- TableInspector_Show(tbl, label) -- open with `tbl` as root, `label` shown in breadcrumb
|
||||
-- TableInspector_Push(tbl, label) -- drill in (called by row click)
|
||||
-- TableInspector_Back() -- pop one level
|
||||
-- TableInspector_Refresh() -- re-read the current level's live table (scroll kept)
|
||||
-- TableInspector_Close() -- hide the window
|
||||
-- /tinspect <luaexpr> -- evaluate `<luaexpr>` as Lua and open if result is a table
|
||||
------------------------------------------------------------------------------
|
||||
@@ -184,6 +185,13 @@ function TableInspector_Back()
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Refresh()
|
||||
local top = _navStack[table.getn(_navStack)];
|
||||
if (not top or type(top.tbl) ~= "table") then return; end
|
||||
_RebuildEntries();
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Close()
|
||||
if (_TableInspectorFrame) then _TableInspectorFrame:Hide(); end
|
||||
_navStack = {};
|
||||
|
||||
@@ -45,7 +45,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemClassInfo`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemClassInfo`, `GetItemIcon`, `GetItemSubClassInfo`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.wipe` |
|
||||
@@ -61,17 +61,17 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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.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.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `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` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [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` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTimeCached` |
|
||||
| [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` |
|
||||
| [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.RegisterComboDuration` |
|
||||
| [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.RegisterAuraDurationModifier`, `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` |
|
||||
|
||||
|
||||
+234
-3
@@ -129,6 +129,7 @@ build instructions.
|
||||
- [`QUEST_REMOVED` event](#quest_removed-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
- [`UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)](#update_mouseover_unit-event-loss-fire-fix)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
|
||||
- [Expansion](#expansion)
|
||||
@@ -293,6 +294,7 @@ build instructions.
|
||||
- [`C_Loot.LootUnit(guid)`](#c_lootlootunitguid)
|
||||
- [`C_Loot.LootUnitItem(guid, itemID)`](#c_lootlootunititemguid-itemid)
|
||||
- [`C_Loot.ScanNearbyLoot()`](#c_lootscannearbyloot)
|
||||
- [`C_Loot.LootAllCorpses([max])`](#c_lootlootallcorpsesmax)
|
||||
- [`C_Loot.IsScanInProgress()`](#c_lootisscaninprogress)
|
||||
- [`C_Loot.GetLastScanResults()`](#c_lootgetlastscanresults)
|
||||
|
||||
@@ -459,6 +461,8 @@ build instructions.
|
||||
- [`IsStealthed()`](#isstealthed)
|
||||
- [`IsFalling()`](#isfalling)
|
||||
- [`IsSwimming()`](#isswimming)
|
||||
- [`IsIndoors()`](#isindoors)
|
||||
- [`IsOutdoors()`](#isoutdoors)
|
||||
- [`IsAssistingRitual()`](#isassistingritual)
|
||||
- [`IsInGroup()`](#isingroup)
|
||||
- [`IsInRaid()`](#isinraid)
|
||||
@@ -491,6 +495,7 @@ build instructions.
|
||||
|
||||
- [Time](#time)
|
||||
- [`GetServerTime()`](#getservertime)
|
||||
- [`GetTimeCached()`](#gettimecached)
|
||||
- [`C_Timer.After(seconds, callback)`](#c_timeraftersseconds-callback)
|
||||
- [`C_Timer.NewTimer(seconds, callback)`](#c_timernewtimerseconds-callback)
|
||||
- [`C_Timer.NewTicker(seconds, callback, [iterations])`](#c_timernewtickerseconds-callback-iterations)
|
||||
@@ -553,6 +558,7 @@ build instructions.
|
||||
- [`C_UnitAuras.GetPlayerAuraBySpellID(spellID)`](#c_unitaurasgetplayeraurabyspellidspellid)
|
||||
- [`C_UnitAuras.GetAuraDataBySpellName(unit, spellName [, filter])`](#c_unitaurasgetauradatabyspellnameunit-spellname--filter)
|
||||
- [`C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)`](#c_unitaurasregistercombodurationspellid-baseseconds-maxseconds)
|
||||
- [`C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`](#c_unitaurasregisterauradurationmodifiertriggerspellid-affectedfamily-affectedfamilyflags-affectedicon-op--valueseconds)
|
||||
- [`C_UnitAuras.GetUnitAuras(unit [, filter])`](#c_unitaurasgetunitaurasunit--filter)
|
||||
- [`C_UnitAuras.GetAuraDispelTypeColor(dispelName)`](#c_unitaurasgetauradispeltypecolordispelname)
|
||||
|
||||
@@ -2534,9 +2540,15 @@ it: vanilla renders durability UI for equipped gear only.
|
||||
### `HEARTHSTONE_BOUND` event
|
||||
|
||||
Fires (with no payload) every time the player binds their
|
||||
hearthstone at an innkeeper. Polyfills modern WoW's event of the
|
||||
same name — addons listen to `HEARTHSTONE_BOUND` and re-read
|
||||
`GetBindLocation()` to refresh whatever bind-location UI they show.
|
||||
hearthstone at an innkeeper — including a rebind at the same inn.
|
||||
Polyfills modern WoW's event of the same name — addons listen to
|
||||
`HEARTHSTONE_BOUND` and re-read `GetBindLocation()` to refresh
|
||||
whatever bind-location UI they show.
|
||||
|
||||
> Note: the server re-sends the bind packet on map/zone transitions
|
||||
> (behind the loading screen). Those resyncs are suppressed by gating
|
||||
> on the engine's in-world flag, so the event fires only for an actual
|
||||
> innkeeper bind, never on a map change.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
@@ -3120,6 +3132,60 @@ byte reads (cheap) and naturally handles the case where one packet
|
||||
carries multiple rep updates whose threshold crossings ripple AT_WAR
|
||||
state across unrelated slots.
|
||||
|
||||
### `UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)
|
||||
|
||||
`UPDATE_MOUSEOVER_UNIT` exists in the vanilla event table and fires when a
|
||||
mouseover unit is **gained**, but the 1.12 engine **never fires it on
|
||||
loss** — moving the cursor off a unit clears the mouseover and signals
|
||||
nothing. Modern WoW fires it for both gain and loss, so addons observing
|
||||
`UnitExists("mouseover")` can react when it becomes false. We restore the
|
||||
loss fire.
|
||||
|
||||
Payload is unchanged (none in vanilla — same as retail): the event
|
||||
carries no args; handlers read `UnitExists("mouseover")`, which is
|
||||
`nil`/false at fire time on the loss path.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "UPDATE_MOUSEOVER_UNIT" then
|
||||
if UnitExists("mouseover") then
|
||||
-- gained (or changed to) a mouseover unit
|
||||
else
|
||||
-- mouseover unit lost (the vanilla gap this fills)
|
||||
end
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
**Transitions and who fires them:**
|
||||
|
||||
| From | To | Fires? | Source |
|
||||
|------|----|:------:|--------|
|
||||
| unit | nothing | yes | ClassicAPI (silent in stock 1.12) |
|
||||
| unit | gameobject | yes | ClassicAPI (mouseover unit genuinely lost) |
|
||||
| gameobject | nothing | no | — (never fired a gain; not a unit) |
|
||||
| anything | unit | yes | engine's own inline fire (gain / unit→unit) |
|
||||
|
||||
**Implementation notes**
|
||||
|
||||
The mouseover GUID lives in two globals written **only** by the engine's
|
||||
mouseover set/clear chokepoint (`0x00492890`), so every gain / loss /
|
||||
change flows through it — on a unit gain it fires `UPDATE_MOUSEOVER_UNIT`
|
||||
itself, on loss it clears the GUID silently. We co-hook that function:
|
||||
resolve whether the mouseover is a *unit* before and after the original,
|
||||
and fire on the unit→non-unit transition. Gating on unit *type* (not just
|
||||
"GUID present → absent") is load-bearing — the same GUID slot also holds
|
||||
gameobjects and items, which never fire a gain, so the type check is what
|
||||
keeps moving off a herb node or chest from spuriously firing. The
|
||||
`!hasUnit` half prevents a double-fire on any →unit transition the engine
|
||||
already covered.
|
||||
|
||||
The target is change-gated by its callers (if it ran per-frame the gain
|
||||
event would spam while hovering), so it's a cool hook — no per-frame
|
||||
overhead.
|
||||
|
||||
### `UPDATE_SHAPESHIFT_FORM` event
|
||||
|
||||
Fires whenever the local player's shapeshift form changes — entering
|
||||
@@ -7105,6 +7171,45 @@ Per-corpse step timeout is ~3-6 seconds (180 WorldTick frames) so a
|
||||
single dropped server response doesn't hang the whole scan. Failed
|
||||
corpses simply don't appear in the results.
|
||||
|
||||
### `C_Loot.LootAllCorpses([max])`
|
||||
|
||||
Loots **every** nearby lootable corpse in sequence — the same corpse
|
||||
walk as [`ScanNearbyLoot`](#c_lootscannearbyloot), but it actually takes
|
||||
the loot: coin (`CMSG_LOOT_MONEY`) plus every item slot (`CMSG_LOOT_ITEM`)
|
||||
from each window before releasing and advancing to the next corpse. The
|
||||
optional `max` caps how many corpses are visited (default: all in range).
|
||||
|
||||
Returns `true` if the loot walk was queued, `false` if a walk (this or
|
||||
`ScanNearbyLoot`) is already in progress or the world isn't loaded — the
|
||||
two share one state machine, so [`IsScanInProgress`](#c_lootisscaninprogress)
|
||||
reports either, and only one runs at a time.
|
||||
|
||||
Like the scan, the walk is **silent**: `LOOT_OPENED` / `LOOT_CLOSED` are
|
||||
suppressed so `LootFrame` never flickers open per corpse. The normal
|
||||
item-received and money events still fire (`CHAT_MSG_LOOT`,
|
||||
`CHAT_MSG_MONEY`, inventory updates), so the player sees what they got.
|
||||
|
||||
```lua
|
||||
-- Loot everything within interact range
|
||||
C_Loot.LootAllCorpses()
|
||||
|
||||
-- Loot at most 5 corpses
|
||||
C_Loot.LootAllCorpses(5)
|
||||
```
|
||||
|
||||
Completion is signaled by [`LOOT_SCAN_COMPLETED`](#loot_scan_completed-event);
|
||||
afterward [`GetLastScanResults()`](#c_lootgetlastscanresults) reports what
|
||||
each corpse held — i.e. what was looted.
|
||||
|
||||
> **Range and permissions.** Only corpses inside the engine's own
|
||||
> right-click-loot range are queued (same distance test as
|
||||
> `GetNearbyLootableUnits`), and only those the server flagged lootable
|
||||
> for you. The item sends bypass the client-side BoP/unique **confirm
|
||||
> dialog** (appropriate for a programmatic loot-all), but the server still
|
||||
> enforces every real rule — loot rights, distance, master-loot. Unlike a
|
||||
> right-click, this does **not** change your target or auto-walk you toward
|
||||
> out-of-range corpses.
|
||||
|
||||
### `C_Loot.IsScanInProgress()`
|
||||
|
||||
Returns `true` while a `ScanNearbyLoot` call is mid-flight, `false`
|
||||
@@ -11099,6 +11204,37 @@ if IsSwimming() then
|
||||
end
|
||||
```
|
||||
|
||||
### `IsIndoors()`
|
||||
|
||||
Returns `1` if the local player is under a WMO roof — inside a building,
|
||||
cave, or instance interior — and `nil` otherwise. `nil` before the player
|
||||
object exists (pre-world).
|
||||
|
||||
Recomputed live from the engine's WMO geometry query, so it flips as you
|
||||
cross an interior threshold rather than tracking the current zone/area —
|
||||
you can be indoors and outdoors within the same subzone. Returns `1`/`nil`
|
||||
(not `true`/`false`) to match the historical retail contract and
|
||||
SuperWoW 2.2, so both `if IsIndoors()` and `IsIndoors() == 1` work.
|
||||
|
||||
```lua
|
||||
if IsIndoors() then
|
||||
-- suppress a mount cast that would just fail
|
||||
end
|
||||
```
|
||||
|
||||
### `IsOutdoors()`
|
||||
|
||||
Returns `1` if the local player is outdoors (open sky, or simply not
|
||||
inside a WMO interior) and `nil` otherwise. Exact complement of
|
||||
[`IsIndoors()`](#isindoors) for a resolvable player; both return `nil`
|
||||
pre-world.
|
||||
|
||||
```lua
|
||||
if IsOutdoors() then
|
||||
CastSpellByName("Mount Up")
|
||||
end
|
||||
```
|
||||
|
||||
### `IsAssistingRitual()`
|
||||
|
||||
Returns `true` if the local player is currently clicking a warlock
|
||||
@@ -11771,6 +11907,40 @@ right call for calendar / log-timestamp / cooldown-sync use cases.
|
||||
> boundary and the timestamp is accurate to within a second of the
|
||||
> engine's clock for as long as the session continues.
|
||||
|
||||
### `GetTimeCached()`
|
||||
|
||||
Returns the same value as [`GetTime()`](https://warcraft.wiki.gg/wiki/API_GetTime)
|
||||
— seconds on the engine's uptime clock — but **frame-stable**: every call
|
||||
within a single frame returns the identical value, refreshed once per
|
||||
frame. Same epoch as `GetTime()`, so the two are directly comparable.
|
||||
|
||||
Backports the retail 4.x `GetTime()` behavior (retail samples the clock
|
||||
once per frame in the main loop) under a distinct name, leaving vanilla's
|
||||
`GetTime()` untouched — 1.12's `GetTime()` is *live*, recomputing the OS
|
||||
tick on every call, so two same-frame `GetTime()` calls can differ.
|
||||
|
||||
```lua
|
||||
-- Frame-stable: consistent timestamps across a frame's work
|
||||
local now = GetTimeCached()
|
||||
if now - lastFire >= interval then
|
||||
lastFire = now
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
> **Not a performance feature.** The underlying tick source is cheap in
|
||||
> 1.12 (`GetTickCount`, a user-mode shared-page read, or `rdtsc`), and
|
||||
> both functions pay the same Lua→C call overhead, which dominates. So
|
||||
> `GetTimeCached()` is not meaningfully faster than `GetTime()` — its
|
||||
> only advantage is the frame-stable semantics. To actually cut cost in a
|
||||
> hot `OnUpdate`, cache the value in a Lua local
|
||||
> (`local now = GetTime()`) and reuse it; that removes the per-call Lua
|
||||
> dispatch, which a cached C function cannot.
|
||||
|
||||
The cache is refreshed from the shared once-per-frame `Tick::WorldTick`
|
||||
hook; before the first world tick (pre-login / glue) it falls back to a
|
||||
live sample, so it never returns 0.
|
||||
|
||||
### `C_Timer.After(seconds, callback)`
|
||||
|
||||
Schedules `callback` to fire once after `seconds`. Returns nothing.
|
||||
@@ -13174,6 +13344,67 @@ is the escape hatch for *other* custom servers with the same class of
|
||||
gap — call it from your own config or addon; a registered override
|
||||
beats both the DBC and the built-in values.
|
||||
|
||||
### `C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`
|
||||
|
||||
ClassicAPI extension for **server-side DoT-duration changes on another
|
||||
unit that the client is never told about**. Returns `true` on success.
|
||||
|
||||
`expirationTime` for a debuff on another unit is computed from the cast
|
||||
packet (`SMSG_SPELL_GO`) at cast time. When the server later changes that
|
||||
debuff's remaining duration — a refresh, or a partial consume — 1.12 sends
|
||||
**no** packet an observing caster can see (verified in the server source:
|
||||
the change runs through `SetAuraDuration`/`RefreshHolder`, and the only
|
||||
duration packet is self-scoped to the aura-bearer; on a mob it isn't even
|
||||
built). So the cached `expirationTime` would go stale.
|
||||
|
||||
The client *does* always see the **triggering** cast, though. Register a
|
||||
rule and ClassicAPI mirrors the server's edit on the cached aura when that
|
||||
trigger lands:
|
||||
|
||||
| Arg | Meaning |
|
||||
|-----|---------|
|
||||
| `triggerSpellID` | The spell whose cast triggers the change (match one rank per call — use the exact IDs the server binds). |
|
||||
| `affectedFamily` | `SpellFamilyName` of the affected aura (e.g. `5` warlock, `11` shaman, `7` druid). |
|
||||
| `affectedFamilyFlags` | A `SpellFamilyFlags` bitmask; the affected aura matches if it overlaps. Family + flag is rank-proof (covers every rank at once). |
|
||||
| `affectedIcon` | `SpellIconID` the affected aura must have, or `0` to match any. |
|
||||
| `op` | `"refresh"` (reset to full duration), `"reduce"` (subtract `valueSeconds`, removing the aura if it would go non-positive), `"set"` (to `valueSeconds`), `"remove"`. |
|
||||
| `valueSeconds` | Amount for `reduce`/`set`; ignored otherwise. |
|
||||
|
||||
The rule only fires for the aura **cast by the same unit** as the trigger
|
||||
(these mechanics act on the caster's own DoT), and misses are excluded for
|
||||
free (a missed trigger isn't in the packet's hit list).
|
||||
|
||||
**`C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`**
|
||||
is the same, but matches the *trigger* by `SpellFamilyName` + school index
|
||||
(`0` physical … `2` fire, `5` shadow, `6` arcane; `< 0` = any) instead of an
|
||||
exact spellID — one rule covers a whole class of spells (every rank, plus
|
||||
server-added ones). Use it when the trigger is a category rather than a named
|
||||
ability.
|
||||
|
||||
`!!!ClassicAPI` ships a default Turtle ruleset
|
||||
([Util/AuraDurationModifiers.lua](../AddOns/!!!ClassicAPI/Util/AuraDurationModifiers.lua)):
|
||||
Conflagrate shaves 3s off the caster's Immolate, Molten Blast refreshes the
|
||||
caster's Flame Shock, and — for a 5/5 Shadow Weaving priest (talent `15334`,
|
||||
where the proc is 100%) — any shadow-school priest cast refreshes the target's
|
||||
Shadow Vulnerability (`15258`). Deliberately excluded: probabilistic effects
|
||||
(Carnage's roll-gated Rip/Rake refresh, and Shadow Weaving below 5/5) — the
|
||||
client can't see the server's roll, so inferring them would show wrong timers.
|
||||
Two accepted best-effort caveats on the Shadow Weaving rule: DoT *ticks*
|
||||
(SW:P / Devouring Plague) also refresh it server-side but emit no cast packet,
|
||||
so a pure DoT-only phase under-counts slightly; and a full-consume Conflagrate
|
||||
variant on other servers *removes* Immolate, which ClassicAPI already evicts
|
||||
via the normal removal path (so the reduce rule is harmless there).
|
||||
|
||||
```lua
|
||||
-- Flags are hex; Lua 5.0 has no 0x literals, so use tonumber(hex, 16).
|
||||
-- Conflagrate (Rank 4) shaves 3s off the caster's Immolate (warlock, flag 0x4):
|
||||
C_UnitAuras.RegisterAuraDurationModifier(18932, 5, tonumber("4", 16), 0, "reduce", 3)
|
||||
-- Molten Blast refreshes the caster's Flame Shock (shaman, flag 0x10000000):
|
||||
C_UnitAuras.RegisterAuraDurationModifier(36916, 11, tonumber("10000000", 16), 0, "refresh")
|
||||
-- Any priest (6) shadow-school (5) cast refreshes Shadow Vulnerability (15258):
|
||||
C_UnitAuras.RegisterAuraDurationModifierByTrigger(6, 5, 6, tonumber("4000000", 16), 9, "refresh")
|
||||
```
|
||||
|
||||
### `C_UnitAuras.GetUnitAuras(unit [, filter])`
|
||||
|
||||
Returns a numerically-indexed array of `AuraData` tables for every
|
||||
|
||||
@@ -435,6 +435,17 @@ and other DLLs claim for custom events.
|
||||
|
||||
## Notes on specific entries
|
||||
|
||||
- **`[336] UPDATE_MOUSEOVER_UNIT` now fires on loss too (ClassicAPI).**
|
||||
Stock 1.12 fires this only when a mouseover unit is *gained*; moving
|
||||
the cursor off a unit clears the mouseover and signals nothing.
|
||||
ClassicAPI co-hooks the engine's mouseover set/clear chokepoint
|
||||
(`0x00492890`, the sole writer of the mouseover GUID) and fires
|
||||
`UPDATE_MOUSEOVER_UNIT` on the unit→non-unit transition, matching
|
||||
retail. Gated on the *old* mouseover being a unit and the *new* one
|
||||
not, so moving off a gameobject / item (which never fired a gain)
|
||||
doesn't spuriously fire. No payload either way — handlers read
|
||||
`UnitExists("mouseover")`, false at fire time on loss. See
|
||||
[API.md](API.md#update_mouseover_unit-event-loss-fire-fix).
|
||||
- **Repeated `UNIT_*` entries at low indices** (e.g. four slots named
|
||||
`UNIT_DAMAGE` at 128–131, five `UNIT_STATS` at 144–148, seven
|
||||
`UNIT_RESISTANCES` at 149–155) — these are the per-stat /
|
||||
|
||||
+88
-7
@@ -316,6 +316,16 @@ enum Offsets {
|
||||
// C_Item.GetItemName exactly. Inner formatter FUN_005D8B00.
|
||||
FUN_ITEM_BUILD_INSTANCE_NAME = 0x005D8BC0,
|
||||
|
||||
// Inner name formatter behind FUN_ITEM_BUILD_INSTANCE_NAME, callable
|
||||
// WITHOUT a CGItem: `__fastcall(char *out /*ecx*/, uint outSize /*edx*/,
|
||||
// uint32 itemID, int suffixID)`. Fetches the ItemStats record for itemID
|
||||
// and, when suffixID is a valid ItemRandomProperties row with a localized
|
||||
// suffix name (record +0x1c + locale*4), formats base + suffix via
|
||||
// ITEM_SUFFIX_TEMPLATE; otherwise the base name. Lets the by-STRING item
|
||||
// paths (GetItemNameByID, C_Item.GetItemInfo) apply an item link's random
|
||||
// suffix without a live item instance.
|
||||
FUN_ITEM_BUILD_NAME_FROM_ID = 0x005D8B00,
|
||||
|
||||
// Engine's inventory swap-and-send. Same primitive
|
||||
// `Script_EquipCursorItem` (0x00489660) uses after the cursor's
|
||||
// source location has been resolved. Sends opcode 0x10D
|
||||
@@ -544,6 +554,31 @@ enum Offsets {
|
||||
// input it's the right primitive.
|
||||
FUN_TOKEN_TO_GUID = 0x00515970,
|
||||
|
||||
// Mouseover-unit GUID globals — the `"mouseover"` token resolves to
|
||||
// these (see the token-dispatch list above). Written ONLY by
|
||||
// `FUN_SET_MOUSEOVER_UNIT`; zero when nothing is moused over. Used by
|
||||
// `Unit::Mouseover` to detect the loss transition.
|
||||
VAR_MOUSEOVER_GUID_LO = 0x00B4E2C8,
|
||||
VAR_MOUSEOVER_GUID_HI = 0x00B4E2CC,
|
||||
|
||||
// The engine's single mouseover set/clear chokepoint —
|
||||
// `__stdcall(guidLo, guidHi, arg3, arg4)` (early returns are all
|
||||
// `ret 0x10`, and the caller pushes 4 without cleaning). It is the
|
||||
// ONLY writer of `VAR_MOUSEOVER_GUID_*`, so every mouseover
|
||||
// gain/loss/change flows through it. On a unit gain it fires
|
||||
// `UPDATE_MOUSEOVER_UNIT` itself (via `FUN_FIRE_EVENT_NO_ARGS(0x150)`
|
||||
// in the friendly/hostile-unit tooltip case); on loss it clears the
|
||||
// GUID but fires nothing — the gap `Unit::Mouseover` closes.
|
||||
// Change-gated by its callers (if it ran per-frame the gain fire
|
||||
// would spam every frame while hovering, which vanilla doesn't), so
|
||||
// it's a cool hook target.
|
||||
FUN_SET_MOUSEOVER_UNIT = 0x00492890,
|
||||
|
||||
// Event ID of `UPDATE_MOUSEOVER_UNIT` (name-table slot
|
||||
// `0x00BE16D8` → `(0x16D8 - 0x1198) / 4`). Fired with no args via
|
||||
// `FUN_FIRE_EVENT_NO_ARGS`; Lua handlers read `UnitExists("mouseover")`.
|
||||
EVENT_UPDATE_MOUSEOVER_UNIT = 0x150,
|
||||
|
||||
// `SStrCmpI(a, b, n)` — Storm's case-insensitive memcmp-style
|
||||
// comparator. **`int __stdcall(const char *a, const char *b, int n)`**
|
||||
// — the function ends with `ret 0xc`, so the callee pops the
|
||||
@@ -3790,6 +3825,19 @@ enum Offsets {
|
||||
// `Script_IsInInstance`).
|
||||
VAR_CURRENT_MAP_ID = 0x00B4E378,
|
||||
|
||||
// "World is live" flag — 1 while the player is fully in the world, 0 during
|
||||
// any loading screen (initial login, zone/instance transition, logout to
|
||||
// glue). Set to 1 by the enter-world init `FUN_004908C0` (guarded
|
||||
// `if (==0){=1;...}`) and back to 0 by the world-teardown `FUN_00490A80`
|
||||
// (`if(!=0){=0;...}`) that runs at the start of every transition. Byte flag.
|
||||
// The clean pollable gate for "can this happen in the world right now" —
|
||||
// e.g. suppressing server resync packets that arrive behind a loading
|
||||
// screen. Verified via the enter/leave pair in Ghidra.
|
||||
//
|
||||
// (NOTE: 0x00B4E37C is NOT this flag — an earlier note wrongly claimed it
|
||||
// was. That address is instance grace-period state, read only by the
|
||||
// "recently left an instance" 90-second check in `FUN_00495c90`.)
|
||||
VAR_IN_WORLD = 0x00B4B424,
|
||||
|
||||
// Local player's current AreaTable.dbc area ID (u32 storage; only
|
||||
// the low 16 bits are used — the value is broadcast over the wire
|
||||
@@ -4704,6 +4752,23 @@ enum Offsets {
|
||||
// `castGameObject != 0`).
|
||||
OFF_CGPLAYER_CAST_GAMEOBJECT_PTR = 0xB4,
|
||||
|
||||
// Indoor/outdoor query for a unit — backs `IsIndoors()` / `IsOutdoors()`
|
||||
// (`unit/State.cpp`). `__fastcall(unit) -> uint` where **nonzero =
|
||||
// outdoors**, 0 = indoors. A 2-instruction thunk: reads the unit's
|
||||
// world/collision-context pointer at `unit + 0xE0` and tail-jumps to the
|
||||
// WMO query `FUN_006706f0`. That query is fully null-safe — null context
|
||||
// or a unit not inside a WMO group returns 1 (outdoors); otherwise it
|
||||
// resolves the current WMO group via `FUN_0069d4f0` and returns bit 15
|
||||
// (`0x8000`) of the group's runtime flags word (the OUTDOOR bit).
|
||||
//
|
||||
// Derived from SuperWoW 2.2's `IsIndoors` handler (which does exactly
|
||||
// `unit = ObjMgr(0x10, PlayerGUID()); push (query(unit)==0)`), then
|
||||
// confirmed against the vanilla binary. Vanilla ships no Lua binding for
|
||||
// this; the 3.3.5 client's `IsIndoors` (`0x00612300`) uses the identical
|
||||
// shape (`unit + 0xB8` → WMO group flag test), so the semantics are
|
||||
// cross-checked across both clients.
|
||||
FUN_UNIT_IS_OUTDOORS = 0x00612cc0,
|
||||
|
||||
// No `MOVEFLAG_FLYING` constant intentionally. Vanilla 1.12
|
||||
// doesn't flip a flying bit during taxi flights (verified
|
||||
// empirically: `IsFlying()` checking 0x01000000 stayed false
|
||||
@@ -4823,18 +4888,19 @@ enum Offsets {
|
||||
//
|
||||
// Single caller (the packet dispatcher's jump table for opcode
|
||||
// 0x155) — quiet hook target, no contention with other DLLs.
|
||||
// Hooked by `Player::HearthstoneBound` to fire `HEARTHSTONE_BOUND`
|
||||
// every time the user re-binds at an innkeeper. Gating off the
|
||||
// bind-valid flag's transition distinguishes "initial sync" (the
|
||||
// one we suppress) from "user rebound at an inn" (the one we
|
||||
// fire on — even when the area string stays the same, since the
|
||||
// bind ACTION is what fires the event).
|
||||
// Hooked by `Player::HearthstoneBound` to fire `HEARTHSTONE_BOUND`.
|
||||
// Two packets must be suppressed so the event reflects only a real
|
||||
// innkeeper bind: (1) the initial post-login/char-switch sync
|
||||
// (gated on VAR_BIND_POINT_VALID being 0 before the handler runs),
|
||||
// and (2) Turtle's per-map-transition resync, which re-sends the
|
||||
// packet behind a loading screen — gated on VAR_IN_WORLD (the
|
||||
// player can't bind mid-load). See `Player::HearthstoneBound`.
|
||||
FUN_BINDPOINT_UPDATE_HANDLER = 0x005ED3C0,
|
||||
// "Bind point is valid" flag. Set to 1 by the BINDPOINTUPDATE
|
||||
// handler after the packet has been fully parsed. Zeroed by
|
||||
// `FUN_005E2510` (the per-character-entry init routine).
|
||||
// Reading this BEFORE the handler runs distinguishes initial
|
||||
// sync (`== 0`) from a real re-bind (`== 1`).
|
||||
// sync (`== 0`) from a later packet (`== 1`).
|
||||
VAR_BIND_POINT_VALID = 0x00C4D4E0,
|
||||
|
||||
// SMSG_QUESTGIVER_QUEST_COMPLETE handler (opcode 0x191). Server
|
||||
@@ -5048,6 +5114,20 @@ enum Offsets {
|
||||
// delayed crashes on unrelated vtable reads.
|
||||
FUN_CMSG_LOOT_ITEM = 0x005E1AD0,
|
||||
|
||||
// `__fastcall(player)` — sends `CMSG_LOOT_MONEY` (the coin/money
|
||||
// request) for the player's currently-open loot window. Vanilla has
|
||||
// no `LootMoney` Lua global; coin is virtual loot slot 0 and the Lua
|
||||
// `LootSlot` handler `FUN_004C2790` routes the coin slot here (its
|
||||
// `param_1 == 0 && VAR_LOOT_LOOTABLE != 0` branch calls this with the
|
||||
// resolved local player). Guards internally on the player's active
|
||||
// loot GUID at `[player + 0x1D28]` — which `FUN_CMSG_LOOT_UNIT` writes
|
||||
// when it sends the loot request — so it silently no-ops if no window
|
||||
// is open. Pass the SAME player pointer used for `FUN_CMSG_LOOT_UNIT`
|
||||
// so the `+0x1D28` guard it just wrote is the one read here.
|
||||
// `Loot::Scan`'s `LootAllCorpses` path uses it to grab coin alongside
|
||||
// the per-slot `CMSG_LOOT_ITEM` sends.
|
||||
FUN_CMSG_LOOT_MONEY = 0x005EA8C0,
|
||||
|
||||
// UNIT_DYNAMIC_FLAGS within `m_objectFields`. Standard
|
||||
// server-driven dynamic-state bitmask. Empirically verified by
|
||||
// cross-referencing three `Script_*` accessors that all read
|
||||
@@ -5417,6 +5497,7 @@ enum Offsets {
|
||||
// sum). AttributesEx2 (+0x24) bit 0x20000000 disables spell mods.
|
||||
OFF_SPELL_RECORD_FAMILY_NAME = 0x280, // u32
|
||||
OFF_SPELL_RECORD_FAMILY_FLAGS = 0x284, // u64
|
||||
OFF_SPELL_RECORD_ICON_ID = 0x1D4, // u32 SpellIconID (→ SpellIcon.dbc)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES = 0x18, // u32 (base Attributes)
|
||||
SPELL_ATTR_PASSIVE = 0x40, // bit 6 — always-on aura
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x24, // u32
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Aura::Source {
|
||||
|
||||
@@ -234,6 +235,263 @@ void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Server-side duration modifiers (trigger-driven inference) -----------
|
||||
//
|
||||
// Some server mechanics change a DoT's remaining time on another unit with
|
||||
// NO client-visible signal — verified in tortoise-wow: the change goes
|
||||
// through SetAuraDuration / RefreshHolder, and UpdateAuraDuration only
|
||||
// notifies the aura's target-if-a-player (self-scoped), never the observing
|
||||
// caster; on a mob the packet isn't even built. We can't hear the *change*,
|
||||
// but we DO see the TRIGGERING cast's SMSG_SPELL_GO, so we mirror the
|
||||
// server's edit on the cached entry. Rules come from Lua
|
||||
// (`C_UnitAuras.RegisterAuraDurationModifier`), so the server-specific set
|
||||
// lives in the addon, not the DLL. Turtle examples:
|
||||
// Conflagrate -> Immolate: reduce 3s (Immolate keeps ticking, -3s)
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder → reset to max)
|
||||
// A trigger is matched by exact spellID (from the server's script binding,
|
||||
// stable across ranks); the affected aura by SpellFamilyName + a family-flag
|
||||
// overlap (+ optional icon) — rank-proof, exactly how the server's scripts
|
||||
// find it. Conflagrate's *full*-consume path removes Immolate, which clears
|
||||
// the descriptor slot → OnAuraRemoved already handles it; the reduce rule
|
||||
// covers the keep-ticking case. Probabilistic refreshes (Carnage's roll) are
|
||||
// deliberately NOT shipped as defaults — the client can't know the server's
|
||||
// roll outcome, so inferring them would show wrong timers.
|
||||
|
||||
enum ModOp { MOD_REFRESH = 0, MOD_REDUCE = 1, MOD_SET = 2, MOD_REMOVE = 3 };
|
||||
|
||||
struct DurationMod {
|
||||
uint32_t triggerSpellId; // exact trigger; 0 = match by family+school below
|
||||
uint32_t triggerFamily; // (triggerSpellId==0) trigger's SpellFamilyName
|
||||
int32_t triggerSchool; // (triggerSpellId==0) school index; -1 = any school
|
||||
uint32_t affectedFamily; // SpellFamilyName of the affected aura
|
||||
uint64_t affectedMask; // must overlap the affected aura's SpellFamilyFlags
|
||||
uint32_t affectedIcon; // 0 = any; else affected aura's SpellIconID must equal
|
||||
int32_t op;
|
||||
int32_t valueMs; // REDUCE/SET amount; ignored by REFRESH/REMOVE
|
||||
};
|
||||
constexpr int kMaxMods = 128;
|
||||
DurationMod g_mods[kMaxMods];
|
||||
int g_modCount = 0;
|
||||
|
||||
bool AffectedMatches(const uint8_t *rec, const DurationMod &m) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != m.affectedFamily)
|
||||
return false;
|
||||
const uint64_t flags = *reinterpret_cast<const uint64_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_FLAGS);
|
||||
if ((flags & m.affectedMask) == 0)
|
||||
return false;
|
||||
if (m.affectedIcon != 0 &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_ICON_ID) != m.affectedIcon)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// A rule's trigger matches either by exact spellID, or (triggerSpellId == 0)
|
||||
// by the cast spell's SpellFamilyName + school index — one rule then covers
|
||||
// every rank / server-added spell of a class's school (e.g. any priest
|
||||
// shadow-school cast, for Shadow Weaving). triggerSchool < 0 = any school.
|
||||
bool TriggerMatches(const DurationMod &m, uint32_t triggerSpellId,
|
||||
const uint8_t *triggerRec) {
|
||||
if (m.triggerSpellId != 0)
|
||||
return m.triggerSpellId == triggerSpellId;
|
||||
if (triggerRec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
triggerRec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != m.triggerFamily)
|
||||
return false;
|
||||
if (m.triggerSchool >= 0 &&
|
||||
static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
|
||||
triggerRec + Offsets::OFF_SPELL_SCHOOL)) != m.triggerSchool)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
switch (m.op) {
|
||||
case MOD_REFRESH:
|
||||
if (e.durationMs > 0)
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → reset to max
|
||||
break;
|
||||
case MOD_SET:
|
||||
e.durationMs = static_cast<uint32_t>(m.valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(m.valueMs);
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(m.valueMs))
|
||||
e.expirationMs -= static_cast<uint32_t>(m.valueMs);
|
||||
else
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
}
|
||||
break;
|
||||
case MOD_REMOVE:
|
||||
e.used = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// On a trigger cast landing on its hit targets, mirror the server's duration
|
||||
// edit on the caster's own matching cached aura. Called from SpellGo_h before
|
||||
// the aura gate — triggers (Conflagrate, Molten Blast) apply no aura of their
|
||||
// own, so they'd otherwise be dropped.
|
||||
void ApplyDurationModifiers(uint32_t triggerSpellId, uint64_t caster,
|
||||
const uint64_t *targets, int numTargets) {
|
||||
if (triggerSpellId == 0 || caster == 0 || numTargets <= 0)
|
||||
return;
|
||||
const uint8_t *triggerRec =
|
||||
Spell::Lookup::RecordForID(static_cast<int>(triggerSpellId));
|
||||
const uint32_t now = NowMs();
|
||||
for (int r = 0; r < g_modCount; ++r) {
|
||||
const DurationMod &m = g_mods[r];
|
||||
if (!TriggerMatches(m, triggerSpellId, triggerRec))
|
||||
continue;
|
||||
for (int t = 0; t < numTargets; ++t) {
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.targetGuid != targets[t])
|
||||
continue;
|
||||
// The mechanic acts on the trigger-caster's own aura. Cast-applied
|
||||
// auras (Immolate, Flame Shock) carry caster == player from SpellGo;
|
||||
// but PROC-applied ones (Shadow Weaving) enter the cache via the
|
||||
// application hooks with casterGuid 0 (no SMSG_SPELL_GO ever named a
|
||||
// caster), so accept those too and attribute them to this trigger
|
||||
// caster (also gives them a sourceUnit).
|
||||
if (e.casterGuid != 0 && e.casterGuid != caster)
|
||||
continue;
|
||||
if (!AffectedMatches(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId)), m))
|
||||
continue;
|
||||
if (e.casterGuid == 0)
|
||||
e.casterGuid = caster;
|
||||
ApplyMod(e, m, now);
|
||||
break; // one matching aura per (rule, target), like the server
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RegisterDurationMod(uint32_t triggerSpellId, uint32_t triggerFamily,
|
||||
int32_t triggerSchool, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs) {
|
||||
// Trigger must be identified one way or the other.
|
||||
if ((triggerSpellId == 0 && triggerFamily == 0) || affectedMask == 0 ||
|
||||
op < MOD_REFRESH || op > MOD_REMOVE)
|
||||
return false;
|
||||
for (int i = 0; i < g_modCount; ++i) { // replace an identical rule
|
||||
DurationMod &m = g_mods[i];
|
||||
if (m.triggerSpellId == triggerSpellId && m.triggerFamily == triggerFamily &&
|
||||
m.triggerSchool == triggerSchool && m.affectedFamily == affectedFamily &&
|
||||
m.affectedMask == affectedMask && m.affectedIcon == affectedIcon) {
|
||||
m.op = op;
|
||||
m.valueMs = valueMs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (g_modCount >= kMaxMods)
|
||||
return false;
|
||||
g_mods[g_modCount++] = {triggerSpellId, triggerFamily, triggerSchool,
|
||||
affectedFamily, affectedMask, affectedIcon, op, valueMs};
|
||||
return true;
|
||||
}
|
||||
|
||||
int OpFromString(const char *s) {
|
||||
if (s == nullptr)
|
||||
return -1;
|
||||
if (_stricmp(s, "refresh") == 0)
|
||||
return MOD_REFRESH;
|
||||
if (_stricmp(s, "reduce") == 0)
|
||||
return MOD_REDUCE;
|
||||
if (_stricmp(s, "set") == 0)
|
||||
return MOD_SET;
|
||||
if (_stricmp(s, "remove") == 0)
|
||||
return MOD_REMOVE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily,
|
||||
// affectedFamilyFlags, affectedIcon, op [, valueSeconds])` -> bool.
|
||||
// op: "refresh" | "reduce" | "set" | "remove". valueSeconds used by
|
||||
// reduce/set. affectedIcon 0 = match any icon. Re-registering an identical
|
||||
// (trigger, family, flags, icon) tuple replaces its op/value.
|
||||
int __fastcall Script_RegisterAuraDurationModifier(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4) ||
|
||||
!Game::Lua::IsString(L, 5)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, "
|
||||
"affectedFamily, affectedFamilyFlags, affectedIcon, op[, "
|
||||
"valueSeconds])");
|
||||
return 0;
|
||||
}
|
||||
const auto trigger = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const auto family = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
const auto mask = static_cast<uint64_t>(Game::Lua::ToNumber(L, 3));
|
||||
const auto icon = static_cast<uint32_t>(Game::Lua::ToNumber(L, 4));
|
||||
const int op = OpFromString(Game::Lua::ToString(L, 5));
|
||||
const int32_t valueMs =
|
||||
Game::Lua::IsNumber(L, 6)
|
||||
? static_cast<int32_t>(Game::Lua::ToNumber(L, 6) * 1000.0)
|
||||
: 0;
|
||||
if (op < 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L, RegisterDurationMod(trigger, /*triggerFamily*/ 0,
|
||||
/*triggerSchool*/ -1, family, mask,
|
||||
icon, op, valueMs));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily,
|
||||
// triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op
|
||||
// [, valueSeconds])` -> bool. Like the above, but the trigger is matched
|
||||
// by SpellFamilyName + school index instead of an exact spellID — one rule
|
||||
// covers every rank / server-added spell of a class's school. triggerSchool
|
||||
// < 0 = any school. E.g. Shadow Weaving: priest (6) shadow-school (5) casts
|
||||
// refresh the target's Shadow Vulnerability.
|
||||
int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4) ||
|
||||
!Game::Lua::IsNumber(L, 5) || !Game::Lua::IsString(L, 6)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_UnitAuras.RegisterAuraDurationModifierByTrigger("
|
||||
"triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, "
|
||||
"affectedIcon, op[, valueSeconds])");
|
||||
return 0;
|
||||
}
|
||||
const auto tfamily = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const auto tschool = static_cast<int32_t>(Game::Lua::ToNumber(L, 2));
|
||||
const auto family = static_cast<uint32_t>(Game::Lua::ToNumber(L, 3));
|
||||
const auto mask = static_cast<uint64_t>(Game::Lua::ToNumber(L, 4));
|
||||
const auto icon = static_cast<uint32_t>(Game::Lua::ToNumber(L, 5));
|
||||
const int op = OpFromString(Game::Lua::ToString(L, 6));
|
||||
const int32_t valueMs =
|
||||
Game::Lua::IsNumber(L, 7)
|
||||
? static_cast<int32_t>(Game::Lua::ToNumber(L, 7) * 1000.0)
|
||||
: 0;
|
||||
if (op < 0 || tfamily == 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L, RegisterDurationMod(/*triggerSpellId*/ 0, tfamily, tschool,
|
||||
family, mask, icon, op, valueMs));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterDurationModLua() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterAuraDurationModifier",
|
||||
&Script_RegisterAuraDurationModifier);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras",
|
||||
"RegisterAuraDurationModifierByTrigger",
|
||||
&Script_RegisterAuraDurationModifierByTrigger);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
|
||||
// Wipe the whole cache. Used on a map transition (see OnWorldTick).
|
||||
void FlushAll() {
|
||||
for (auto &e : g_cache)
|
||||
@@ -328,6 +586,11 @@ void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
if (caster == Unit::Identity::PlayerGuid())
|
||||
Totem::Tracker::OnPlayerSpellGo(spellId);
|
||||
|
||||
// Mirror server-side duration edits the client is never told about
|
||||
// (Conflagrate -3s Immolate, Molten Blast refresh Flame Shock, …). Runs
|
||||
// before the aura gate below: the trigger spell applies no aura itself.
|
||||
ApplyDurationModifiers(spellId, caster, targets, numTargets);
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Bag::UpdateDelayed {
|
||||
|
||||
namespace {
|
||||
@@ -127,6 +129,19 @@ unsigned __fastcall KeyringDescChange_h(void *bagDesc, void *edx,
|
||||
void OnWorldTick() {
|
||||
if (!g_pending)
|
||||
return;
|
||||
// Bag changes aren't meaningful until the player is actually in the world.
|
||||
// During any loading screen (initial login, zone/instance transition) the
|
||||
// engine re-populates bag slots as objects stream in (setting g_pending)
|
||||
// while still behind the screen; the per-frame world tick runs there too,
|
||||
// so firing BAG_UPDATE_DELAYED would make addons rescan bags repeatedly
|
||||
// mid-load for no real change. Hold the pending flag — do NOT clear it —
|
||||
// until the engine's in-world flag is set; the first in-world frame then
|
||||
// emits a single DELAYED covering the settled inventory (matching retail's
|
||||
// one BAG_UPDATE_DELAYED per load). VAR_IN_WORLD clears on every transition,
|
||||
// so this re-suppresses on later zone changes too, not just initial login.
|
||||
if (*reinterpret_cast<const volatile uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_IN_WORLD)) == 0)
|
||||
return;
|
||||
g_pending = false;
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
if (slot >= 0)
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
TargetUnit("focus");
|
||||
</Binding>
|
||||
<Binding name="AOELOOT">
|
||||
if not C_Loot.IsScanInProgress() then
|
||||
C_Loot.ScanNearbyLoot()
|
||||
end
|
||||
C_Loot.LootAllCorpses()
|
||||
</Binding>
|
||||
</Bindings>
|
||||
|
||||
+13
-2
@@ -21,7 +21,7 @@
|
||||
namespace Item::Arg {
|
||||
|
||||
Resolved Resolve(void *L, int idx) {
|
||||
Resolved out{0, nullptr};
|
||||
Resolved out{0, 0, nullptr};
|
||||
if (Game::Lua::IsNumber(L, idx)) {
|
||||
out.itemID = static_cast<int>(Game::Lua::ToNumber(L, idx));
|
||||
return out;
|
||||
@@ -34,7 +34,18 @@ Resolved Resolve(void *L, int idx) {
|
||||
return out;
|
||||
}
|
||||
if (const char *m = std::strstr(s, "item:")) {
|
||||
out.itemID = std::atoi(m + 5);
|
||||
m += 5;
|
||||
out.itemID = std::atoi(m);
|
||||
// Random suffix is the 3rd colon-separated field: itemID:enchant:suffix.
|
||||
// Walk to the 2nd ':' (stopping at the link boundary), read the number
|
||||
// after it. Bare "item:N" (no colons) leaves suffix 0.
|
||||
int colons = 0;
|
||||
for (const char *p = m; *p != '\0' && *p != '|'; ++p) {
|
||||
if (*p == ':' && ++colons == 2) {
|
||||
out.suffix = std::atoi(p + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Plain string — try numeric, otherwise expose as a name for
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Item::Arg {
|
||||
// can ignore `name` and treat `itemID == 0` as "no usable input".
|
||||
struct Resolved {
|
||||
int itemID;
|
||||
int suffix; // random-property / suffix ID (3rd `item:` field), 0 if none
|
||||
const char *name;
|
||||
};
|
||||
|
||||
|
||||
+15
-4
@@ -259,7 +259,8 @@ static int __fastcall Script_C_Item_GetItemFamily(void *L) {
|
||||
// the value lands on a retry after `GET_ITEM_INFO_RECEIVED`,
|
||||
// matching the modern async contract.
|
||||
static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
const Item::Arg::Resolved arg = Item::Arg::Resolve(L, 1);
|
||||
const int itemID = arg.itemID;
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
|
||||
@@ -288,14 +289,24 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
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
|
||||
// suffix these reduce to the base name / plain `item:N:0:0:0` link.
|
||||
char suffixedName[128];
|
||||
const char *displayName =
|
||||
Item::Link::NameFromIDSuffix(static_cast<uint32_t>(itemID), arg.suffix,
|
||||
suffixedName, sizeof(suffixedName))
|
||||
? suffixedName
|
||||
: name;
|
||||
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
char link[256];
|
||||
const bool haveLink = Item::Link::BasicFromItemID(static_cast<uint32_t>(itemID),
|
||||
link, sizeof(link));
|
||||
const bool haveLink = Item::Link::BasicFromIDSuffix(static_cast<uint32_t>(itemID),
|
||||
arg.suffix, link, sizeof(link));
|
||||
|
||||
Game::Lua::PushString(L, PushedOrEmpty(name)); // 1 itemName
|
||||
Game::Lua::PushString(L, PushedOrEmpty(displayName)); // 1 itemName
|
||||
if (haveLink)
|
||||
Game::Lua::PushString(L, link); // 2 itemLink
|
||||
else
|
||||
|
||||
+31
-7
@@ -30,6 +30,10 @@ using BuildItemLink_t = const char *(__fastcall *)(const void *cgItem);
|
||||
using BuildInstanceName_t = void(__thiscall *)(const void *cgItem, char *out,
|
||||
unsigned outSize);
|
||||
|
||||
// Inner name builder, no CGItem needed: (out, outSize, itemID, suffixID).
|
||||
using BuildNameFromID_t = void(__fastcall *)(char *out, unsigned outSize,
|
||||
uint32_t itemID, int suffixID);
|
||||
|
||||
// Cached ItemStats record lookup. Same `_GetRecord` pattern the rest of
|
||||
// the codebase uses; passes a noop NULL callback so no SMSG fires for
|
||||
// uncached items — caller treats "not cached" as a clean failure.
|
||||
@@ -63,25 +67,45 @@ bool NameFromCGItem(const uint8_t *cgItem, char *out, size_t outSize) {
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize) {
|
||||
bool NameFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
auto fn = reinterpret_cast<BuildNameFromID_t>(
|
||||
Offsets::FUN_ITEM_BUILD_NAME_FROM_ID);
|
||||
fn(out, static_cast<unsigned>(outSize), itemID, suffixID);
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
bool BasicFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
const uint8_t *record = PeekItemRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
char name[128];
|
||||
if (!NameFromIDSuffix(itemID, suffixID, name, sizeof(name))) {
|
||||
// Suffix builder yielded nothing (e.g. suffix row missing) — fall back
|
||||
// to the base record name so we at least produce a valid link.
|
||||
const char *base = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (base == nullptr || *base == '\0')
|
||||
return false;
|
||||
std::snprintf(name, sizeof(name), "%s", base);
|
||||
}
|
||||
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
const int n = std::snprintf(out, outSize,
|
||||
"%s|Hitem:%u:0:0:0|h[%s]|h|r",
|
||||
"%s|Hitem:%u:0:%d:0|h[%s]|h|r",
|
||||
Item::QualityColor::Prefix(static_cast<int>(quality)),
|
||||
itemID, name);
|
||||
itemID, suffixID, name);
|
||||
return n > 0 && static_cast<size_t>(n) < outSize;
|
||||
}
|
||||
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize) {
|
||||
return BasicFromIDSuffix(itemID, 0, out, outSize);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// `C_Item.GetItemLink(itemLocation)` — returns the fully-decorated
|
||||
|
||||
@@ -51,4 +51,18 @@ bool NameFromCGItem(const uint8_t *cgItem, char *out, size_t outSize);
|
||||
// possible link (`item:` payload + colored prefix + bracketed name).
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize);
|
||||
|
||||
// Writes the suffix-decorated display name for `(itemID, suffixID)` into
|
||||
// `out` WITHOUT a live item instance — the by-string analog of
|
||||
// NameFromCGItem. `suffixID` is the 3rd `item:` field (random property /
|
||||
// suffix); 0 = base name. Applies the "... of the Owl" suffix via the
|
||||
// engine's inner builder (FUN_ITEM_BUILD_NAME_FROM_ID) when the item is
|
||||
// cached and the suffix is a valid ItemRandomProperties row. Returns false
|
||||
// (out empty) if the item isn't cached or has no name — warm and retry.
|
||||
bool NameFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize);
|
||||
|
||||
// Like BasicFromItemID but preserves the random `suffixID` in both the
|
||||
// `item:` payload (`item:N:0:suffix:0`) and the bracketed, suffix-decorated
|
||||
// name. `suffixID` 0 reduces exactly to BasicFromItemID.
|
||||
bool BasicFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize);
|
||||
|
||||
} // namespace Item::Link
|
||||
|
||||
+20
-1
@@ -94,8 +94,27 @@ static int __fastcall Script_C_Item_GetItemName(void *L) {
|
||||
// for `itemID`, firing a background cache fill on miss. Accepts the
|
||||
// same input shapes as `GetItemInfoInstant` (number, `"item:NNN..."`
|
||||
// string, or any string containing a parseable `item:NN` chunk).
|
||||
//
|
||||
// When the input is an item link carrying a random suffix
|
||||
// (`item:12022:0:769:0`), the returned name is suffix-decorated
|
||||
// ("Iridium Chain of the Owl") — the 3rd `item:` field is parsed and
|
||||
// applied via the engine's own name builder, so it matches the global
|
||||
// `GetItemInfo`'s name. A bare itemID (number or `item:N`) has no suffix
|
||||
// and yields the base name.
|
||||
static int __fastcall Script_C_Item_GetItemNameByID(void *L) {
|
||||
return PushNameForItemID(L, Item::Arg::ResolveItemID(L, 1));
|
||||
const Item::Arg::Resolved r = Item::Arg::Resolve(L, 1);
|
||||
if (r.itemID <= 0)
|
||||
return 0;
|
||||
char buf[128];
|
||||
if (Item::Link::NameFromIDSuffix(static_cast<uint32_t>(r.itemID), r.suffix,
|
||||
buf, sizeof(buf))) {
|
||||
Game::Lua::PushString(L, buf);
|
||||
return 1;
|
||||
}
|
||||
// Not cached yet — fire a background fill and return nil (same miss
|
||||
// behavior as the base-name path).
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(r.itemID));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
|
||||
@@ -110,6 +110,8 @@ constexpr uint32_t kSettleMs = 1500;
|
||||
// ~currently-owned items rather than everything ever owned.
|
||||
constexpr int kMaxMissStreak = 4;
|
||||
|
||||
constexpr int kMaxContainerSlots = 64;
|
||||
|
||||
// One seen-set entry: an owned item's GUID and how many consecutive scans it
|
||||
// has been absent from the read (0 = present this scan).
|
||||
struct SeenEntry {
|
||||
@@ -313,7 +315,9 @@ void AppendNonBagOwned(uint64_t *out, int *count) {
|
||||
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (bagInvMgr == nullptr)
|
||||
continue;
|
||||
const int bagSlots = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInvMgr));
|
||||
int bagSlots = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInvMgr));
|
||||
if (bagSlots > kMaxContainerSlots)
|
||||
bagSlots = kMaxContainerSlots; // guard against a garbage count at login
|
||||
if (bagSlots > 0)
|
||||
AppendGuidArray(bagInvMgr, 0, bagSlots - 1, out, count);
|
||||
}
|
||||
|
||||
+123
-37
@@ -8,16 +8,27 @@
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// Hook-driven loot scanner. Walks the nearby-lootable-units set, opens
|
||||
// each corpse's loot session via `FUN_CMSG_LOOT_UNIT`, then *intercepts
|
||||
// the engine's loot-window response* before any visual updates happen.
|
||||
// The intercept (`FUN_LOOT_CONTROLLER`) lets the original function run
|
||||
// so `VAR_LOOT_SLOTS` populates and the link builder works — but a
|
||||
// Hook-driven loot corpse-walker. Walks the nearby-lootable-units set,
|
||||
// opens each corpse's loot session via `FUN_CMSG_LOOT_UNIT`, then
|
||||
// *intercepts the engine's loot-window response* before any visual updates
|
||||
// happen. The intercept (`FUN_LOOT_CONTROLLER`) lets the original function
|
||||
// run so `VAR_LOOT_SLOTS` populates and the link builder works — but a
|
||||
// paired hook on `FUN_FIRE_EVENT_NO_ARGS` swallows the `LOOT_OPENED` /
|
||||
// `LOOT_CLOSED` event fires while the scan is in progress. Net result:
|
||||
// the engine's state machine cycles silently for each corpse and
|
||||
// `LootFrame` never reacts. Other Lua addons listening to those events
|
||||
// also see nothing during the scan; that's deliberate.
|
||||
// `LOOT_CLOSED` event fires while the walk is in progress. Net result: the
|
||||
// engine's state machine cycles silently for each corpse and `LootFrame`
|
||||
// never reacts. Other Lua addons listening to those events also see nothing
|
||||
// during the walk; that's deliberate.
|
||||
//
|
||||
// Two Lua entry points share this one walker (and, importantly, this one
|
||||
// `FUN_LOOT_CONTROLLER` hook — MinHook allows a single hook per target, so
|
||||
// they can't be separate modules):
|
||||
// - `C_Loot.ScanNearbyLoot()` — scrape-only: read each corpse's contents
|
||||
// into `GetLastScanResults()` and release without looting.
|
||||
// - `C_Loot.LootAllCorpses([max])` — the same walk but actually LOOTS
|
||||
// each window (coin via `CMSG_LOOT_MONEY`, every item slot via
|
||||
// `CMSG_LOOT_ITEM`) before releasing. Silent (no LootFrame flicker);
|
||||
// the usual item-received / money chat + inventory events still fire,
|
||||
// since only the LootFrame open/close events are swallowed.
|
||||
//
|
||||
// Compared to the older polling-driven version this also collapses the
|
||||
// state machine — we no longer need `WaitingOpen` / `WaitingClose` /
|
||||
@@ -79,6 +90,7 @@ struct ScanCtx {
|
||||
uint64_t currentGuid = 0;
|
||||
int ticksSinceTransition = 0;
|
||||
bool suppressEvents = false; // read by the FireEvent hook
|
||||
bool lootMode = false; // LootAllCorpses (true) vs ScanNearbyLoot (false)
|
||||
std::vector<uint64_t> queue;
|
||||
std::vector<LootEntry> results;
|
||||
};
|
||||
@@ -106,6 +118,12 @@ using CloseLootInner_t = void(__fastcall *)(int sendRelease,
|
||||
using LootSlotLinkBuilder_t = const char *(__fastcall *)(uint32_t userFacingSlot,
|
||||
char *outBuf,
|
||||
int bufSize);
|
||||
// `FUN_CMSG_LOOT_ITEM` — `__stdcall(uint8_t wireSlot)`, sends CMSG_LOOT_ITEM
|
||||
// for one slot of the open window (see the offset comment for the
|
||||
// __stdcall-vs-__fastcall stack-corruption hazard).
|
||||
using SendLootItem_t = void(__stdcall *)(uint8_t wireSlot);
|
||||
// `FUN_CMSG_LOOT_MONEY` — `__fastcall(player)`, sends the coin request.
|
||||
using LootMoney_t = void(__fastcall *)(void *player);
|
||||
// `FUN_LOOT_CONTROLLER` is `__fastcall(int, undefined4, int)` — three
|
||||
// args: lootStructPtr in ECX, coin in EDX, an extra unk on stack[0].
|
||||
// Mis-declaring as 4-arg __fastcall would push one phantom stack slot
|
||||
@@ -207,6 +225,42 @@ void ScrapeCurrentLoot(LootEntry *out) {
|
||||
}
|
||||
}
|
||||
|
||||
// Loots everything in the currently-open window: coin (if any) plus every
|
||||
// populated item slot. Called from the controller hook (loot mode) after the
|
||||
// original populated `VAR_LOOT_SLOTS`, so the slot table is fully filled in.
|
||||
//
|
||||
// Items go through `FUN_CMSG_LOOT_ITEM` directly — the same silent path
|
||||
// `C_Loot.LootUnitItem` uses, which bypasses the BoP/unique confirm dialog
|
||||
// the Lua `LootSlot` wrapper would raise (appropriate for a programmatic
|
||||
// loot-all; the server still enforces real permissions). Coin goes through
|
||||
// `FUN_CMSG_LOOT_MONEY` with `s_scan.player` — the SAME pointer we passed to
|
||||
// `FUN_CMSG_LOOT_UNIT`, whose send wrote this window's target GUID to the
|
||||
// player's `+0x1D28` guard slot, so the money send's guard passes.
|
||||
//
|
||||
// Packet order to the server is items, then coin, then the release that
|
||||
// follows (`SendCloseLoot`); the server processes loot-item / loot-money
|
||||
// before the release, so nothing is dropped.
|
||||
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);
|
||||
if (coinRaw != 0 && coinRaw != 0xFFFFFFFFu) {
|
||||
auto LootMoney = reinterpret_cast<LootMoney_t>(Offsets::FUN_CMSG_LOOT_MONEY);
|
||||
LootMoney(s_scan.player);
|
||||
}
|
||||
|
||||
auto SendItem = reinterpret_cast<SendLootItem_t>(Offsets::FUN_CMSG_LOOT_ITEM);
|
||||
for (int slot = 0; slot < Offsets::LOOT_MAX_SLOTS; ++slot) {
|
||||
auto *entry = reinterpret_cast<const uint8_t *>(
|
||||
Offsets::VAR_LOOT_SLOTS + slot * Offsets::LOOT_SLOT_STRIDE);
|
||||
const uint32_t itemID = *reinterpret_cast<const uint32_t *>(entry);
|
||||
if (itemID == 0)
|
||||
continue;
|
||||
SendItem(*(entry + Offsets::OFF_LOOT_SLOT_WIRE_INDEX));
|
||||
}
|
||||
}
|
||||
|
||||
// === State-machine transitions ===
|
||||
|
||||
void StartLoot(uint64_t guid);
|
||||
@@ -221,6 +275,7 @@ void Complete() {
|
||||
s_scan.state = State::Idle;
|
||||
s_scan.currentGuid = 0;
|
||||
s_scan.suppressEvents = false;
|
||||
s_scan.lootMode = false;
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEventCompleted), "");
|
||||
}
|
||||
|
||||
@@ -315,6 +370,12 @@ void __fastcall LootController_h(int lootStructPtr, int coin, int unk) {
|
||||
ScrapeCurrentLoot(&entry);
|
||||
s_scan.results.push_back(std::move(entry));
|
||||
|
||||
// Loot mode: actually take everything (coin + all slots) before the
|
||||
// release below. Scrape ran first, so `GetLastScanResults()` still
|
||||
// reports what each corpse held.
|
||||
if (s_scan.lootMode)
|
||||
LootCurrentWindow();
|
||||
|
||||
// Sends `CMSG_LOOT_RELEASE` and recursively calls this hook
|
||||
// with `arg1 == 0`. That recursive call falls into the
|
||||
// passthrough branch but still sees `suppressEvents == true`
|
||||
@@ -377,46 +438,69 @@ int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void BuildQueue(void *player) {
|
||||
void BuildQueue(void *player, size_t maxCount) {
|
||||
s_scan.queue.clear();
|
||||
auto Enum = reinterpret_cast<ClntObjMgrEnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
EnumCtx ctx{player, &s_scan.queue};
|
||||
Enum(reinterpret_cast<ClntObjMgrEnumVisibleObjectsCallback_t>(&EnumCallback),
|
||||
&ctx);
|
||||
// Cap to `maxCount` corpses (0 = unlimited). Order is engine
|
||||
// enumeration order, not distance-sorted, so the truncation is
|
||||
// arbitrary among in-range corpses.
|
||||
if (maxCount != 0 && s_scan.queue.size() > maxCount)
|
||||
s_scan.queue.resize(maxCount);
|
||||
}
|
||||
|
||||
// Shared starter for both entry points: resolve the player, build the
|
||||
// corpse queue, kick off the walk. Returns whether it started — `false` if a
|
||||
// walk is already in progress or there's no local player.
|
||||
bool BeginWalk(bool lootMode, size_t maxCount) {
|
||||
if (s_scan.state != State::Idle)
|
||||
return false;
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr)
|
||||
return false;
|
||||
auto Resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = Resolve("player");
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
s_scan.results.clear();
|
||||
s_scan.player = player;
|
||||
s_scan.lootMode = lootMode;
|
||||
BuildQueue(player, maxCount);
|
||||
|
||||
if (s_scan.queue.empty()) {
|
||||
Complete(); // nothing nearby — fire LOOT_SCAN_COMPLETED immediately
|
||||
return true;
|
||||
}
|
||||
TryStartNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
// === Lua bindings ===
|
||||
|
||||
// `C_Loot.ScanNearbyLoot()` — scrape-only walk. Returns `true` if it
|
||||
// started (a subsequent `GetLastScanResults()` holds the contents once
|
||||
// `LOOT_SCAN_COMPLETED` fires), `false` if a walk is already running or
|
||||
// there's no local player.
|
||||
int __fastcall Script_ScanNearbyLoot(void *L) {
|
||||
if (s_scan.state != State::Idle) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto Resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = Resolve("player");
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L, BeginWalk(/*lootMode=*/false, /*maxCount=*/0));
|
||||
return 1;
|
||||
}
|
||||
|
||||
s_scan.results.clear();
|
||||
s_scan.player = player;
|
||||
BuildQueue(player);
|
||||
|
||||
if (s_scan.queue.empty()) {
|
||||
Complete();
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
TryStartNext();
|
||||
Game::Lua::PushBool(L, true);
|
||||
// `C_Loot.LootAllCorpses([max])` — loots every nearby lootable corpse
|
||||
// (coin + all items) in sequence. Optional `max` caps how many corpses are
|
||||
// visited. Returns `true` if it started, `false` if a walk (scan or loot)
|
||||
// is already in progress or there's no local player. `LOOT_SCAN_COMPLETED`
|
||||
// fires when the walk finishes; `GetLastScanResults()` then reports what
|
||||
// each corpse held (i.e. what was looted).
|
||||
int __fastcall Script_LootAllCorpses(void *L) {
|
||||
const size_t maxCount = Game::Lua::IsNumber(L, 1)
|
||||
? static_cast<size_t>(Game::Lua::ToNumber(L, 1))
|
||||
: 0;
|
||||
Game::Lua::PushBool(L, BeginWalk(/*lootMode=*/true, maxCount));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -462,6 +546,8 @@ int __fastcall Script_GetLastScanResults(void *L) {
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "ScanNearbyLoot",
|
||||
&Script_ScanNearbyLoot);
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "LootAllCorpses",
|
||||
&Script_LootAllCorpses);
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "IsScanInProgress",
|
||||
&Script_IsScanInProgress);
|
||||
Game::Lua::RegisterTableFunction("C_Loot", "GetLastScanResults",
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
// mirroring the engine's registration shape exactly (bank 4, size 8). The
|
||||
// dispatcher invokes us only when a slot's item GUID actually changed
|
||||
// (per-node mirror memcmp), so there's no polling and no diff bookkeeping
|
||||
// on our side, and the nodes share the engine observers' lifetime (they
|
||||
// die with the player object; the setup re-runs each enter-world).
|
||||
// on our side. The setup re-runs each enter-world; on logout/login the
|
||||
// player object is recreated so old nodes die with it, but /reload REUSES
|
||||
// the object, so we unregister-then-register per slot to avoid stacking a
|
||||
// duplicate observer each reload (see ObserverSetup_h).
|
||||
//
|
||||
// `equipmentSlot` is the 1-based Lua inventory slot (1 = head … 19 =
|
||||
// tabard), matching modern. `hasCurrent` is 1 when the slot now holds an
|
||||
@@ -126,6 +128,10 @@ int __fastcall EquipSlotChanged_cb(uint32_t fieldOffset, uint32_t /*size*/,
|
||||
using RegisterObserver_t = void(__fastcall *)(
|
||||
int bank, uint32_t fieldOffset, uint32_t guidLo, uint32_t guidHi, int size,
|
||||
const void *callback, void *userArg1, void *userArg2);
|
||||
using UnregisterObserver_t = void(__fastcall *)(int bank, uint32_t fieldOffset,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
const void *callback,
|
||||
void *userArg1);
|
||||
using ObserverSetup_t = void(__fastcall *)();
|
||||
ObserverSetup_t g_origSetup = nullptr;
|
||||
|
||||
@@ -147,12 +153,26 @@ void __fastcall ObserverSetup_h() {
|
||||
const uint64_t guid = Unit::Identity::PlayerGuid();
|
||||
if (guid == 0)
|
||||
return;
|
||||
const uint32_t lo = static_cast<uint32_t>(guid);
|
||||
const uint32_t hi = static_cast<uint32_t>(guid >> 32);
|
||||
auto reg = reinterpret_cast<RegisterObserver_t>(
|
||||
Offsets::FUN_DESC_OBSERVER_REGISTER);
|
||||
auto unreg = reinterpret_cast<UnregisterObserver_t>(
|
||||
Offsets::FUN_DESC_OBSERVER_UNREGISTER);
|
||||
for (int slot = 0; slot < Offsets::DESC_PLAYER_EQUIP_SLOTS; ++slot) {
|
||||
reg(Offsets::DESC_OBSERVER_BANK_PLAYER,
|
||||
Offsets::OFF_DESC_PLAYER_EQUIP_FIRST + slot * 8,
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
|
||||
const uint32_t field = Offsets::OFF_DESC_PLAYER_EQUIP_FIRST + slot * 8;
|
||||
// Drop any observer we already registered for this field before adding
|
||||
// a fresh one. The setup re-runs on every enter-world, including
|
||||
// /reload — but /reload does NOT destroy the CGPlayer object, so our
|
||||
// previous nodes are still attached and the registrar always APPENDS
|
||||
// (no dedup by callback). Without this, each /reload stacks another
|
||||
// observer per slot → PLAYER_EQUIPMENT_CHANGED fires N times after N
|
||||
// reloads. On logout/login the object is recreated and the old nodes
|
||||
// died with it, so the unregister no-ops there. Same objects-survive-
|
||||
// /reload hazard `Unit::TokenObserver` guards against for nameplates.
|
||||
unreg(Offsets::DESC_OBSERVER_BANK_PLAYER, field, lo, hi,
|
||||
reinterpret_cast<const void *>(&EquipSlotChanged_cb), nullptr);
|
||||
reg(Offsets::DESC_OBSERVER_BANK_PLAYER, field, lo, hi,
|
||||
/*size*/ 8, reinterpret_cast<const void *>(&EquipSlotChanged_cb),
|
||||
nullptr, nullptr);
|
||||
}
|
||||
|
||||
@@ -10,19 +10,27 @@
|
||||
|
||||
// `HEARTHSTONE_BOUND` event — polyfills modern WoW's event of the
|
||||
// same name. Fires every time the player binds their hearthstone at
|
||||
// an innkeeper, even when they re-bind at the SAME inn — matches
|
||||
// an innkeeper, even when they re-bind at the SAME inn — matching
|
||||
// modern semantics where the bind ACTION fires the event regardless
|
||||
// of whether the location string changes.
|
||||
// of whether the location changes.
|
||||
//
|
||||
// Hooks the BINDPOINTUPDATE packet handler at `FUN_005ED3C0` and
|
||||
// gates on `VAR_BIND_POINT_VALID` to suppress only the initial
|
||||
// post-login sync. The engine zeroes that flag during per-character
|
||||
// entry init (`FUN_005E2510`); the handler sets it to 1 after
|
||||
// parsing. If the flag is 0 before the original runs, this packet
|
||||
// is the initial sync — don't fire. Any subsequent invocation is
|
||||
// a real innkeeper rebind. Character switch is handled automatically
|
||||
// because the entry init re-zeroes the flag, so each character's
|
||||
// first update is also treated as sync.
|
||||
// Hooks the BINDPOINTUPDATE packet handler at `FUN_005ED3C0`. Two
|
||||
// non-bind packets must be suppressed:
|
||||
//
|
||||
// 1. Initial post-login / character-switch sync — gated on
|
||||
// `VAR_BIND_POINT_VALID`. The engine zeroes that flag during
|
||||
// per-character entry init (`FUN_005E2510`); the handler sets it
|
||||
// to 1 after parsing. Flag 0 before the original runs ⇒ initial
|
||||
// sync, don't fire.
|
||||
// 2. Map/zone-transition resync — Turtle's server re-sends this
|
||||
// packet on every map change with the (unchanged) bind location.
|
||||
// The valid flag is already 1 by then, so (1) doesn't catch it.
|
||||
// But those packets arrive behind a LOADING SCREEN, and the
|
||||
// player can't bind mid-load — so gate on `VAR_IN_WORLD` (the
|
||||
// engine's "world is live" flag, 0 during any loading screen).
|
||||
// A genuine rebind always arrives with the world live (flag 1),
|
||||
// so this suppresses the resync while still firing for a rebind
|
||||
// at the same innkeeper.
|
||||
//
|
||||
// Event has no payload — addons call `GetBindLocation()` to read
|
||||
// the new location, matching modern semantics.
|
||||
@@ -51,7 +59,14 @@ void __fastcall BindPointUpdateHandler_h(void *packetBuffer) {
|
||||
BindPointUpdateHandler_o(packetBuffer);
|
||||
|
||||
if (wasValid == 0)
|
||||
return; // initial post-login sync — not a user rebind
|
||||
return; // initial post-login / char-switch sync — not a user rebind
|
||||
|
||||
// Resync packets ride the enter-world burst behind a loading screen;
|
||||
// a real innkeeper bind only happens with the world live. The player
|
||||
// can't bind mid-load, so a not-in-world packet is always a resync.
|
||||
if (*reinterpret_cast<const volatile uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_IN_WORLD)) == 0)
|
||||
return;
|
||||
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
Event::Custom::Fire(slot, "");
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetTimeCached()` — a frame-stable companion to vanilla `GetTime()`.
|
||||
//
|
||||
// 1.12's `GetTime()` is *live*: `Script_GetTime` (`0x00515ea0`) calls the
|
||||
// OS tick source (`FUN_OS_TICKCOUNT_MS` → `GetTickCount`, or QPC in
|
||||
// timing-mode 1) on every invocation and scales by 0.001, so two calls in
|
||||
// the same frame can differ. Retail 4.x changed `GetTime()` to read a
|
||||
// value sampled once per frame in the main loop, so all same-frame calls
|
||||
// return the identical timestamp. Rather than change vanilla `GetTime`'s
|
||||
// long-standing live behavior, we expose the frame-stable value under a
|
||||
// new name and leave `GetTime()` alone.
|
||||
//
|
||||
// `GetTimeCached()` returns seconds on the SAME epoch as `GetTime()` (the
|
||||
// OS millisecond counter × 0.001), so the two are directly comparable —
|
||||
// the only difference is that `GetTimeCached()` holds constant for the
|
||||
// duration of a frame. We refresh the cache from the shared once-per-frame
|
||||
// `Tick::WorldTick` hook (which fires at the tail of each world frame).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Time::Cached {
|
||||
|
||||
namespace {
|
||||
|
||||
// OS millisecond counter sampled at the last frame boundary. 0 until the
|
||||
// first world tick fires (pre-world). Read as uint32 — `GetTickCount`
|
||||
// wraps at ~24.86 days, matching how the rest of the codebase treats this
|
||||
// counter (see aura/Data.cpp), and how `Script_GetTime` masks it to 32
|
||||
// bits before scaling.
|
||||
uint32_t g_frameMs = 0;
|
||||
|
||||
uint32_t SampleTickMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
|
||||
// Tail-of-frame refresh. Tail-of-frame-N ≈ start-of-frame-(N+1) minus the
|
||||
// inter-frame gap, which is all a frame-stable timestamp needs: every read
|
||||
// during a frame returns the value stamped at the previous boundary.
|
||||
void OnWorldTick() {
|
||||
g_frameMs = SampleTickMs();
|
||||
}
|
||||
|
||||
// `GetTimeCached()` → number (seconds). Frame-stable analog of
|
||||
// `GetTime()`, same epoch. Falls back to a live sample when no frame has
|
||||
// ticked yet (pre-world / glue), so it never returns 0.
|
||||
int __fastcall Script_GetTimeCached(void *L) {
|
||||
uint32_t ms = g_frameMs;
|
||||
if (ms == 0)
|
||||
ms = SampleTickMs();
|
||||
Game::Lua::PushNumber(L, static_cast<double>(ms) * 0.001);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetTimeCached", &Script_GetTimeCached);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Time::Cached
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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/>.
|
||||
|
||||
// UPDATE_MOUSEOVER_UNIT loss-fire polyfill.
|
||||
//
|
||||
// Vanilla fires `UPDATE_MOUSEOVER_UNIT` only when a mouseover is *gained*
|
||||
// (the engine's `FUN_SET_MOUSEOVER_UNIT` fires it inline in the
|
||||
// friendly/hostile-unit tooltip case). When the cursor leaves the unit,
|
||||
// the engine clears the mouseover GUID but fires nothing — so an addon
|
||||
// that reacted to `UnitExists("mouseover")` becoming true never gets told
|
||||
// when it becomes false. Retail fires the event for both gain and loss.
|
||||
//
|
||||
// We close the gap by co-hooking the single set/clear chokepoint (it is
|
||||
// the ONLY writer of the mouseover GUID globals, so every gain / loss /
|
||||
// change flows through it): resolve whether the mouseover was a *unit*
|
||||
// before the original, then after, and fire `UPDATE_MOUSEOVER_UNIT` when
|
||||
// it went from a unit to a non-unit.
|
||||
//
|
||||
// The unit-vs-non-unit test matters: the mouseover GUID also holds
|
||||
// gameobjects (and items), but the engine only fires the *gain* event for
|
||||
// units — the gameobject/item cases never fire it. So a naive
|
||||
// "GUID present -> absent" test spuriously fires when you move off a
|
||||
// gameobject that never fired a gain. Gating on "old was a unit, new
|
||||
// isn't" fixes that while still matching retail's other cases:
|
||||
//
|
||||
// * unit -> nothing : fire (retail fires)
|
||||
// * unit -> gameobject : fire (retail fires — mouseover unit lost)
|
||||
// * gameobject -> nothing: no (bug fixed; no gain ever fired)
|
||||
// * X -> unit : engine already fired inline; we don't (new is
|
||||
// a unit, so our `!newIsUnit` guard is false —
|
||||
// no double-fire on gain or unit->unit change)
|
||||
//
|
||||
// The event carries no arguments in vanilla (same as retail); handlers
|
||||
// read `UnitExists("mouseover")` — which is false at fire time on the loss
|
||||
// path, exactly matching retail semantics.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Mouseover {
|
||||
|
||||
namespace {
|
||||
|
||||
// `FUN_SET_MOUSEOVER_UNIT` is `__stdcall(guidLo, guidHi, arg3, arg4)` —
|
||||
// see Offsets.h. We don't use the args; they pass straight through.
|
||||
using SetMouseover_t = void(__stdcall *)(uint32_t guidLo, uint32_t guidHi,
|
||||
uint32_t arg3, uint32_t arg4);
|
||||
|
||||
using FireEventNoArgs_t = void(__fastcall *)(int eventID);
|
||||
|
||||
// `FUN_OBJECT_RESOLVE_BY_GUID(typeMask, debugName, guidLo, guidHi, prio)`
|
||||
// — keeps the object only if its type flags intersect `typeMask`, so the
|
||||
// unit|player mask returns non-null only for a live unit / player / pet /
|
||||
// MC'd creature (never a gameobject or item). Same primitive
|
||||
// `Unit::Flags::ResolveUnitOrPlayerByGuid` uses.
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int typeMask,
|
||||
const char *debugName,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
// True iff the current mouseover GUID resolves to a unit (not a
|
||||
// gameobject / item / nothing).
|
||||
bool MouseoverIsUnit() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MOUSEOVER_GUID_LO));
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MOUSEOVER_GUID_HI));
|
||||
if ((lo | hi) == 0)
|
||||
return false;
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
return fn(kUnitOrPlayerMask, "UPDATE_MOUSEOVER_UNIT", lo, hi, 0) != nullptr;
|
||||
}
|
||||
|
||||
SetMouseover_t SetMouseover_o = nullptr;
|
||||
|
||||
void __stdcall SetMouseover_h(uint32_t guidLo, uint32_t guidHi,
|
||||
uint32_t arg3, uint32_t arg4) {
|
||||
const bool hadUnit = MouseoverIsUnit();
|
||||
SetMouseover_o(guidLo, guidHi, arg3, arg4);
|
||||
const bool hasUnit = MouseoverIsUnit();
|
||||
|
||||
// Only the unit-loss transition is missing from the engine. Any
|
||||
// ->unit transition (gain / unit->unit change) already fired inline
|
||||
// in the original, so `!hasUnit` prevents a double-fire there.
|
||||
if (hadUnit && !hasUnit) {
|
||||
auto fire = reinterpret_cast<FireEventNoArgs_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS));
|
||||
fire(Offsets::EVENT_UPDATE_MOUSEOVER_UNIT);
|
||||
}
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_SET_MOUSEOVER_UNIT,
|
||||
reinterpret_cast<void *>(&SetMouseover_h),
|
||||
reinterpret_cast<void **>(&SetMouseover_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::Mouseover
|
||||
@@ -31,6 +31,22 @@ namespace {
|
||||
|
||||
using CancelAuraSend_t = void(__fastcall *)(int spellID);
|
||||
|
||||
// `FUN_UNIT_IS_OUTDOORS(unit) -> uint`, nonzero = outdoors (see Offsets.h).
|
||||
// The thunk tail-jumps into the WMO query, so eax carries the result even
|
||||
// though a decompiler renders the thunk itself as `void`.
|
||||
using IsOutdoorsQuery_t = uint32_t(__fastcall *)(const void *unit);
|
||||
|
||||
// Tri-state indoor/outdoor read for the local player: 1 = outdoors,
|
||||
// 0 = indoors, -1 = unknown (no resolvable player object, e.g. pre-world).
|
||||
int PlayerOutdoorState() {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return -1;
|
||||
auto fn = reinterpret_cast<IsOutdoorsQuery_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_UNIT_IS_OUTDOORS));
|
||||
return fn(player) != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t PlayerMovementFlags() {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
@@ -200,10 +216,37 @@ int __fastcall Script_IsAssistingRitual(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `IsIndoors()` — 1 iff the local player is under a WMO roof (inside a
|
||||
// building / cave / instance interior), else nil. Recomputed live from the
|
||||
// engine's WMO geometry query, so it flips as you cross an interior
|
||||
// threshold — not tied to the zone/area. nil before the player object
|
||||
// exists (pre-world). Vanilla 1.12 ships no binding for this.
|
||||
//
|
||||
int __fastcall Script_IsIndoors(void *L) {
|
||||
if (PlayerOutdoorState() == 0)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `IsOutdoors()` — 1 iff the local player is outdoors (open sky, or not
|
||||
// inside a WMO interior), else nil. Exact complement of `IsIndoors` for a
|
||||
// resolvable player; both return nil pre-world. See `Script_IsIndoors`.
|
||||
int __fastcall Script_IsOutdoors(void *L) {
|
||||
if (PlayerOutdoorState() == 1)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsMounted", &Script_IsMounted);
|
||||
Game::Lua::RegisterGlobalFunction("IsIndoors", &Script_IsIndoors);
|
||||
Game::Lua::RegisterGlobalFunction("IsOutdoors", &Script_IsOutdoors);
|
||||
Game::Lua::RegisterGlobalFunction("Dismount", &Script_Dismount);
|
||||
Game::Lua::RegisterGlobalFunction("IsStealthed", &Script_IsStealthed);
|
||||
Game::Lua::RegisterGlobalFunction("IsFalling", &Script_IsFalling);
|
||||
|
||||
Reference in New Issue
Block a user