mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 14:47:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d91ae0f91 | ||
|
|
fedccf5b95 | ||
|
|
0ba73b8457 | ||
|
|
d9565fbe1f | ||
|
|
61db68dd7f | ||
|
|
f895d38f76 | ||
|
|
804ed61c76 | ||
|
|
aacace1645 | ||
|
|
4bf2c17e0f | ||
|
|
3191c4dcfc | ||
|
|
373dce8202 | ||
|
|
4c7f1c36d6 | ||
|
|
36c367a6d6 | ||
|
|
fe9e7dddcd | ||
|
|
9d8f1b1b5d | ||
|
|
9d1e14410c | ||
|
|
8b06caf708 | ||
|
|
4684088675 | ||
|
|
9a26207df2 | ||
|
|
2adfaff020 | ||
|
|
8a7ab490f4 | ||
|
|
e26f8958e9 | ||
|
|
f0a83f1f31 | ||
|
|
5070c892e2 | ||
|
|
8295ec9442 | ||
|
|
c5b1431073 | ||
|
|
b39c16578b | ||
|
|
7d7e392924 | ||
|
|
14fa610bc8 | ||
|
|
8ca415e9a0 | ||
|
|
becf2f4866 | ||
|
|
28658c2995 | ||
|
|
f5cce25d23 | ||
|
|
1306eaf1b8 | ||
|
|
807afc083f | ||
|
|
c26afcc050 | ||
|
|
a124629669 | ||
|
|
be03d45f0c | ||
|
|
66c878d03e | ||
|
|
3ad87eb2f3 | ||
|
|
a9d2ad73f3 | ||
|
|
db88fca050 | ||
|
|
26af9bd30f | ||
|
|
e7156caa47 | ||
|
|
02ad0ea6ab | ||
|
|
1dee8e5683 | ||
|
|
583f4bb011 | ||
|
|
23ebfbe7ab | ||
|
|
18927551d5 | ||
|
|
2c2f280c51 | ||
|
|
991baa2610 | ||
|
|
e68b690cf1 | ||
|
|
e5690b1c6a | ||
|
|
e91b1457fd | ||
|
|
09c5832ae8 | ||
|
|
4015fee5e2 | ||
|
|
744bfb31d1 | ||
|
|
8706d1d99b | ||
|
|
8637bf05e5 | ||
|
|
802039fd08 | ||
|
|
d14f0b44c1 | ||
|
|
008282d65a | ||
|
|
9a8c314d69 | ||
|
|
4b1bf16ba5 | ||
|
|
5528fb5fa4 | ||
|
|
7ab32df2aa | ||
|
|
8e1429bfac | ||
|
|
9322382596 | ||
|
|
c3cf9e6f3a | ||
|
|
f5d6046523 | ||
|
|
e1ebf6ecce | ||
|
|
698827e9c5 | ||
|
|
60530e32fc | ||
|
|
3cffb71b20 | ||
|
|
7887237a6e | ||
|
|
98af79137b | ||
|
|
622882725f | ||
|
|
0517bb3753 | ||
|
|
fb17488237 | ||
|
|
b1ee8cc183 | ||
|
|
c08ded6db4 | ||
|
|
640f78b2eb | ||
|
|
a51ec40e52 | ||
|
|
2ce5b007af | ||
|
|
be92d721f0 | ||
|
|
b81f672ad7 | ||
|
|
fdbe66b057 | ||
|
|
bd1bd9f5d8 | ||
|
|
b6698ecc7e | ||
|
|
e123a6facc |
@@ -97,12 +97,28 @@ jobs:
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
|
||||
# Dev builds carry their own symbols. With the PDB sitting next to the DLL
|
||||
# the client's crash handler resolves our frames to file and line — a
|
||||
# tester's log reads `Texture::Mask::PrimIndexed_h+12 (Mask.cpp,246)`
|
||||
# instead of a bare address — and the map resolves `0001:offset` addresses
|
||||
# by hand when there is no dump. Both match THIS build and nothing else,
|
||||
# which is why they travel with it rather than being rebuilt later from the
|
||||
# same commit. Unlike the release bundle these need no off-putting name:
|
||||
# the artifact is behind the Actions UI, so whoever downloads it is already
|
||||
# the audience for symbols.
|
||||
- name: Upload artifact
|
||||
if: steps.ver.outputs.is_release != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ClassicAPI-${{ github.sha }}
|
||||
path: build/Release/ClassicAPI.dll
|
||||
# Shared parent is build/Release, so all three land at the zip root.
|
||||
path: |
|
||||
build/Release/ClassicAPI.dll
|
||||
build/Release/ClassicAPI.pdb
|
||||
build/Release/ClassicAPI.map
|
||||
# A symbol-less artifact looks identical to a good one until someone
|
||||
# needs a stack out of it, so fail the build instead of warning.
|
||||
if-no-files-found: error
|
||||
|
||||
# Bundle both debug artifacts into one zip so a crash from a shipped DLL
|
||||
# can be triaged exactly. The PDB gives WinDbg/x64dbg/VS a line-level
|
||||
|
||||
@@ -43,6 +43,7 @@ Util\Rectangle.lua
|
||||
Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UiMapPoint.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\AuraUtil.lua
|
||||
@@ -50,3 +51,4 @@ Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
Util\MacroOptions.lua
|
||||
Util\SecureStateDriver.lua
|
||||
Util\MacroIconSelection.lua
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
EventUtil.ContinueOnAddOnLoaded('pfUI', function()
|
||||
-- A pfUI that sets `handlesHookScript` was written against our
|
||||
-- additions and manages them itself, so none of the shims below apply
|
||||
-- to it.
|
||||
if pfUI.handlesHookScript then return end
|
||||
|
||||
-- pfUI\pfUI.lua redeclares the RAID_CLASS_COLORS object at least 14 times (insane!!)
|
||||
-- This stops it from replacing all our ColorMixin values
|
||||
pfUI.UpdateColors = function() end
|
||||
@@ -17,13 +22,12 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
-- CreateFrame to shadow HookScript with a falsy field so those forks
|
||||
-- fall back to their vanilla SetScript path. The wrapper is removed as
|
||||
-- soon as the actionbar module finishes loading so it affects nothing
|
||||
-- else. A pfUI is trusted to handle HookScript itself if EITHER signal
|
||||
-- is present: the maintained "brues" X-Website tag OR the
|
||||
-- `pfUI.handlesHookScript` capability flag. Without the website the
|
||||
-- flag is required; if neither is present we apply the shim.
|
||||
-- else. The maintained "brues" fork, identified by its X-Website tag,
|
||||
-- handles HookScript on its own buttons and is exempt from this part;
|
||||
-- every other fork gets the shim.
|
||||
local website = GetAddOnMetadata("pfUI", "X-Website")
|
||||
local hasBruesWebsite = website and strfind(website, 'brues')
|
||||
if not hasBruesWebsite and not pfUI.handlesHookScript then
|
||||
if not hasBruesWebsite then
|
||||
local _createFrame = CreateFrame
|
||||
local HookedCreateFrame = function(frameType, name, parent, template)
|
||||
local frame = _createFrame(frameType, name, parent, template)
|
||||
@@ -43,13 +47,10 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
end)
|
||||
end
|
||||
|
||||
if C_AddOns.DoesAddOnExist("SpecialTalentUI") then
|
||||
EventUtil.ContinueOnAddOnLoaded("SpecialTalentUI", CAPI_ApplyStandardColorGlobals)
|
||||
end
|
||||
|
||||
-- ShaguTweaks libpredict register's TBC events that ClassicAPI backports.
|
||||
if C_AddOns.DoesAddOnExist("ShaguTweaks") then
|
||||
EventUtil.ContinueOnAddOnLoaded("ShaguTweaks", function()
|
||||
if not ShaguTweaks or (ShaguTweaks.API and ShaguTweaks.API.classicapi_version) then return end
|
||||
local libp = ShaguTweaks.libpredict
|
||||
if libp then
|
||||
libp.sender:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
||||
@@ -79,6 +80,53 @@ end
|
||||
|
||||
CAPI_MouseoverClearedCompat(GameTooltip)
|
||||
|
||||
-- Restores vanilla calling for ONE frame's script handler. Our DLL hands script
|
||||
-- handlers modern positional arguments (`self`, then the event's own args) on
|
||||
-- top of the `this` / `arg1` globals vanilla uses. That is additive for a
|
||||
-- handler declared `function()`, but a handler declared with a parameter of its
|
||||
-- own -- one written to be BOTH called directly and used as a script -- now
|
||||
-- receives `self` in that parameter instead of the nil it was written for.
|
||||
--
|
||||
-- The wrapper below takes no parameters, so those extra arguments stop here,
|
||||
-- and the original runs with the empty argument list vanilla gave it. The
|
||||
-- globals are untouched by the nesting, so the handler still reads `this` and
|
||||
-- `event` normally.
|
||||
function CAPI_VanillaScriptArgsCompat(frame, script)
|
||||
if not frame then return end
|
||||
local original = frame:GetScript(script)
|
||||
if not original then return end
|
||||
frame:SetScript(script, function()
|
||||
original()
|
||||
end)
|
||||
end
|
||||
|
||||
-- LunaUnitFrames sets `SetupGroupHeader(groupType)` as its group headers'
|
||||
-- OnEvent while also calling it directly with a group name. As a handler it
|
||||
-- expects `groupType` to be nil so it falls back to `this.unitGroup`. Given
|
||||
-- `self` instead, it looks the header up by frame rather than by name, finds
|
||||
-- nothing, and indexes nil (units.lua:358, issue #41). Its raid counterpart
|
||||
-- takes the header frame itself, so `self` is already what it wants -- only the
|
||||
-- group headers need this.
|
||||
if C_AddOns.DoesAddOnExist("LunaUnitFrames") then
|
||||
EventUtil.ContinueOnAddOnLoaded("LunaUnitFrames", function()
|
||||
if not (LunaUF and LunaUF.Units) then return end
|
||||
|
||||
local function FixGroupHeader(unit)
|
||||
CAPI_VanillaScriptArgsCompat(getglobal("LUFHeader" .. (unit or "")),
|
||||
"OnEvent")
|
||||
end
|
||||
|
||||
-- Headers are built on demand, so cover the ones already up and every
|
||||
-- one created later.
|
||||
FixGroupHeader("party")
|
||||
FixGroupHeader("partytarget")
|
||||
FixGroupHeader("partypet")
|
||||
hooksecurefunc(LunaUF.Units, "LoadGroupHeader", function(self, unit)
|
||||
FixGroupHeader(unit)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
if C_AddOns.DoesAddOnExist("Puppeteer") then
|
||||
EventUtil.ContinueOnAddOnLoaded("Puppeteer", function()
|
||||
CAPI_MouseoverClearedCompat(PTEnemyUpdater)
|
||||
@@ -113,3 +161,97 @@ if C_AddOns.DoesAddOnExist("ModernSpellBook") and not HookScript then
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Compost-2.0 (rev 17406+, bundled by BigWigs among others) replaces most of
|
||||
-- its own body with stubs once it believes it is on Lua 5.1, on the reasoning
|
||||
-- that table recycling is pointless when the collector already does it:
|
||||
--
|
||||
-- local lua51 = loadstring("return function(...) return ... end") and true or false
|
||||
-- ...
|
||||
-- if lua51 then function lib:Erase() return {} end
|
||||
-- else function lib:Erase(t) --[[ clears t in place ]] end
|
||||
--
|
||||
-- Our 5.1 syntax backport makes that probe pass, so the stubs win. For
|
||||
-- GetTable and Reclaim that is harmless -- both are pure recycling hints. Erase
|
||||
-- is not: callers hand it a live table and expect its keys gone. The stub takes
|
||||
-- no parameter at all, so every `compost:Erase(t)` that clears in place (rather
|
||||
-- than assigning the returned table) silently stops clearing.
|
||||
--
|
||||
-- Puppeteer is the reported casualty. PTUnit:ClearAuras erases its
|
||||
-- TrackedDebuffTypes, AfflictedDebuffTypes and aura-ID sets in place, so a
|
||||
-- dispel type never leaves the set once applied: a cursed raid member keeps its
|
||||
-- debuff colour after the curse is cleaned, and HasDebuffID answers true for
|
||||
-- auras that are long gone. It takes a second addon shipping the newer Compost
|
||||
-- to show up, because AceLibrary arbitrates on the highest $Revision and
|
||||
-- ignores load order -- so a lone Puppeteer keeps its own working rev 11579.
|
||||
--
|
||||
-- Restore an in-place Erase matching the pre-stub body. Probed by behavior
|
||||
-- rather than by revision or addon name, so a Compost that already clears is
|
||||
-- left alone.
|
||||
--
|
||||
-- Re-checked on every ADDON_LOADED rather than once, because AceLibrary can
|
||||
-- upgrade a library at any point during load -- including from an addon loaded
|
||||
-- on demand mid-session -- and an upgrade installs the new copy's functions
|
||||
-- over ours. We cannot simply wait for a late event either, since this addon
|
||||
-- loads first and AceLibrary does not exist yet at that point.
|
||||
--
|
||||
-- Repeating is cheap because the verdict is cached against the function that
|
||||
-- earned it: a check settles to two identity comparisons once the live Erase
|
||||
-- has been either vetted or replaced, and only an Erase we have never seen
|
||||
-- costs a probe. The probe table is reused across checks (refilled each time,
|
||||
-- so leftover state cannot skew the answer) rather than built per check.
|
||||
local compostProbe = {}
|
||||
local vettedErase
|
||||
|
||||
local function EraseInPlace(self, t)
|
||||
if type(t) ~= "table" then return end
|
||||
setmetatable(t, nil)
|
||||
for k in pairs(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
table.setn(t, 0)
|
||||
return t
|
||||
end
|
||||
|
||||
local function EnsureCompostErasesInPlace()
|
||||
if not (AceLibrary and AceLibrary.HasInstance
|
||||
and AceLibrary:HasInstance("Compost-2.0")) then
|
||||
return
|
||||
end
|
||||
|
||||
local compost = AceLibrary("Compost-2.0")
|
||||
if compost.Erase == EraseInPlace or compost.Erase == vettedErase then
|
||||
return
|
||||
end
|
||||
|
||||
compostProbe[1] = "array"
|
||||
compostProbe.key = "hash"
|
||||
compost:Erase(compostProbe)
|
||||
if next(compostProbe) == nil then
|
||||
vettedErase = compost.Erase -- clears in place; leave it alone
|
||||
return
|
||||
end
|
||||
|
||||
compost.Erase = EraseInPlace
|
||||
end
|
||||
|
||||
EventRegistry:RegisterFrameEventAndCallback("ADDON_LOADED",
|
||||
EnsureCompostErasesInPlace)
|
||||
EventRegistry:RegisterFrameEventAndCallback("PLAYER_LOGIN",
|
||||
EnsureCompostErasesInPlace)
|
||||
EnsureCompostErasesInPlace()
|
||||
|
||||
-- Some addons carry a copy of Blizzard's own color definitions -- a plain
|
||||
-- `NORMAL_FONT_COLOR = {r=1.0, g=0.82, b=0}` and its friends -- and assign
|
||||
-- them at file scope. That takes ColorMixin off those globals for every addon
|
||||
-- that loads later, which is what breaks pfUI's who list when it calls
|
||||
-- `NORMAL_FONT_COLOR:GetRGB()` (brues-code/pfUI#56).
|
||||
--
|
||||
-- Re-apply after each addon loads, so a clobbered color is repaired before the
|
||||
-- next addon reads it. This also covers an addon loaded on demand mid-session.
|
||||
-- The pass mixes the methods back into the table that is there, and costs one
|
||||
-- lookup per color when nothing is broken.
|
||||
EventRegistry:RegisterFrameEventAndCallback("ADDON_LOADED",
|
||||
CAPI_ApplyStandardColorGlobals)
|
||||
EventRegistry:RegisterFrameEventAndCallback("PLAYER_LOGIN",
|
||||
CAPI_ApplyStandardColorGlobals)
|
||||
|
||||
@@ -14,24 +14,61 @@
|
||||
-- * FindAuraByName(auraName, unit, filter)
|
||||
-- * UnpackAuraData(auraData)
|
||||
--
|
||||
-- Iteration batches through C_UnitAuras.GetAuraSlots + a by-slot fetch
|
||||
-- (continuation tokens), exactly as retail's does: one enumeration per batch,
|
||||
-- then a direct fetch per aura -- linear in the aura count. The by-index getters
|
||||
-- (GetAuraDataByIndex / UnitAura) re-walk the aura array from slot 0 on every
|
||||
-- call, so iterating by index is quadratic; don't go back to it here.
|
||||
--
|
||||
-- Two deliberate differences from that source:
|
||||
-- 1. Retail's ForEachAura/FindAura batch through GetAuraSlots / GetAuraDataBySlot
|
||||
-- (continuation tokens) -- 1.12 has no slot-batching API, so the bodies here
|
||||
-- iterate by index (1, 2, ...) until the accessor returns no aura. Same public
|
||||
-- contract, different internals.
|
||||
-- 2. The non-packed ForEachAura and FindAura source each aura from the positional
|
||||
-- C_UnitAuras.UnitAura (no table), so both are zero-allocation here -- FindAura
|
||||
-- in particular, where the retail version allocates a table per aura scanned.
|
||||
-- 1. Retail's helpers receive GetAuraSlots' return list as varargs
|
||||
-- (ForEachAuraHelper / FindAuraRecurse). Lua 5.0 builds an `arg` table for
|
||||
-- EVERY vararg call, so that shape allocates one table per batch here --
|
||||
-- exactly the per-frame garbage this library exists to avoid. We use
|
||||
-- GetAuraSlots' fill-a-table form instead (a table as its 5th argument,
|
||||
-- a ClassicAPI extension): the slot ids land in a reusable buffer and the
|
||||
-- call returns `continuationToken, n`. No vararg frame anywhere.
|
||||
-- 2. The non-packed ForEachAura and FindAura fetch each aura through the
|
||||
-- positional C_UnitAuras.UnitAuraBySlot (no table), the way retail's
|
||||
-- AuraUtil did before it retired the positional by-slot accessor. So both
|
||||
-- are zero-allocation here -- FindAura in particular, where the current
|
||||
-- retail version allocates a table per aura scanned.
|
||||
--
|
||||
-- The comparator / dispel-priority / border helpers (DefaultAuraCompare,
|
||||
-- UnitFrameDebuffComparator, ShouldDisplayDebuff, SetAuraBorder*, IsPriorityDebuff)
|
||||
-- are NOT ported: they depend on isPriorityAura / auraInstanceID / the aura
|
||||
-- visualization pipeline / secure-call machinery, none of which exist on 1.12.
|
||||
|
||||
local UnitAura = C_UnitAuras.UnitAura
|
||||
local GetAuraSlots = C_UnitAuras.GetAuraSlots
|
||||
local GetAuraDataBySlot = C_UnitAuras.GetAuraDataBySlot
|
||||
local UnitAuraBySlot = C_UnitAuras.UnitAuraBySlot
|
||||
|
||||
AuraUtil = AuraUtil or {};
|
||||
|
||||
-- Slot-id buffers for GetAuraSlots' fill form, one per nesting level: a `func`
|
||||
-- or `predicate` that itself iterates another unit's auras gets its own buffer
|
||||
-- instead of clobbering the outer scan's. Costs one table per nesting level for
|
||||
-- the session (retail's varargs are re-entrant for free; ours must be explicit).
|
||||
-- An error thrown out of `func` skips the release and leaves `depth` one higher
|
||||
-- for the rest of the session -- harmless: later scans use the next buffer up,
|
||||
-- allocated once.
|
||||
local slotBufs = {};
|
||||
local depth = 0;
|
||||
|
||||
local function AcquireSlotBuf()
|
||||
depth = depth + 1;
|
||||
local buf = slotBufs[depth];
|
||||
if not buf then
|
||||
buf = {};
|
||||
slotBufs[depth] = buf;
|
||||
end
|
||||
return buf;
|
||||
end
|
||||
|
||||
local function ReleaseSlotBuf()
|
||||
depth = depth - 1;
|
||||
end
|
||||
|
||||
-- Returns the AuraData table's fields in the classic `UnitAura` positional order,
|
||||
-- for code written against that shape. Retail appends `unpack(auraData.points)`;
|
||||
-- vanilla auras carry no points, so the tuple ends at timeMod (15 values). Field
|
||||
@@ -58,66 +95,56 @@ function AuraUtil.UnpackAuraData(auraData)
|
||||
auraData.timeMod;
|
||||
end
|
||||
|
||||
-- Calls `func` for each aura on `unit` matching `filter`, in index order, until
|
||||
-- `func` returns a truthy value or the auras run out. Non-packed (default) passes
|
||||
-- the 15 positional `UnitAura` values (no table); `usePackedAura` passes the
|
||||
-- AuraData table from GetAuraDataByIndex. `batchSize <= 0` does nothing; any other
|
||||
-- value (or nil) visits every matching aura -- `batchSize` is retail's slot-batch
|
||||
-- hint, not a cap on how many auras are visited.
|
||||
-- Calls `func` for each aura on `unit` matching `filter`, in order, until `func`
|
||||
-- returns a truthy value or the auras run out. Non-packed (default) passes the 15
|
||||
-- positional `UnitAura` values (no table); `usePackedAura` passes the AuraData
|
||||
-- table. `batchSize` is the number of slots fetched per GetAuraSlots call (nil =
|
||||
-- all at once); it never caps how many auras are visited. `batchSize <= 0` does
|
||||
-- nothing.
|
||||
function AuraUtil.ForEachAura(unit, filter, batchSize, func, usePackedAura)
|
||||
if batchSize and batchSize <= 0 then
|
||||
return;
|
||||
end
|
||||
local index = 1;
|
||||
while true do
|
||||
local done;
|
||||
if usePackedAura then
|
||||
local auraData = C_UnitAuras.GetAuraDataByIndex(unit, index, filter);
|
||||
if auraData == nil then
|
||||
local buf = AcquireSlotBuf();
|
||||
local continuationToken, n;
|
||||
repeat
|
||||
continuationToken, n = GetAuraSlots(unit, filter, batchSize, continuationToken, buf);
|
||||
for i = 1, n do
|
||||
local done;
|
||||
if usePackedAura then
|
||||
done = func(GetAuraDataBySlot(unit, buf[i]));
|
||||
else
|
||||
done = func(UnitAuraBySlot(unit, buf[i]));
|
||||
end
|
||||
if done then
|
||||
ReleaseSlotBuf();
|
||||
return;
|
||||
end
|
||||
done = func(auraData);
|
||||
else
|
||||
local name, icon, count, dispelType, duration, expirationTime, source,
|
||||
isStealable, nameplateShowPersonal, spellId, canApplyAura,
|
||||
isBossDebuff, castByPlayer, nameplateShowAll, timeMod =
|
||||
UnitAura(unit, index, filter);
|
||||
if name == nil then
|
||||
return;
|
||||
end
|
||||
done = func(name, icon, count, dispelType, duration, expirationTime,
|
||||
source, isStealable, nameplateShowPersonal, spellId, canApplyAura,
|
||||
isBossDebuff, castByPlayer, nameplateShowAll, timeMod);
|
||||
end
|
||||
if done then
|
||||
return;
|
||||
end
|
||||
index = index + 1;
|
||||
end
|
||||
end
|
||||
|
||||
-- Recursive worker: `...` is the current aura's positional values (nil when the
|
||||
-- index is past the last aura). Tail-recurses to the next index. Lua 5.0 has
|
||||
-- proper tail calls, so depth is constant regardless of aura count.
|
||||
local function FindAuraRecurse(predicate, unit, filter, index, arg1, arg2, arg3, ...)
|
||||
if ... == nil then
|
||||
return nil; -- past the last aura -> not found
|
||||
end
|
||||
if predicate(arg1, arg2, arg3, ...) then
|
||||
return ...;
|
||||
end
|
||||
index = index + 1;
|
||||
return FindAuraRecurse(predicate, unit, filter, index, arg1, arg2, arg3,
|
||||
UnitAura(unit, index, filter));
|
||||
until continuationToken == nil;
|
||||
ReleaseSlotBuf();
|
||||
end
|
||||
|
||||
-- Returns the positional values of the first aura for which `predicate` returns
|
||||
-- truthy, or nil. `predicate` receives up to three caller args followed by the
|
||||
-- aura's positional values: predicate(arg1, arg2, arg3, name, icon, ...). Sources
|
||||
-- auras from the positional accessor, so no table is allocated while scanning.
|
||||
-- auras from the positional by-slot accessor, so no table is allocated while
|
||||
-- scanning.
|
||||
function AuraUtil.FindAura(predicate, unit, filter, arg1, arg2, arg3)
|
||||
return FindAuraRecurse(predicate, unit, filter, 1, arg1, arg2, arg3,
|
||||
UnitAura(unit, 1, filter));
|
||||
local buf = AcquireSlotBuf();
|
||||
local continuationToken, n;
|
||||
repeat
|
||||
continuationToken, n = GetAuraSlots(unit, filter, nil, continuationToken, buf);
|
||||
for i = 1, n do
|
||||
local slot = buf[i];
|
||||
if predicate(arg1, arg2, arg3, UnitAuraBySlot(unit, slot)) then
|
||||
ReleaseSlotBuf();
|
||||
return UnitAuraBySlot(unit, slot);
|
||||
end
|
||||
end
|
||||
until continuationToken == nil;
|
||||
ReleaseSlotBuf();
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- Returns the positional values of the first aura whose name matches `auraName`,
|
||||
|
||||
@@ -81,18 +81,89 @@ function ColorMixin:WrapTextInColorTableCode(text)
|
||||
return WrapTextInColorCode(text, self:GenerateHexColor())
|
||||
end
|
||||
|
||||
for _, v in pairs(ITEM_QUALITY_COLORS) do
|
||||
v.color = CreateColor(v.r, v.g, v.b, 1)
|
||||
-- Publishes the standard color globals (NORMAL_FONT_COLOR and friends) as
|
||||
-- ColorMixin objects, the way Blizzard_SharedXMLBase/Color.lua does.
|
||||
--
|
||||
-- Runs at load and again after every addon loads (see Util/AddOnCompat.lua).
|
||||
-- An addon that carries a copy of Blizzard's own definition -- a plain
|
||||
-- `NORMAL_FONT_COLOR = {r=1.0, g=0.82, b=0}` -- assigns it over ours at file
|
||||
-- scope, and every addon that loads after it then finds a color with no
|
||||
-- methods. A later pass mixes ColorMixin INTO the table that is there instead
|
||||
-- of replacing it, so the values that addon chose stay, and each reference it
|
||||
-- gave out gets the methods too.
|
||||
--
|
||||
-- Only a color that is absent or has lost its methods is touched. A tag that
|
||||
-- holds something other than a color table belongs to some other addon, so it
|
||||
-- is left alone. A `<TAG>_CODE` string that is already set is never rewritten:
|
||||
-- FrameXML writes those together with the values they go with.
|
||||
local standardColors
|
||||
|
||||
-- True for a table that carries r/g/b numbers.
|
||||
local function HasRGB(value)
|
||||
return type(value) == "table"
|
||||
and type(value.r) == "number"
|
||||
and type(value.g) == "number"
|
||||
and type(value.b) == "number"
|
||||
end
|
||||
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
local envTbl = _G
|
||||
for _, dbColor in ipairs(C_UIColor.GetColors()) do
|
||||
if not envTbl[dbColor.baseTag] or not envTbl[dbColor.baseTag].GetRGB then
|
||||
local color = CreateColor(dbColor.color.r, dbColor.color.g, dbColor.color.b, dbColor.color.a)
|
||||
envTbl[dbColor.baseTag] = color
|
||||
envTbl[dbColor.baseTag.."_CODE"] = color:GenerateHexColorMarkup()
|
||||
-- True for a color table that still needs the mixin.
|
||||
local function IsPlainColorTable(value)
|
||||
return HasRGB(value) and not value.GetRGB
|
||||
end
|
||||
|
||||
-- ITEM_QUALITY_COLORS is the same story with one more level: FrameXML builds
|
||||
-- each entry as a plain {r, g, b, hex} table, and the `color` object on it is
|
||||
-- ours. An addon that carries its own copy of the table takes those objects
|
||||
-- with it, so repair the entries on the same pass. Keys run from -1, so this
|
||||
-- walks the table with pairs.
|
||||
local function ApplyItemQualityColors()
|
||||
if type(ITEM_QUALITY_COLORS) ~= "table" then return end
|
||||
|
||||
for _, quality in pairs(ITEM_QUALITY_COLORS) do
|
||||
if type(quality) == "table" then
|
||||
if IsPlainColorTable(quality.color) then
|
||||
quality.color = Mixin(quality.color, ColorMixin)
|
||||
quality.color.a = quality.color.a or 1
|
||||
elseif quality.color == nil and HasRGB(quality) then
|
||||
quality.color = CreateColor(quality.r, quality.g, quality.b, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
-- `C_UIColor.GetColors()` makes a color object for each of its 187 rows,
|
||||
-- so keep the values from the first call. Each later pass is then one
|
||||
-- lookup per tag.
|
||||
if not standardColors then
|
||||
standardColors = {}
|
||||
for _, dbColor in ipairs(C_UIColor.GetColors()) do
|
||||
table.insert(standardColors, {
|
||||
tag = dbColor.baseTag,
|
||||
r = dbColor.color.r,
|
||||
g = dbColor.color.g,
|
||||
b = dbColor.color.b,
|
||||
a = dbColor.color.a,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
for _, entry in ipairs(standardColors) do
|
||||
local current = _G[entry.tag]
|
||||
local color
|
||||
if current == nil then
|
||||
color = CreateColor(entry.r, entry.g, entry.b, entry.a)
|
||||
_G[entry.tag] = color
|
||||
elseif IsPlainColorTable(current) then
|
||||
color = Mixin(current, ColorMixin)
|
||||
color.a = color.a or 1
|
||||
end
|
||||
|
||||
if color and _G[entry.tag.."_CODE"] == nil then
|
||||
_G[entry.tag.."_CODE"] = color:GenerateHexColorMarkup()
|
||||
end
|
||||
end
|
||||
|
||||
ApplyItemQualityColors()
|
||||
end
|
||||
CAPI_ApplyStandardColorGlobals()
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
-- Only the dependency-light, commonly-called helpers are ported. Omitted:
|
||||
-- - the fade/flash functions (UIFrameFade*/UIFrameFlash*) are already
|
||||
-- native in 1.12.
|
||||
-- - RegisterFrameForUnitEvents (no RegisterUnitEvent in 1.12), and the
|
||||
-- scale/layout/secure/EventRegistry helpers (rely on APIs absent here).
|
||||
-- - the scale/layout/secure/EventRegistry helpers (rely on APIs absent
|
||||
-- here).
|
||||
|
||||
FrameUtil = FrameUtil or {};
|
||||
|
||||
@@ -14,6 +14,12 @@ function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForUnitEvents(frame, events, ...)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterUnitEvent(event, unpack(arg));
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.UnregisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- MacroIconSelection.lua -- the Macro UI's macro list shows the icon a
|
||||
-- `#showtooltip` resolved to, the way the action button does.
|
||||
--
|
||||
-- `GetMacroInfo` reports the icon the macro STORES, and it has to keep
|
||||
-- reporting that one: `MacroFrame_Update` seeds the icon selector from that
|
||||
-- return, so answering with a resolved icon there would pre-select an icon the
|
||||
-- player never picked, in the one place that edits the stored icon.
|
||||
--
|
||||
-- That leaves the list buttons showing the question mark for a macro whose
|
||||
-- action button shows a spell. `C_Macro.GetMacroIcon` returns the icon a macro
|
||||
-- currently shows, so paint the buttons from it once the stock update has
|
||||
-- drawn them. An empty slot and a macro with a chosen icon both answer with
|
||||
-- what stock already drew, so those are left alone.
|
||||
|
||||
local function ApplyIcon(texture, macroSlot)
|
||||
if not texture then
|
||||
return
|
||||
end
|
||||
local shown = C_Macro.GetMacroIcon(macroSlot)
|
||||
if shown then
|
||||
texture:SetTexture(shown)
|
||||
end
|
||||
end
|
||||
|
||||
local function PaintMacroList()
|
||||
if not (MacroFrame and MacroFrame.macroBase) then
|
||||
return
|
||||
end
|
||||
for i = 1, MAX_MACROS do
|
||||
ApplyIcon(_G["MacroButton" .. i .. "Icon"], MacroFrame.macroBase + i)
|
||||
end
|
||||
if MacroFrame.selectedMacro then
|
||||
ApplyIcon(MacroFrameSelectedMacroButtonIcon, MacroFrame.selectedMacro)
|
||||
end
|
||||
end
|
||||
|
||||
local function WrapMacroFrameUpdate()
|
||||
if not MacroFrame_Update then
|
||||
return
|
||||
end
|
||||
|
||||
local original = MacroFrame_Update
|
||||
-- No `...`: `MacroFrame_Update` takes no arguments, and Lua 5.0 would
|
||||
-- build an `arg` table for every vararg call.
|
||||
MacroFrame_Update = function()
|
||||
original()
|
||||
PaintMacroList()
|
||||
end
|
||||
end
|
||||
|
||||
-- Blizzard_MacroUI loads on demand.
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", WrapMacroFrameUpdate)
|
||||
@@ -346,6 +346,14 @@ CONDITIONS.worn = CONDITIONS.equipped;
|
||||
|
||||
local warned = {};
|
||||
|
||||
-- Keywords met during the current `ParseOptions` run that this parser does
|
||||
-- not own. A condition we cannot evaluate means the options were written for
|
||||
-- another macro parser's dialect, so the fact is recorded on the parsed
|
||||
-- clause set rather than reported here. The caller decides what to do with
|
||||
-- it: a command the player ran names the keyword, and the macro-display scan
|
||||
-- leaves the macro alone.
|
||||
local parseUnknowns;
|
||||
|
||||
-- piece -> { pred = fn, neg = bool, args = { n, [i] } or nil }
|
||||
local function ParseCondition(piece)
|
||||
local neg = false;
|
||||
@@ -368,10 +376,11 @@ local function ParseCondition(piece)
|
||||
|
||||
local pred = CONDITIONS[keyword];
|
||||
if not pred then
|
||||
if not warned[keyword] then
|
||||
warned[keyword] = true;
|
||||
print("SecureCmdOptionParse: unknown condition '" .. keyword .. "'");
|
||||
if not parseUnknowns then
|
||||
parseUnknowns = { n = 0 };
|
||||
end
|
||||
parseUnknowns.n = parseUnknowns.n + 1;
|
||||
parseUnknowns[parseUnknowns.n] = keyword;
|
||||
pred = AlwaysFalse;
|
||||
neg = false; -- an unknown keyword is a hard false
|
||||
end
|
||||
@@ -413,8 +422,10 @@ local function ParseClause(text)
|
||||
return { value = Trim(value), groups = groups };
|
||||
end
|
||||
|
||||
-- options -> { n, [i] = clause }. Splits on ';' at bracket depth 0.
|
||||
-- options -> { n, [i] = clause, unknown = { n, [i] = keyword } or nil }.
|
||||
-- Splits on ';' at bracket depth 0.
|
||||
local function ParseOptions(options)
|
||||
parseUnknowns = nil;
|
||||
local clauses = { n = 0 };
|
||||
local len = strlen(options);
|
||||
local pos = 1;
|
||||
@@ -436,6 +447,8 @@ local function ParseOptions(options)
|
||||
clauses[clauses.n] = ParseClause(strsub(options, pos, i - 1));
|
||||
pos = i + 1;
|
||||
end
|
||||
clauses.unknown = parseUnknowns;
|
||||
parseUnknowns = nil;
|
||||
return clauses;
|
||||
end
|
||||
|
||||
@@ -445,8 +458,80 @@ end
|
||||
|
||||
local cache = {};
|
||||
|
||||
-- `@unit` takes a character name as well as a token (`[target=Feral]`), and a
|
||||
-- name has to be searched for in the object manager. That search is the one
|
||||
-- expensive step here, so a pass remembers what it already found. The table
|
||||
-- is only built once a pass meets a name, and the next pass drops it: a token
|
||||
-- kept across passes could name a different unit by then.
|
||||
local passNames;
|
||||
|
||||
local function TokenForName(name)
|
||||
if passNames then
|
||||
local cached = passNames[name];
|
||||
if cached ~= nil then
|
||||
return cached or nil; -- false records "nobody there"
|
||||
end
|
||||
else
|
||||
passNames = {};
|
||||
end
|
||||
local token = UnitTokenFromName(name);
|
||||
passNames[name] = token or false;
|
||||
return token;
|
||||
end
|
||||
|
||||
-- A `@unit` piece as something the engine's unit functions accept, with no
|
||||
-- question asked about whether a unit is there. `IsUnitToken` tells a token
|
||||
-- from a name by asking the engine's own resolver, and it does not go looking
|
||||
-- for the unit -- which is what the conditions do next anyway.
|
||||
local function TokenForTarget(target)
|
||||
if IsUnitToken(target) then
|
||||
return target;
|
||||
end
|
||||
return TokenForName(target);
|
||||
end
|
||||
|
||||
-- Whether a `@unit` piece has a unit behind it right now. The pcall tells a
|
||||
-- token from a name in one step: the engine raises for a string that names no
|
||||
-- token, which is exactly the case to hand to the by-name search. That also
|
||||
-- keeps a typo failing the group instead of the caller.
|
||||
local function TargetExists(target)
|
||||
local ok, exists = pcall(UnitExists, target);
|
||||
if ok then
|
||||
return exists and true or false;
|
||||
end
|
||||
local token = TokenForName(target);
|
||||
return token ~= nil and UnitExists(token) and true or false;
|
||||
end
|
||||
|
||||
-- Targets that do not name a unit. `none` clears the target and `cursor`
|
||||
-- names a world position, so neither can be tested with UnitExists and both
|
||||
-- always pass the existence gate below.
|
||||
local NON_UNIT_TARGETS = { none = true, cursor = true };
|
||||
|
||||
local function GroupPasses(group)
|
||||
local target = group.target or "target";
|
||||
local target = group.target;
|
||||
local named = target and not NON_UNIT_TARGETS[strlower(target)];
|
||||
if group.n == 0 then
|
||||
-- A group that only names a unit (`[@mouseover]`) passes only while
|
||||
-- that unit exists, so `[@mouseover][] Spell` falls through to the
|
||||
-- next group when nothing is moused over. A bare `[]` and the
|
||||
-- non-unit targets always pass. Groups with conditions leave
|
||||
-- existence to them (`[@focus,noexists]` still works).
|
||||
if not named then
|
||||
return true;
|
||||
end
|
||||
return TargetExists(target);
|
||||
end
|
||||
if named then
|
||||
-- The conditions ask the engine about this unit, so a name has to
|
||||
-- become a token first. Existence is theirs to test, so it is not
|
||||
-- asked for here.
|
||||
target = TokenForTarget(target);
|
||||
if not target then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
target = target or "target";
|
||||
for i = 1, group.n do
|
||||
local cond = group[i];
|
||||
local ok = cond.pred(target, cond.args);
|
||||
@@ -460,29 +545,56 @@ local function GroupPasses(group)
|
||||
return true;
|
||||
end
|
||||
|
||||
function SecureCmdOptionParse(options)
|
||||
-- Returns the matched value, the passing group's target, and the name of the
|
||||
-- first condition the options use that this parser does not own.
|
||||
--
|
||||
-- That third value marks the options as another macro parser's dialect. A
|
||||
-- caller that scans macro bodies it did not write passes `quiet` and reads it,
|
||||
-- so it can leave such a macro alone instead of naming a keyword the player
|
||||
-- has no reason to fix.
|
||||
function SecureCmdOptionParse(options, quiet)
|
||||
if type(options) ~= "string" then
|
||||
error("Usage: SecureCmdOptionParse(\"options\")");
|
||||
return nil; -- WoW's error() may not unwind
|
||||
end
|
||||
|
||||
-- Start of a pass: drop what the last one resolved by name. Nothing is
|
||||
-- allocated unless a name was actually met, so the all-token case (the
|
||||
-- common one, polled several times a second) costs nothing here.
|
||||
if passNames then
|
||||
passNames = nil;
|
||||
end
|
||||
|
||||
local clauses = cache[options];
|
||||
if not clauses then
|
||||
clauses = ParseOptions(options);
|
||||
cache[options] = clauses;
|
||||
end
|
||||
|
||||
-- Name each keyword once, for a caller that wants to hear about it.
|
||||
local unknown = clauses.unknown;
|
||||
if unknown and not quiet then
|
||||
for i = 1, unknown.n do
|
||||
local keyword = unknown[i];
|
||||
if not warned[keyword] then
|
||||
warned[keyword] = true;
|
||||
print("SecureCmdOptionParse: unknown condition '" .. keyword .. "'");
|
||||
end
|
||||
end
|
||||
end
|
||||
local foreign = unknown and unknown[1] or nil;
|
||||
|
||||
for i = 1, clauses.n do
|
||||
local clause = clauses[i];
|
||||
local groups = clause.groups;
|
||||
if groups.n == 0 then
|
||||
return clause.value;
|
||||
return clause.value, nil, foreign;
|
||||
end
|
||||
for j = 1, groups.n do
|
||||
if GroupPasses(groups[j]) then
|
||||
return clause.value, groups[j].target;
|
||||
return clause.value, groups[j].target, foreign;
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil;
|
||||
return nil, nil, foreign;
|
||||
end
|
||||
|
||||
@@ -6,6 +6,179 @@ function RegisterNewSlashCommand(callback, command, commandAlias)
|
||||
SlashCmdList[name] = callback;
|
||||
end
|
||||
|
||||
-- Keys that exist before this file runs, so the block at the bottom can tell
|
||||
-- the commands we ADD from the ones we replace.
|
||||
local preexistingKeys = {};
|
||||
for key in pairs(SlashCmdList) do
|
||||
preexistingKeys[key] = true;
|
||||
end
|
||||
|
||||
-- `/cast` and `/use` (the FrameXML ChatFrame.lua shape): conditionals through
|
||||
-- SecureCmdOptionParse, items before spells, `[@unit]` cast targets.
|
||||
-- A bare number is an inventory slot only when it is one (1..19); any other
|
||||
-- number is a spellID (`/cast 5019`), which the spell resolver already
|
||||
-- accepts anywhere a spell name goes.
|
||||
local INVSLOT_FIRST, INVSLOT_LAST = 1, 19;
|
||||
|
||||
function SecureCmdItemParse(item)
|
||||
if ( not item ) then
|
||||
return nil, nil, nil;
|
||||
end
|
||||
local bag, slot = string.match(item, "^(%d+)%s+(%d+)$");
|
||||
if ( not bag ) then
|
||||
slot = string.match(item, "^(%d+)$");
|
||||
if ( slot ) then
|
||||
local n = tonumber(slot);
|
||||
if ( n < INVSLOT_FIRST or n > INVSLOT_LAST ) then
|
||||
slot = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
if ( bag ) then
|
||||
item = GetContainerItemLink(bag, slot);
|
||||
elseif ( slot ) then
|
||||
item = GetInventoryItemLink("player", slot);
|
||||
end
|
||||
return item, bag, slot;
|
||||
end
|
||||
|
||||
-- A number with no bag or inventory slot behind it. `/cast 5019` names a
|
||||
-- spell, so the item lookup must never see it: an item reference reads a
|
||||
-- number as an itemID, and a carried item with that ID would be used in
|
||||
-- place of the spell.
|
||||
local function IsBareNumber(text)
|
||||
return string.match(text, "^%d+$") ~= nil;
|
||||
end
|
||||
|
||||
-- Targets that name no unit. `@cursor` names the position under the mouse,
|
||||
-- which a ground-target spell or item is placed at; `@none` asks for no unit
|
||||
-- at all, so it becomes the untargeted form of whatever verb ran.
|
||||
local NON_UNIT_TARGETS = { none = true, cursor = true };
|
||||
|
||||
-- `@unit` and `target=unit` accept a character name as well as a unit token
|
||||
-- (`[target=Feral]`), and the engine's unit functions take only tokens.
|
||||
-- Resolve once here so every command below hands them something they accept.
|
||||
-- Returns nil for a name with nobody around to match it.
|
||||
local function SecureCmdTargetUnit(target)
|
||||
if ( not target or target == "" or IsUnitToken(target) ) then
|
||||
return target;
|
||||
end
|
||||
-- Keep the non-unit targets out of the by-name search, which matches on
|
||||
-- the START of a name: `@none` would happily find a nearby Nonek.
|
||||
if ( NON_UNIT_TARGETS[strlower(target)] ) then
|
||||
return nil;
|
||||
end
|
||||
return UnitTokenFromName(target);
|
||||
end
|
||||
|
||||
-- One target normalization for every verb below: an empty target and `@none`
|
||||
-- become nil (no unit), a non-unit target keeps its own name, and anything
|
||||
-- else has to resolve to a token. Returns `false` for a name with nobody
|
||||
-- around to match it, which the callers treat as "do nothing".
|
||||
local function SecureCmdNormalizeTarget(target)
|
||||
if ( not target or target == "" ) then
|
||||
return nil;
|
||||
end
|
||||
local lower = strlower(target);
|
||||
if ( lower == "none" ) then
|
||||
return nil;
|
||||
end
|
||||
if ( NON_UNIT_TARGETS[lower] ) then
|
||||
return lower;
|
||||
end
|
||||
return SecureCmdTargetUnit(target) or false;
|
||||
end
|
||||
|
||||
-- The item a `/use` line names, in the form the three calls below take. An
|
||||
-- explicit bag or inventory slot becomes a location, which names ONE item:
|
||||
-- looking the item up by name again would take the first match anywhere, and
|
||||
-- with two stacks of the same thing that is the wrong one.
|
||||
local function SecureCmdItemLocation(name, bag, slot)
|
||||
if ( bag ) then
|
||||
return { bagID = tonumber(bag), slotIndex = tonumber(slot) };
|
||||
end
|
||||
if ( slot ) then
|
||||
return { equipmentSlotIndex = tonumber(slot) };
|
||||
end
|
||||
return name;
|
||||
end
|
||||
|
||||
function SecureCmdUseItem(name, bag, slot, target)
|
||||
local item = SecureCmdItemLocation(name, bag, slot);
|
||||
if ( not item ) then
|
||||
return;
|
||||
end
|
||||
if ( target == "cursor" ) then
|
||||
-- `@cursor` names a world position, so a ground-target item places
|
||||
-- its effect there. An item with no ground effect is used normally.
|
||||
C_Item.UseAtCursor(item);
|
||||
elseif ( target == "player" ) then
|
||||
-- `@player` drops a ground-target item at your own feet. Only your
|
||||
-- own position is offered this way; aiming one at another unit is
|
||||
-- not something you can do by hand either. An item with no ground
|
||||
-- effect is used on you, as before.
|
||||
C_Item.UseAtUnit(item, "player");
|
||||
else
|
||||
-- Every form goes through one call, so a slot is aimed at `@unit`
|
||||
-- just like a name is. `/use` also always means use: clicking a bag
|
||||
-- slot sells the item at a merchant and repairs it under the repair
|
||||
-- cursor, which a typed command should never do.
|
||||
C_Item.UseItemByName(item, target);
|
||||
end
|
||||
end
|
||||
|
||||
local function SecureCmdCast(msg)
|
||||
local action, target = SecureCmdOptionParse(msg);
|
||||
if ( not action or action == "" ) then
|
||||
return;
|
||||
end
|
||||
-- `!Name` asks for the spell to be started but never turned off, for the
|
||||
-- abilities that toggle: auto-repeat (Shoot, Auto Shot) and the self-auras
|
||||
-- (stances, aspects, seals, forms, tracking). Strip the prefix here and
|
||||
-- cast through `CastSpellNoToggle`, which asks the engine whether the
|
||||
-- ability is already up before it casts.
|
||||
local noToggle;
|
||||
if ( string.sub(action, 1, 1) == "!" ) then
|
||||
noToggle = true;
|
||||
action = string.sub(action, 2);
|
||||
if ( action == "" ) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
target = SecureCmdNormalizeTarget(target);
|
||||
if ( target == false ) then
|
||||
return; -- a named unit with nobody around to match it
|
||||
end
|
||||
local name, bag, slot = SecureCmdItemParse(action);
|
||||
if ( slot or (name and not IsBareNumber(action) and C_Item.GetItemCount(name) > 0) ) then
|
||||
SecureCmdUseItem(name, bag, slot, target);
|
||||
elseif ( target == "cursor" ) then
|
||||
C_Spell.CastAtCursor(action);
|
||||
elseif ( not target or target == "target" ) then
|
||||
if ( noToggle ) then
|
||||
CastSpellNoToggle(action);
|
||||
else
|
||||
CastSpellByName(action);
|
||||
end
|
||||
elseif ( noToggle ) then
|
||||
-- Same unit rules as below: your own feet take a ground-target
|
||||
-- spell, another unit does not.
|
||||
CastSpellNoToggle(action, target, target == "player");
|
||||
elseif ( target == "player" ) then
|
||||
-- A ground-target spell lands at your own feet; a normal one is
|
||||
-- cast on you.
|
||||
C_Spell.CastAtUnit(action, target, true);
|
||||
else
|
||||
-- Any other unit has the spell cast on it, but a ground-target one
|
||||
-- is not placed there: that is aim you do not have by hand. The
|
||||
-- reticle comes up as usual.
|
||||
C_Spell.CastAtUnit(action, target, false);
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["CAST"] = SecureCmdCast;
|
||||
SlashCmdList["USE"] = SecureCmdCast;
|
||||
|
||||
SlashCmdList["FOCUS"] = function(msg)
|
||||
if ( msg == "" ) then
|
||||
FocusUnit();
|
||||
@@ -15,7 +188,10 @@ SlashCmdList["FOCUS"] = function(msg)
|
||||
if ( not target or target == "focus" ) then
|
||||
target = action;
|
||||
end
|
||||
FocusUnit(target);
|
||||
target = SecureCmdTargetUnit(target);
|
||||
if ( target ) then
|
||||
FocusUnit(target);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -26,6 +202,25 @@ SlashCmdList["CLEARFOCUS"] = function(msg)
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["STARTATTACK"] = function(msg)
|
||||
local action, target = SecureCmdOptionParse(msg);
|
||||
if ( action ) then
|
||||
if ( not target or target == "target" ) then
|
||||
target = action;
|
||||
end
|
||||
target = SecureCmdTargetUnit(target);
|
||||
if ( target ) then
|
||||
StartAttack(target);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["STOPATTACK"] = function(msg)
|
||||
if ( SecureCmdOptionParse(msg) ) then
|
||||
StopAttack();
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["EQUIP_SET"] = function(msg)
|
||||
local set = SecureCmdOptionParse(msg);
|
||||
if ( set and set ~= "" ) then
|
||||
@@ -46,3 +241,403 @@ SlashCmdList["CLICK"] = function(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Runs the conditions and calls `fn()` when a clause matched. The shape of
|
||||
-- every command whose only argument is its conditions.
|
||||
local function SecureCmdGated(msg, fn)
|
||||
if ( SecureCmdOptionParse(msg) ) then
|
||||
fn();
|
||||
end
|
||||
end
|
||||
|
||||
-- Strips a trailing "(Rank N)" so a rank-qualified name still matches.
|
||||
local function PlainSpellName(spell)
|
||||
local plain = string.match(spell, "^([^(]+)%(");
|
||||
if ( plain ) then
|
||||
return strtrim(plain);
|
||||
end
|
||||
return spell;
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Targeting
|
||||
--
|
||||
-- Each verb has one entry that takes a unit token and another that takes a
|
||||
-- character name, and each errors on the other kind. `SecureCmdOptionParse`
|
||||
-- hands back either, so pick with `IsUnitToken`.
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
local function SecureCmdTarget(msg, exactMatch)
|
||||
local action, target = SecureCmdOptionParse(msg);
|
||||
if ( not action ) then
|
||||
return;
|
||||
end
|
||||
if ( target ) then
|
||||
local lower = strlower(target);
|
||||
if ( lower == "none" ) then
|
||||
ClearTarget(); -- `@none` clears the target
|
||||
return;
|
||||
end
|
||||
if ( NON_UNIT_TARGETS[lower] ) then
|
||||
return; -- `@cursor` names no unit to target
|
||||
end
|
||||
end
|
||||
if ( not target or target == "target" ) then
|
||||
target = action;
|
||||
end
|
||||
if ( not target or target == "" ) then
|
||||
return;
|
||||
end
|
||||
if ( IsUnitToken(target) ) then
|
||||
TargetUnit(target);
|
||||
else
|
||||
TargetByName(target, exactMatch);
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET"] = function(msg)
|
||||
SecureCmdTarget(msg);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_EXACT"] = function(msg)
|
||||
SecureCmdTarget(msg, 1);
|
||||
end
|
||||
|
||||
SlashCmdList["ASSIST"] = function(msg)
|
||||
if ( msg == "" ) then
|
||||
AssistUnit("target");
|
||||
return;
|
||||
end
|
||||
local action, target = SecureCmdOptionParse(msg);
|
||||
if ( not action ) then
|
||||
return;
|
||||
end
|
||||
if ( target and NON_UNIT_TARGETS[strlower(target)] ) then
|
||||
return; -- `@none` and `@cursor` name no unit to assist
|
||||
end
|
||||
-- An explicit `@target` leaves the trailing name in charge, the same way
|
||||
-- `/target` and `/follow` read it.
|
||||
if ( not target or target == "target" ) then
|
||||
target = action;
|
||||
end
|
||||
if ( target == "" ) then
|
||||
AssistUnit("target");
|
||||
elseif ( IsUnitToken(target) ) then
|
||||
AssistUnit(target);
|
||||
else
|
||||
AssistByName(target);
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["FOLLOW"] = function(msg)
|
||||
if ( msg == "" ) then
|
||||
FollowUnit("target");
|
||||
return;
|
||||
end
|
||||
local action, target = SecureCmdOptionParse(msg);
|
||||
if ( not action ) then
|
||||
return;
|
||||
end
|
||||
if ( target and NON_UNIT_TARGETS[strlower(target)] ) then
|
||||
return; -- `@none` and `@cursor` name no unit to follow
|
||||
end
|
||||
if ( not target or target == "target" ) then
|
||||
target = action;
|
||||
end
|
||||
if ( target == "" ) then
|
||||
FollowUnit("target");
|
||||
elseif ( IsUnitToken(target) ) then
|
||||
FollowUnit(target);
|
||||
else
|
||||
FollowByName(target);
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["CLEARTARGET"] = function(msg)
|
||||
SecureCmdGated(msg, ClearTarget);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_LAST_TARGET"] = function(msg)
|
||||
SecureCmdGated(msg, TargetLastTarget);
|
||||
end
|
||||
|
||||
-- The cycling selectors take the parsed value as their "reverse" flag, so
|
||||
-- `/targetenemy [mod:shift] 1` steps backwards while shift is held. A clause
|
||||
-- that matched with no value has to arrive as nil: an empty string is a true
|
||||
-- boolean in Lua, which would make every bare `/targetenemy` step backwards.
|
||||
local function SecureCmdTargetCycle(msg, fn)
|
||||
local action = SecureCmdOptionParse(msg);
|
||||
if ( action ) then
|
||||
fn(action ~= "" and action or nil);
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_LAST_ENEMY"] = function(msg)
|
||||
SecureCmdTargetCycle(msg, TargetLastEnemy);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_NEAREST_ENEMY"] = function(msg)
|
||||
SecureCmdTargetCycle(msg, TargetNearestEnemy);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_NEAREST_ENEMY_PLAYER"] = function(msg)
|
||||
SecureCmdTargetCycle(msg, TargetNearestEnemyPlayer);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_NEAREST_FRIEND"] = function(msg)
|
||||
SecureCmdTargetCycle(msg, TargetNearestFriend);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_NEAREST_FRIEND_PLAYER"] = function(msg)
|
||||
SecureCmdTargetCycle(msg, TargetNearestFriendPlayer);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_NEAREST_PARTY"] = function(msg)
|
||||
SecureCmdTargetCycle(msg, TargetNearestPartyMember);
|
||||
end
|
||||
|
||||
SlashCmdList["TARGET_NEAREST_RAID"] = function(msg)
|
||||
SecureCmdTargetCycle(msg, TargetNearestRaidMember);
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Casting and player state
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
SlashCmdList["STOPCASTING"] = function(msg)
|
||||
SecureCmdGated(msg, SpellStopCasting);
|
||||
end
|
||||
|
||||
SlashCmdList["CANCELFORM"] = function(msg)
|
||||
SecureCmdGated(msg, CancelShapeshiftForm);
|
||||
end
|
||||
|
||||
SlashCmdList["DISMOUNT"] = function(msg)
|
||||
SecureCmdGated(msg, Dismount);
|
||||
end
|
||||
|
||||
SlashCmdList["CANCELAURA"] = function(msg)
|
||||
local spell = SecureCmdOptionParse(msg);
|
||||
if ( spell and spell ~= "" ) then
|
||||
CancelSpellByName(PlainSpellName(spell));
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Equipment
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
SlashCmdList["EQUIP"] = function(msg)
|
||||
local action = SecureCmdOptionParse(msg);
|
||||
if ( action and action ~= "" ) then
|
||||
local item = SecureCmdItemParse(action);
|
||||
if ( item ) then
|
||||
C_Item.EquipItemByName(item);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["EQUIP_TO_SLOT"] = function(msg)
|
||||
local action = SecureCmdOptionParse(msg);
|
||||
if ( not action ) then
|
||||
return;
|
||||
end
|
||||
local slot, name = string.match(action, "^(%d+)%s+(.+)");
|
||||
slot = tonumber(slot);
|
||||
if ( not slot or slot < INVSLOT_FIRST or slot > INVSLOT_LAST ) then
|
||||
return;
|
||||
end
|
||||
local item = SecureCmdItemParse(name);
|
||||
if ( item ) then
|
||||
C_Item.EquipItemByName(item, slot);
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Action bars
|
||||
--
|
||||
-- `ChangeActionBarPage` takes no argument here. It announces the page in
|
||||
-- `CURRENT_ACTIONBAR_PAGE`, so set that first.
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
local function SetActionBarPage(page)
|
||||
if ( page and page >= 1 and page <= NUM_ACTIONBAR_PAGES ) then
|
||||
CURRENT_ACTIONBAR_PAGE = page;
|
||||
ChangeActionBarPage();
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["CHANGEACTIONBAR"] = function(msg)
|
||||
local action = SecureCmdOptionParse(msg);
|
||||
if ( action and action ~= "" ) then
|
||||
SetActionBarPage(tonumber(action));
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["SWAPACTIONBAR"] = function(msg)
|
||||
local action = SecureCmdOptionParse(msg);
|
||||
if ( not action ) then
|
||||
return;
|
||||
end
|
||||
local a, b = string.match(action, "(%d+)%s+(%d+)");
|
||||
a, b = tonumber(a), tonumber(b);
|
||||
if ( a and b ) then
|
||||
if ( CURRENT_ACTIONBAR_PAGE == a ) then
|
||||
SetActionBarPage(b);
|
||||
else
|
||||
SetActionBarPage(a);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Pet
|
||||
--
|
||||
-- `PetAttack` takes no target here: it always sends the pet at the
|
||||
-- player's current target. Conditions still decide whether it fires, but
|
||||
-- `/petattack [@mouseover]` cannot aim at the mouseover.
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
SlashCmdList["PET_ATTACK"] = function(msg)
|
||||
SecureCmdGated(msg, PetAttack);
|
||||
end
|
||||
|
||||
SlashCmdList["PET_FOLLOW"] = function(msg)
|
||||
SecureCmdGated(msg, PetFollow);
|
||||
end
|
||||
|
||||
SlashCmdList["PET_STAY"] = function(msg)
|
||||
SecureCmdGated(msg, PetWait);
|
||||
end
|
||||
|
||||
SlashCmdList["PET_PASSIVE"] = function(msg)
|
||||
SecureCmdGated(msg, PetPassiveMode);
|
||||
end
|
||||
|
||||
SlashCmdList["PET_DEFENSIVE"] = function(msg)
|
||||
SecureCmdGated(msg, PetDefensiveMode);
|
||||
end
|
||||
|
||||
SlashCmdList["PET_AGGRESSIVE"] = function(msg)
|
||||
SecureCmdGated(msg, PetAggressiveMode);
|
||||
end
|
||||
|
||||
-- Autocast is a spellbook-slot toggle here, so a name has to be resolved
|
||||
-- against the pet book, and on/off read the current state and toggle only
|
||||
-- when it differs.
|
||||
local function PetSpellSlot(name)
|
||||
local numPetSpells = HasPetSpells();
|
||||
if ( not numPetSpells ) then
|
||||
return nil;
|
||||
end
|
||||
name = strlower(name);
|
||||
for i = 1, numPetSpells do
|
||||
local spell = GetSpellName(i, BOOKTYPE_PET);
|
||||
if ( spell and strlower(spell) == name ) then
|
||||
return i;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SecureCmdPetAutocast(msg, enable)
|
||||
local spell = SecureCmdOptionParse(msg);
|
||||
if ( not spell or spell == "" ) then
|
||||
return;
|
||||
end
|
||||
local slot = PetSpellSlot(PlainSpellName(spell));
|
||||
if ( not slot ) then
|
||||
return;
|
||||
end
|
||||
if ( enable == nil ) then
|
||||
ToggleSpellAutocast(slot, BOOKTYPE_PET);
|
||||
return;
|
||||
end
|
||||
local allowed, enabled = GetSpellAutocast(slot, BOOKTYPE_PET);
|
||||
if ( allowed and ((enabled and true or false) ~= enable) ) then
|
||||
ToggleSpellAutocast(slot, BOOKTYPE_PET);
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["PET_AUTOCASTON"] = function(msg)
|
||||
SecureCmdPetAutocast(msg, true);
|
||||
end
|
||||
|
||||
SlashCmdList["PET_AUTOCASTOFF"] = function(msg)
|
||||
SecureCmdPetAutocast(msg, false);
|
||||
end
|
||||
|
||||
SlashCmdList["PET_AUTOCASTTOGGLE"] = function(msg)
|
||||
SecureCmdPetAutocast(msg, nil);
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Give a contested command back to whoever already owned it
|
||||
--
|
||||
-- Vanilla has no `/petattack`, so the entry above ADDS it. A macro addon can
|
||||
-- ship the same command under its own SlashCmdList key -- SuperCleveRoidMacros
|
||||
-- registers `PETATTACK` where we register `PET_ATTACK`, both with a
|
||||
-- `SLASH_*` string of "/petattack". `ChatEdit_ParseText` walks SlashCmdList
|
||||
-- with `pairs` and takes the first match, so two keys claiming one command
|
||||
-- make the winner hash order: a coin flip. When ours won, it fed
|
||||
-- `[hastarget,alive,harm]` to SecureCmdOptionParse, which does not know that
|
||||
-- addon's conditions, so the line matched nothing and the macro did nothing.
|
||||
--
|
||||
-- Rejecting a condition we cannot evaluate is right on its own -- retail does
|
||||
-- the same -- so the error is owning the command at all. Hand it back. One
|
||||
-- command split across two condition dialects would be worse than one owner,
|
||||
-- because the dialects disagree: our `[@unit]`-only group passes only while
|
||||
-- that unit exists, where 3.3.5's passes unconditionally.
|
||||
--
|
||||
-- Only commands we ADD are given up. A command we replace (`/cast`, `/target`)
|
||||
-- has no second entry to lose to, and an addon that wraps ours afterwards
|
||||
-- chains through us as it always did.
|
||||
--
|
||||
-- Resolved a frame later, not at file scope: this addon loads first by design,
|
||||
-- so while it runs the other addon has not registered yet and there is nothing
|
||||
-- to detect. It cannot wait on PLAYER_LOGIN either -- that has already fired
|
||||
-- on a `/reload`, so `ContinueOnPlayerLogin` would run the check immediately
|
||||
-- and see the same empty table. The addon load pass is synchronous, so the
|
||||
-- next frame is after every non-demand addon has registered, on a cold login
|
||||
-- and on a reload alike. An addon that loads on demand later keeps its own
|
||||
-- entry and the coin flip with it.
|
||||
local addedKeys = {};
|
||||
for key in pairs(SlashCmdList) do
|
||||
if ( not preexistingKeys[key] ) then
|
||||
addedKeys[key] = true;
|
||||
end
|
||||
end
|
||||
|
||||
-- The command strings a SlashCmdList key answers to, upper-cased the way
|
||||
-- `ChatEdit_ParseText` compares them.
|
||||
local function CommandStrings(key)
|
||||
local commands = {};
|
||||
local i = 1;
|
||||
while ( _G["SLASH_"..key..i] ) do
|
||||
commands[strupper(_G["SLASH_"..key..i])] = true;
|
||||
i = i + 1;
|
||||
end
|
||||
return commands;
|
||||
end
|
||||
|
||||
local function ClaimedElsewhere(key, commands)
|
||||
for otherKey in pairs(SlashCmdList) do
|
||||
if ( otherKey ~= key and not addedKeys[otherKey] ) then
|
||||
local i = 1;
|
||||
while ( _G["SLASH_"..otherKey..i] ) do
|
||||
if ( commands[strupper(_G["SLASH_"..otherKey..i])] ) then
|
||||
return true;
|
||||
end
|
||||
i = i + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
RunNextFrame(function()
|
||||
for key in pairs(addedKeys) do
|
||||
if ( ClaimedElsewhere(key, CommandStrings(key)) ) then
|
||||
SlashCmdList[key] = nil;
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -92,3 +92,37 @@ function CreateSimpleTextureMarkup(file, width, height, xOffset, yOffset)
|
||||
, yOffset or 0
|
||||
);
|
||||
end
|
||||
|
||||
function CreateAtlasMarkup(atlasName, width, height, offsetX, offsetY)
|
||||
return string.format("|A:%s:%d:%d:%d:%d|a",
|
||||
atlasName
|
||||
, height or 0
|
||||
, width or 0
|
||||
, offsetX or 0
|
||||
, offsetY or 0
|
||||
);
|
||||
end
|
||||
|
||||
-- Raid target markers, published as atlas names.
|
||||
--
|
||||
-- All eight markers live in one 4x4 sheet, so naming a single marker means
|
||||
-- naming a cell inside that sheet — exactly what an atlas is for. Blizzard
|
||||
-- publishes no atlas names for these, so the names here follow the standalone
|
||||
-- marker files that other clients ship. An addon written against those files
|
||||
-- finds the art under the name it already knows.
|
||||
--
|
||||
-- Marker N sits at column (N-1) mod 4 and row floor((N-1) / 4), counting from
|
||||
-- the top left. That is the same cell math the chat marker tokens use.
|
||||
local RAID_TARGET_SHEET = "Interface\\TargetingFrame\\UI-RaidTargetingIcons";
|
||||
local RAID_TARGET_COLUMNS = 4;
|
||||
local RAID_TARGET_CELL = 0.25; -- a 64 pixel cell in a 256 pixel sheet
|
||||
|
||||
for marker = 1, 8 do
|
||||
local index = marker - 1;
|
||||
local left = math.mod(index, RAID_TARGET_COLUMNS) * RAID_TARGET_CELL;
|
||||
local top = math.floor(index / RAID_TARGET_COLUMNS) * RAID_TARGET_CELL;
|
||||
C_Texture.RegisterAtlas("UI-RaidTargetingIcon_"..marker, RAID_TARGET_SHEET,
|
||||
64, 64,
|
||||
left, left + RAID_TARGET_CELL,
|
||||
top, top + RAID_TARGET_CELL);
|
||||
end
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/UiMapPoint.lua. A UiMapPoint is the
|
||||
-- plain struct the C_Map waypoint calls take and return:
|
||||
-- { uiMapID = number, position = Vector2DMixin, z = number? }
|
||||
local CreateVector2D = CreateVector2D
|
||||
|
||||
UiMapPoint = {};
|
||||
|
||||
function UiMapPoint.CreateFromCoordinates(uiMapID, x, y, z)
|
||||
return { uiMapID = uiMapID, position = CreateVector2D(x, y), z = z };
|
||||
end
|
||||
|
||||
function UiMapPoint.CreateFromVector2D(uiMapID, position, z)
|
||||
return { uiMapID = uiMapID, position = CreateVector2D(position.x, position.y), z = z };
|
||||
end
|
||||
|
||||
function UiMapPoint.CreateFromVector3D(uiMapID, position)
|
||||
return { uiMapID = uiMapID, position = CreateVector2D(position.x, position.y), z = position.z };
|
||||
end
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Notizen für %s eintragen:";
|
||||
SET_NOTE = "Notiz eintragen";
|
||||
SILVER_AMOUNT_SYMBOL = "S";
|
||||
SLASH_CANCELAURA1 = "/auraabbruch";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/gestaltabbrechen";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/aktionsleistewechsel";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/fokusaufheben";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/zielaufheben";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/klick";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/absitzen";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/anlegen";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/Ausrüstungsset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/platzanlegen";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/fokus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/baggressiv";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/bangriff";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/bautozauberaus";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/bautozauberan";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/bautozauberanaus";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/bdefensiv";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/bfolgen";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/bpassiv";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/bbleib";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/angriffstart";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/angriffstop";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/stoppwirken";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/aktionsleisteumschalten";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/spezifischanvisieren";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/vorherigerfeind";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/vorherigesziel";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/feindanvisieren";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/feindlichenspielerwählen";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/freundanvisieren";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/freundlichenspielerwählen";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/gruppeanvisieren";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/schlachtzuganvisieren";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/benutzen";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "Hier klicken, um einen Link zu Eurem Beruf zu erstellen.";
|
||||
|
||||
@@ -79,13 +79,73 @@ SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
SLASH_CANCELAURA1 = "/cancelaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelform";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/changeactionbar";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/cleartarget";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/dismount";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/equip";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/petattack";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/petautocasttoggle";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/petdefensive";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/petfollow";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/petpassive";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/petstay";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/stopcasting";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/swapactionbar";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/targetexact";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/targetlasttarget";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Establecer nota para %s:";
|
||||
SET_NOTE = "Establecer nota";
|
||||
SILVER_AMOUNT_SYMBOL = "p";
|
||||
SLASH_CANCELAURA1 = "/cancelaraura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelforma";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/cambiarbarraccion";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/quitarenfoque";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/quitarobjetivo";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/clic";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/desmontar";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/equipar";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/conjequip";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipranu";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/enfoque";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/mascagresiva";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/mascataque";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/nomascotaautohechizo";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/mascotautohechizo";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/alternarmascotautohechizo";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/mascdefensiva";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/mascsigue";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/mascpasiva";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/mascqueda";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/empezarataque";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/pararataque";
|
||||
SLASH_STOPCASTING1 = "/pararlanzamiento";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/intercambiarbarraccion";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/objetivoexacto";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/ultimoobjetivoenemigo";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/ultimobjetivo";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/objetivoenemigo";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/objetivojugadorenemigo";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/objetivoamistoso";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/objetivojugadoraliado";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/objetivogrupo";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/objetivobanda";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/usar";
|
||||
LINK_TRADESKILL_TOOLTIP = "Haz clic aquí para crear un vínculo a tu profesión.";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Écrire une note pour %s :";
|
||||
SET_NOTE = "Écrire une note";
|
||||
SILVER_AMOUNT_SYMBOL = "a";
|
||||
SLASH_CANCELAURA1 = "/annuleaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/annuleform";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/changebarreaction";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/annulerfocal";
|
||||
SLASH_CLEARTARGET1 = "/effacercible";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/cliquer";
|
||||
SLASH_DISMOUNT1 = "/descendre";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/équiper";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/équipens";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipempl";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focal";
|
||||
SLASH_PET_AGGRESSIVE1 = "/famagresse";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/famattaque";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/lancautofamoff";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/lancautofamon";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/lancautofamchange";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/famdéfend";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/famsuit";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/fampassif";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/famreste";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/lanceattaque";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/stopattaque";
|
||||
SLASH_STOPCASTING1 = "/stopsort";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/échangerbarreaction";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/cibleexacte";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/dernierennemi";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/dernièrecible";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/ciblerennemi";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/ciblerjoueurennemi";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/ciblerami";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/ciblerjoueurami";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/ciblergroupe";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/ciblerraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/utiliser";
|
||||
LINK_TRADESKILL_TOOLTIP = "Cliquez ici pour créer un lien vers votre profession.";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Заметка о |3-5(%s):";
|
||||
SET_NOTE = "Добавить заметку";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SLASH_CANCELAURA1 = "/снятьауру";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/нормоблик";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/сменитьпанель";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/сбросфокуса";
|
||||
SLASH_CLEARTARGET1 = "/сбросцели";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/щелчок";
|
||||
SLASH_DISMOUNT1 = "/спешиться";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/экип";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/комплект";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/надеть";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/фокус";
|
||||
SLASH_PET_AGGRESSIVE1 = "/питомецагрессивный";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/питомецатака";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/питомецзаклинаниеоткл";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/питомецзаклинаниевкл";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/питомецавтозаклинание";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/питомецзащита";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/питомецзамной";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/питомецпассивный";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/питомецстоять";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/начатьатаку";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/стопатака";
|
||||
SLASH_STOPCASTING1 = "/стопзаклинание";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/сменитьпанелькоманд";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/цельточно";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/цельпослвраг";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/цельпослцель";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/цельвраг";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/цельдруг";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/цельгруппа";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/цельрейд";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/использовать";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "为%s设置备注:";
|
||||
SET_NOTE = "设置备注";
|
||||
SILVER_AMOUNT_SYMBOL = "银";
|
||||
SLASH_CANCELAURA1 = "/cancelaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelform";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/changeactionbar";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/cleartarget";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/dismount";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/equip";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/petattack";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/petautocasttoggle";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/petdefensive";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/petfollow";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/petpassive";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/petstay";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/stopcasting";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/swapactionbar";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/targetexact";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/targetlasttarget";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "設定%s的註記:";
|
||||
SET_NOTE = "設定註記";
|
||||
SILVER_AMOUNT_SYMBOL = "銀";
|
||||
SLASH_CANCELAURA1 = "/取消光環";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/取消形態";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/改變快捷列";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/清除專注目標";
|
||||
SLASH_CLEARTARGET1 = "/清除目標";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/點擊";
|
||||
SLASH_DISMOUNT1 = "/下馬";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/裝備";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/裝備套裝";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/裝備欄位";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/專注目標";
|
||||
SLASH_PET_AGGRESSIVE1 = "/寵物攻擊型";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/寵物攻擊";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/關閉寵物自動施法";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/開啟寵物自動施法";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/切換寵物自動施法";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/寵物防禦型";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/寵物跟隨";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/寵物被動型";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/寵物停留";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/開始攻擊";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/停止攻擊";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/停止施法";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/切換快捷列";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/選定指定目標";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/選定前一敵人";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/選定前一目標";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/目標敵人";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/選定敵方玩家";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/目標友方";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/選定友方玩家";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/目標隊伍";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/目標團隊";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/使用";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
|
||||
|
||||
@@ -2,6 +2,21 @@ cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(ClassicAPI LANGUAGES CXX RC)
|
||||
|
||||
# WoW.exe is x86, so the injected DLL must be x86 too — there is no valid
|
||||
# 64-bit configure of this project. Fail here rather than let one through:
|
||||
# a 64-bit build's first visible symptom is a static_assert deep in a
|
||||
# struct-layout check, which reads as an unrelated code fault. An IDE that
|
||||
# configures on its own (VS Code's CMake Tools picks an architecture from a
|
||||
# scanned kit) can flip an existing build directory this way, which is what
|
||||
# CMakePresets.json's `win32` preset exists to prevent.
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
message(FATAL_ERROR
|
||||
"ClassicAPI builds 32-bit only. This configure uses "
|
||||
"${CMAKE_SIZEOF_VOID_P}-byte pointers. Delete the build directory, "
|
||||
"because CMake cannot switch platform in place. Then configure with "
|
||||
"`cmake --preset win32` or `cmake -B build -A Win32`.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 3,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 21,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "win32",
|
||||
"displayName": "Win32 (x86)",
|
||||
"description": "The only supported configuration: WoW.exe is 32-bit, so the DLL must be 32-bit too. No generator is named, so CMake picks the newest installed Visual Studio, matching the documented command line.",
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"architecture": "Win32"
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "release",
|
||||
"displayName": "Release",
|
||||
"configurePreset": "win32",
|
||||
"configuration": "Release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -22,6 +22,7 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| [Syntax](docs/API.md#lua-51-syntax) | Compiles the Lua 5.1 length (`#`), modulo (`%`), `...`-expression, `0x` hex-literal, and leveled long bracket (`[=[ ]=]`) syntax that vanilla's Lua 5.0 rejects, by rewriting addon source before it compiles. Each addon file also receives its `(name, table)` through `...` (`local name, tbl = ...`). |
|
||||
| [Upvalue limit](docs/API.md#upvalue-limit) | A function can use up to 60 upvalues, the outer locals it refers to, matching Lua 5.1. Lua 5.0 stops at 32 and rejects the whole file, so a large handler that reads many file-level locals can be valid 5.1 and still fail to load here. Nothing changes for a function within 32. |
|
||||
| [String methods](docs/API.md#string-methods-supper-sformat) | Every string value accepts method calls — `("asd"):upper()`, `("%d gold"):format(n)`, `msg:match("^!(%w+)")` — resolving through the `string` table, the way Lua 5.1 works. Works on literals and variables, in-world and on the login screen, and inside coroutines. |
|
||||
| [Script-handler arguments](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — so modern addon ports work unmodified. The `this` / `arg1` globals stay set. A handler that declares no parameters is unaffected. A handler that declared a parameter and expected nil now receives its real value. On by default. `SetModernScriptArgs(false)` restores exact vanilla dispatch. |
|
||||
|
||||
@@ -29,12 +30,16 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows what the macro does. The icon, tooltip, cooldown, range, usable state and auto-repeat glow all follow the spell or item that the directive resolves to, conditions included. They update as the conditions change. The result goes into the engine field that action-bar addons already read, so they need no code of their own. An addon with its own macro parser can publish its result through `C_Macro.SetMacroDisplay`. It then does not need to replace the action globals. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
|
||||
| [Focus target](docs/API.md#focus) | A sticky focus unit. `FocusUnit("target")` pins it and `ClearFocus()` drops it, with `PLAYER_FOCUS_CHANGED` on every change. The `focus` / `focustarget` tokens resolve with every `UnitX` function and fire unit events (`UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "focus"`), and the predefined `FOCUSTARGET` / `TARGETFOCUS` keybinds are ready to set. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. |
|
||||
| [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`<Name>_ClassicAPI.toc` or `<Name>_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. |
|
||||
| [Launch switches](docs/API.md#launch) | Options you add to the shortcut that starts the game. `-config <name>` reads and writes `WTF\<name>` in place of `WTF\Config.wtf`, so one install can hold several settings profiles. `-gluescript` and `-gluescriptFile` run Lua at the login and character-select screens, every time those screens appear. `-gamescript` and `-gamescriptFile` run it one time, after you enter the world. Each script runs after the interface has loaded, so it can call interface functions and use frames. |
|
||||
| [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any size and any shape. Width and height do not need to be powers of two, and there is no fixed upper limit. The only limit is the maximum texture size of your graphics card. This applies to every texture path: `SetTexture`, sprite sheets, inline textures, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one, so small textures cost nothing extra. |
|
||||
|
||||
## Full API reference
|
||||
|
||||
@@ -48,7 +53,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
@@ -56,18 +61,19 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [ColorUtil](docs/API.md#colorutil) | `C_ColorUtil.ConvertRGBToHSV`, `C_ColorUtil.ConvertHSVToRGB`, `C_ColorUtil.ConvertHSVToHSL`, `C_ColorUtil.ConvertHSLToHSV`, `C_ColorUtil.ConvertHSLToRGB`, `C_ColorUtil.GenerateTextColorCode`, `C_ColorUtil.WrapTextInColor`, `C_ColorUtil.WrapTextInColorCode` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown`, `StartAttack`, `StopAttack` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.SetCVarBitfield` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `editBox:SetCursorPosition`, `editBox:GetCursorPosition`, `editBox:GetUTF8CursorPosition`, `editBox:ClearHighlightText`, `editBox:HasFocus`, `editBox:HasText`, `editBox:SetHighlightColor`, `editBox:GetHighlightColor`, `editBox:ClearHistory`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `PreClick` (script), `PostClick` (script), `GetClickFrame` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetAtlas`, `texture:GetAtlas`, `texture:ResetTexCoord`, `texture:SetSpriteSheetCell`, `texture:SetDesaturation`, `texture:GetDesaturation`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `editBox:SetCursorPosition`, `editBox:GetCursorPosition`, `editBox:GetUTF8CursorPosition`, `editBox:ClearHighlightText`, `editBox:HasFocus`, `editBox:HasText`, `editBox:SetHighlightColor`, `editBox:GetHighlightColor`, `editBox:ClearHistory`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:RegisterUnitEvent`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `PreClick` (script), `PostClick` (script), `GetClickFrame` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
@@ -76,14 +82,14 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [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` |
|
||||
| [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.GetItemTempEnchantInfo`, `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.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) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.gsub` (table replacement), `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.gsub` (table replacement), `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
@@ -91,20 +97,21 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [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.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `GetSheathState`, `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` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [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` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [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`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [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` |
|
||||
|
||||
@@ -132,17 +139,40 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Macros</b> — engine-level parsing extensions</summary>
|
||||
<summary><b>Macros</b> — forms a macro body accepts</summary>
|
||||
|
||||
Engine-level extensions to macro parsing and dispatch — no new Lua
|
||||
functions. See the
|
||||
[Macros section in the Lua reference](docs/API.md#macros) for details.
|
||||
The DLL parses these forms, so an action-bar addon needs no code for them.
|
||||
See the [Macros section in the Lua reference](docs/API.md#macros) for
|
||||
details.
|
||||
|
||||
| Form | What it does |
|
||||
|------|--------------|
|
||||
| `/cast <spellID>` | `/cast 5019` casts Shoot if known; macro slot tags correctly for action-bar UI |
|
||||
| `CastSpellByName("<spellID>")` | Same — numeric strings resolve through the engine's name resolver |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | Engine's macro parser now recognizes it as a primary-spell line, so the macro slot in an addon like pfUI highlights when its spell is auto-repeating or its self-aura is active |
|
||||
| `#showtooltip [conditions] Value` | First line of a macro. Sets what the button shows. Bare `#showtooltip` follows the first `/cast` or `/use` line instead. `#show` is the same form. |
|
||||
| `[conditions]` and `@unit` | Accepted by 38 commands. The slash-command list below names them. |
|
||||
| `!Name` | Starts an ability but never turns it off. Use it for auto-repeat shots and for the self-buffs (stance, aspect, seal, form, tracking). |
|
||||
| A line that starts with `#` | A comment. The line never reaches chat. |
|
||||
| `/cast <spellID>` | For a spell you know, `/cast 5019` casts it by ID. The macro slot also tags correctly for an action-bar addon. |
|
||||
| `CastSpellByName("<spellID>")` | A numeric string resolves through the engine name resolver, the same as `/cast`. |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | The engine macro parser reads this as a primary-spell line. The macro slot in an addon such as pfUI then highlights while the spell is on auto-repeat or its self-aura is active. |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Slash commands</b> — <code>[conditions]</code> on 38 commands</summary>
|
||||
|
||||
The bundled addon registers these commands. Each one takes the same
|
||||
`[conditions]` and `@unit` syntax as `/cast`. Each client language has its
|
||||
own command names next to the English ones. See
|
||||
[More commands with `[conditions]`](docs/API.md#more-commands-with-conditions)
|
||||
for what each command does.
|
||||
|
||||
| Group | Commands |
|
||||
|-------|----------|
|
||||
| Casting | `/cast`, `/use`, `/stopcasting`, `/cancelaura`, `/cancelform`, `/dismount` |
|
||||
| Targeting | `/target`, `/targetexact`, `/cleartarget`, `/targetlasttarget`, `/targetlastenemy`, `/targetenemy`, `/targetfriend`, `/targetenemyplayer`, `/targetfriendplayer`, `/targetparty`, `/targetraid`, `/assist`, `/follow`, `/focus`, `/clearfocus`, `/startattack`, `/stopattack` |
|
||||
| Equipment | `/equip`, `/equipslot`, `/equipset` |
|
||||
| Action bars | `/changeactionbar`, `/swapactionbar`, `/click` |
|
||||
| Pet | `/petattack`, `/petfollow`, `/petstay`, `/petpassive`, `/petdefensive`, `/petaggressive`, `/petautocaston`, `/petautocastoff`, `/petautocasttoggle` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -168,6 +198,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|-------|---------|
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
| `EQUIPMENT_SWAP_FINISHED` | `success, setID` |
|
||||
@@ -176,6 +207,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `GLOBAL_MOUSE_UP` | `button` |
|
||||
| `HEARTHSTONE_BOUND` | *(none)* |
|
||||
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
|
||||
| `LEARNED_SPELL_IN_SKILL_LINE` | `spellID, skillLineIndex, isGuildPerkSpell` |
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` |
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
@@ -213,6 +245,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_STARTED` | `numConsumers, utteranceID, durationMS, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FINISHED` | `numConsumers, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FAILED` | `status, utteranceID, destination` |
|
||||
@@ -234,6 +267,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
| Item quality | `Enum.ItemQuality.{Poor,Common,Uncommon,Rare,Epic,Legendary,Artifact}` (0–6) |
|
||||
| Cursor type | `Enum.UICursorType.{Default,Item,Money,Spell,PetAction,Merchant,Macro,Pet,…}` (0–20; this client yields 0–5, 7 and 9) |
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
|
||||
@@ -301,7 +335,9 @@ functions but that consumer code still expects to find as globals —
|
||||
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
|
||||
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), and `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` etc.).
|
||||
(`ContinueOnAddOnLoaded` and more). It also holds the conditional slash
|
||||
commands and `SecureCmdOptionParse`, the parser that reads `[conditions]`
|
||||
and `@unit` for them.
|
||||
|
||||
**You don't have to install the addon manually** — the DLL embeds
|
||||
the contents of [`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/) and
|
||||
|
||||
+2865
-308
File diff suppressed because it is too large
Load Diff
@@ -556,9 +556,26 @@ Render the modern inline-texture escape (warcraft.wiki.gg/wiki/UI_escape_sequenc
|
||||
|Tpath:height[:width[:offsetX:offsetY[:texWidth:texHeight:left:right:top:bottom[:rV:gV:bV]]]]|t
|
||||
```
|
||||
|
||||
Atlas markup (`|A:atlas:…|a`) is Legion+ (needs an atlas DB 1.12 lacks) — out
|
||||
of scope. FileDataID paths are Legion+ — 1.12 resolves texture PATH strings
|
||||
only.
|
||||
Atlas markup is also supported:
|
||||
|
||||
```
|
||||
|A:atlasName:height:width[:offsetX:offsetY]|a
|
||||
```
|
||||
|
||||
It shares every scanner, measure, wrap and placement stage with `|T` — only the
|
||||
payload parser differs. `ParseAtlasIcon` resolves the name through
|
||||
`Texture::Atlas` and fills the SAME `IconDesc`, because an atlas is exactly the
|
||||
"path + normalized sub-rect" that struct already carries for a `|T` span with
|
||||
texcoords. A `height`/`width` of `0` resolves to the atlas's own pixel size at
|
||||
PARSE time (not the line font height `|T` falls back to), which is what keeps the
|
||||
rest of the pipeline marker-agnostic. Tiling atlases draw one quad, so
|
||||
`tilesHorizontally` / `tilesVertically` are not honored here.
|
||||
|
||||
Adding any further marker means touching `IconStartLen`, `FindIconClose`,
|
||||
`InlineSpanLen`, the ghost guard in `FlushLayout` — and, mandatory,
|
||||
`chat/IconFilter.cpp`, or the marker becomes a chat-spoof vector.
|
||||
|
||||
FileDataID paths are Legion+ — 1.12 resolves texture PATH strings only.
|
||||
|
||||
## Confirmed: 1.12 has ZERO inline-texture support
|
||||
|
||||
|
||||
@@ -213,6 +213,8 @@ filenames in Lua 5.0 by behaviour.
|
||||
|--------------|--------------------------|---------------------------------------------------|
|
||||
| `0x006F3140` | `setobj` / TValue copy | 16-byte memcpy |
|
||||
| `0x006F58B0` | `equalobj` | called by `lua_equal` |
|
||||
| `0x006F6050` | `luaD_precall` | frame setup for a call; Lua closure: reads Proto (`+0xc` of the closure), `is_vararg` at Proto `+0x46` → `adjust_varargs`, `numparams` `+0x45`, `maxstacksize` `+0x47`; C closure: reserves 0x14 slots and calls the C function |
|
||||
| `0x006F6200` | `adjust_varargs` | stock 5.0: pads missing fixed params with nil, then `luaH_new(L, nvar, 1)` + `luaH_setnum` per extra arg + sets field `"n"` and pushes the table. So EVERY call to a vararg Lua function allocates one table, whether the body uses `arg` or `...` — keep vararg helpers off per-frame paths |
|
||||
| `0x006F65A0` | `luaD_call` | the actual call dispatcher |
|
||||
| `0x006F6960` | `luaD_pcall` | protected call dispatcher |
|
||||
| `0x006F7340` | `luaD_growstack` (or checkstack) | invoked when stack needs space |
|
||||
|
||||
@@ -31,4 +31,13 @@ bool PatchBytes(void *dst, const void *src, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void BoundedCopy(char *dst, const char *src, size_t n) {
|
||||
if (dst == nullptr || n == 0)
|
||||
return;
|
||||
size_t i = 0;
|
||||
for (; src != nullptr && src[i] != '\0' && i + 1 < n; ++i)
|
||||
dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -11,8 +11,16 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Common {
|
||||
|
||||
bool PatchBytes(void *dst, const void *src, size_t len);
|
||||
|
||||
// Bounded, always-terminated string copy: writes at most `n - 1`
|
||||
// characters plus the terminator, and treats a null `src` as empty. `n`
|
||||
// is the size of `dst`. Stands in for `strncpy`, which MSVC deprecates
|
||||
// (C4996) and which does not terminate on truncation.
|
||||
void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "Game.h"
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "config/FileSwitch.h"
|
||||
#include "event/Custom.h"
|
||||
#include "player/NameCache.h"
|
||||
#include "text/InlineTexture.h"
|
||||
@@ -207,6 +208,14 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
DisableThreadLibraryCalls(hModule);
|
||||
|
||||
// The ONE thing that has to happen this early, and the only module this
|
||||
// file names directly. The engine reads its config file during boot,
|
||||
// several calls before VanillaFixes invokes `Load`, so a `-config`
|
||||
// switch cannot be applied from the normal install path — by then the
|
||||
// file has been read. This is a single 4-byte store, not a hook, so it
|
||||
// carries none of the loader-lock cost described above.
|
||||
Config::FileSwitch::Apply();
|
||||
|
||||
// Install nothing here (loader lock — see the block comment above).
|
||||
//
|
||||
// Under VanillaFixes, `Load` is the ONLY install trigger. VfPatcher.dll
|
||||
|
||||
+164
-16
@@ -99,26 +99,79 @@ void *ResolveObject(void *L, int idx) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void RegisterGlobalFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
namespace {
|
||||
using FrameScriptIsA_t = char(__thiscall *)(void *self, int typeId);
|
||||
|
||||
const char *EngineString(uintptr_t address) {
|
||||
return reinterpret_cast<const char *>(address);
|
||||
}
|
||||
|
||||
// Aliased to the same engine entry; the engine reads VAR_LUA_STATE
|
||||
// internally to decide which state to write to, and the glue hook
|
||||
// runs while that pointer is set to the glue state. Wrapped as a
|
||||
// named function (rather than `using RegisterGlueFunction = ...`) so
|
||||
// callers express intent at the call site.
|
||||
void RegisterGlueFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
// Shared body: the engine's prologue with N accepted type ids.
|
||||
void *ResolveChecked(void *L, int idx, const uintptr_t *typeIdVars, int count,
|
||||
bool raiseError) {
|
||||
if (Type(L, idx) != TYPE_TABLE) {
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_THIS_NON_TABLE));
|
||||
return nullptr;
|
||||
}
|
||||
void *obj = ResolveObject(L, idx);
|
||||
if (obj == nullptr) {
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_THIS_NON_OBJECT));
|
||||
return nullptr;
|
||||
}
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
auto IsA = reinterpret_cast<FrameScriptIsA_t>(
|
||||
vtable[Offsets::OFF_VMT_FRAMESCRIPT_ISA / sizeof(void *)]);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (IsA(obj, FrameScriptTypeId(typeIdVars[i])))
|
||||
return obj;
|
||||
}
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_WRONG_OBJECT_TYPE));
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int FrameScriptTypeId(uintptr_t typeIdVar) {
|
||||
auto &typeId = Game::Ref<int>(typeIdVar);
|
||||
if (typeId == 0) {
|
||||
auto &counter = Game::Ref<int>(Offsets::VAR_FRAMESCRIPT_TYPE_ID_COUNTER);
|
||||
counter += 1;
|
||||
typeId = counter;
|
||||
}
|
||||
return typeId;
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
void *ResolveTypedObject(void *L, int idx, uintptr_t typeIdVar, bool raiseError) {
|
||||
return ResolveChecked(L, idx, &typeIdVar, 1, raiseError);
|
||||
}
|
||||
|
||||
void *ResolveTypedObjectAny(void *L, int idx, const uintptr_t *typeIdVars,
|
||||
int count, bool raiseError) {
|
||||
return ResolveChecked(L, idx, typeIdVars, count, raiseError);
|
||||
}
|
||||
|
||||
void *ResolveTooltip(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_GAMETOOLTIP_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveFrame(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_FRAME_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveRegion(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_REGION_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveTexture(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_TEXTURE_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveFontString(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_FONTSTRING_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveEditBox(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_EDITBOX_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_MODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
|
||||
// Looks up `_G[name]`. If absent, creates a fresh table and binds it.
|
||||
@@ -136,8 +189,102 @@ void EnsureGlobalTable(void *L, const char *name) {
|
||||
Insert(L, -2); // [tbl, name, tbl]
|
||||
SetTable(L, GLOBALS_INDEX); // _G[name] = tbl; pops k+v. [tbl]
|
||||
}
|
||||
|
||||
// Same, one level down: ensures `parent[name]` is a table, with `parent`
|
||||
// on top of the stack. Leaves `[parent, sub]`. Raw access, so a namespace
|
||||
// carrying a metatable can't divert the read or the write.
|
||||
void EnsureSubTable(void *L, const char *name) {
|
||||
PushString(L, name); // [parent, name]
|
||||
RawGet(L, -2); // [parent, parent[name]]
|
||||
if (Type(L, -1) == TYPE_TABLE)
|
||||
return;
|
||||
SetTop(L, -2); // pop the non-table. [parent]
|
||||
NewTable(L); // [parent, sub]
|
||||
PushString(L, name); // [parent, sub, name]
|
||||
PushValue(L, -2); // [parent, sub, name, sub]
|
||||
RawSet(L, -4); // parent[name] = sub. [parent, sub]
|
||||
}
|
||||
|
||||
// The escape-hatch namespace. Registering a name is not the same as owning
|
||||
// it: the engine's registrars write `_G`, FrameXML and every addon load
|
||||
// after that, and whoever writes last wins with no error and no way for the
|
||||
// loser to notice. `GetServerTime` is the case on record — a client whose
|
||||
// GameTime.lua declares its own replaced ours on every login.
|
||||
//
|
||||
// So every name the registrars below bind is also bound under
|
||||
// `_G.ClassicAPI`, which nothing else writes. Two properties earn their
|
||||
// keep, and both depend on doing this HERE rather than from Lua:
|
||||
//
|
||||
// - It runs at registration, so the mirror captures OUR function. A
|
||||
// Lua-side `ClassicAPI.X = X` in the embedded addon would run after
|
||||
// FrameXML and faithfully copy the clobbered version.
|
||||
// - It mirrors by VALUE, reading the name back instead of making a
|
||||
// second closure over the same C function. So `ClassicAPI.X == X`
|
||||
// holds until something replaces the global, and the pair doubles as
|
||||
// a clobber check for addons and for us.
|
||||
//
|
||||
// Not covered: frame methods (`obj:Method()` lives in the per-frame-type
|
||||
// registry, not `_G`) and `RegisterIntegerEnum` values (a mirrored number
|
||||
// is a copy, not a reference).
|
||||
constexpr const char *kMirrorTable = "ClassicAPI";
|
||||
|
||||
void MirrorRegistration(const char *tableName, const char *name) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = GetTop(L);
|
||||
|
||||
// Read back whatever the registration actually bound.
|
||||
if (tableName != nullptr) {
|
||||
EnsureGlobalTable(L, tableName); // [src]
|
||||
PushString(L, name); // [src, name]
|
||||
RawGet(L, -2); // [src, value]
|
||||
} else {
|
||||
PushString(L, name); // [name]
|
||||
GetTable(L, GLOBALS_INDEX); // [value]
|
||||
}
|
||||
if (Type(L, -1) != TYPE_FUNCTION) {
|
||||
SetTop(L, top); // registration didn't land; nothing to mirror
|
||||
return;
|
||||
}
|
||||
const int valueIdx = GetTop(L); // absolute, so the pushes below can't skew it
|
||||
|
||||
EnsureGlobalTable(L, kMirrorTable); // [..., mirror]
|
||||
if (tableName != nullptr)
|
||||
EnsureSubTable(L, tableName); // [..., mirror, sub]
|
||||
PushString(L, name);
|
||||
PushValue(L, valueIdx);
|
||||
RawSet(L, -3); // target[name] = value
|
||||
SetTop(L, top);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void RegisterGlobalFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
MirrorRegistration(nullptr, name);
|
||||
}
|
||||
|
||||
// Aliased to the same engine entry; the engine reads VAR_LUA_STATE
|
||||
// internally to decide which state to write to, and the glue hook
|
||||
// runs while that pointer is set to the glue state. Wrapped as a
|
||||
// named function (rather than `using RegisterGlueFunction = ...`) so
|
||||
// callers express intent at the call site.
|
||||
void RegisterGlueFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
// `State()` is the glue state here for the same reason the engine
|
||||
// registrar writes there, so the mirror lands on the glue `_G`.
|
||||
MirrorRegistration(nullptr, name);
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
}
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`. If the namespace
|
||||
// doesn't already exist, creates an empty table for it.
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName, CFunction func) {
|
||||
@@ -149,6 +296,7 @@ void RegisterTableFunction(const char *tableName, const char *methodName, CFunct
|
||||
PushCClosure(L, func, 0); // [tbl, methodName, closure]
|
||||
SetTable(L, -3); // tbl[m]=c; pops k+v. [tbl]
|
||||
SetTop(L, -2); // pop tbl. []
|
||||
MirrorRegistration(tableName, methodName);
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
|
||||
+50
@@ -216,6 +216,56 @@ void *State();
|
||||
// the `self` slot of any `frame:method(...)` invocation.
|
||||
void *ResolveObject(void *L, int idx);
|
||||
|
||||
// Same resolve, then the type gate every engine frame method applies to its
|
||||
// `self` before touching it: `lua_type == table`, resolve to the object, then
|
||||
// the `IsA(typeId)` vmethod at vtable+0x10. `ResolveObject` alone is not
|
||||
// enough for a method that casts the result — it hands back a
|
||||
// `CFrameScriptObject *` for ANY frame, so `GameTooltip.SetSpellByID(button)`
|
||||
// would reach the tooltip builder with a Button. Frame methods are ordinary
|
||||
// first-class Lua values (the per-type dispatcher is an `__index` handler
|
||||
// that returns the method closure), so that call is reachable from plain Lua
|
||||
// and the gate is not optional.
|
||||
//
|
||||
// `typeIdVar` is the class's `VAR_*_LUA_TYPE_ID`, which must match the
|
||||
// registry the method was registered on. The id is assigned lazily by
|
||||
// whichever method of the class runs first, so these mirror that assignment
|
||||
// and work even before any stock method of the type has been called.
|
||||
//
|
||||
// Raises the engine's own three errors and returns null. Pass
|
||||
// `raiseError = false` for an optional object ARGUMENT, where the caller
|
||||
// wants a silent null rather than a "this" error naming the wrong slot.
|
||||
// Returns null either way, so callers always check.
|
||||
void *ResolveTypedObject(void *L, int idx, uintptr_t typeIdVar,
|
||||
bool raiseError = true);
|
||||
|
||||
// For a method registered on SEVERAL registries, where any of those types is
|
||||
// a legitimate `self`. Accepts the object if `IsA` passes for any of the
|
||||
// listed ids — which is exactly what being in all of those registries means,
|
||||
// and avoids having to pin down the shared base class.
|
||||
void *ResolveTypedObjectAny(void *L, int idx, const uintptr_t *typeIdVars,
|
||||
int count, bool raiseError = true);
|
||||
|
||||
// The class's live type id, assigning it if the engine has not yet. Needed
|
||||
// directly only when calling a type-specific vtable slot.
|
||||
int FrameScriptTypeId(uintptr_t typeIdVar);
|
||||
|
||||
// Per-type sugar over `ResolveTypedObject`, so a call site cannot pair a
|
||||
// method with the wrong class's id. `idx` defaults to the `self` slot.
|
||||
void *ResolveTooltip(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFrame(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveRegion(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
|
||||
|
||||
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
|
||||
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
|
||||
// hatch for names something else replaces later, and by value so
|
||||
// `ClassicAPI.X == X` until that happens. Automatic; a new module needs no
|
||||
// extra call. See `MirrorRegistration` in Game.cpp for why it lives here
|
||||
// and what it does not cover.
|
||||
|
||||
// Registers a single global Lua function (e.g. `GetSpellInfo`). The function
|
||||
// must use the WoW Lua C function ABI: `int __fastcall(void *L)`.
|
||||
void RegisterGlobalFunction(const char *name, CFunction func);
|
||||
|
||||
+1222
-56
File diff suppressed because it is too large
Load Diff
+2
-14
@@ -47,6 +47,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -62,19 +63,6 @@ uint32_t ReadEntry(int slot0) {
|
||||
return base[slot0];
|
||||
}
|
||||
|
||||
// Returns 1-based macro slot index if `key` matches any entry in the
|
||||
// macro-slot map, or 0 if no match. `key` is the action entry with the
|
||||
// `0x40000000` type bit stripped (i.e. `entry & 0xBFFFFFFF`).
|
||||
int FindMacroSlot(uint32_t key) {
|
||||
auto *map = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MACRO_SLOT_MAP));
|
||||
for (int i = 0; i < Offsets::MACRO_SLOT_MAP_COUNT; ++i) {
|
||||
if (map[i] == key)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetActionInfo(slot)");
|
||||
@@ -117,7 +105,7 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
|
||||
if (type == Offsets::ACTION_TYPE_BAG_OR_MACRO) {
|
||||
const uint32_t key = entry & Offsets::ACTION_PAYLOAD_MASK_BAG_OR_MACRO;
|
||||
const int macroSlot = FindMacroSlot(key);
|
||||
const int macroSlot = Action::Slot::MacroSlotForID(key);
|
||||
if (macroSlot > 0) {
|
||||
Game::Lua::PushString(L, "macro");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(macroSlot));
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetActionCooldown` / `GetActionCount` / `IsConsumableAction` for macro
|
||||
// slots whose `#showtooltip` / `#show` resolved to an ITEM. A spell
|
||||
// resolution needs none of this — the engine reads a macro slot's spell from
|
||||
// its primary-spell cache, which `Macro::ShowTooltip` writes — but the engine
|
||||
// has no per-macro item field, so the item forms of these three are answered
|
||||
// here, each with the engine's own helper for the item-by-ID slot case:
|
||||
//
|
||||
// - cooldown → `FUN_ITEM_QUERY_COOLDOWN` via `Item::Cooldown::PushCooldown`
|
||||
// (the same triple `GetContainerItemCooldown` returns);
|
||||
// - count → `FUN_ACTION_ITEM_COUNT`, the value `UseAction` caches per
|
||||
// item-by-ID slot;
|
||||
// - consumable → the rule of the inner `IsConsumableAction` (`0x004E5250`):
|
||||
// ammo / thrown, or any on-use spell slot with negative
|
||||
// charges.
|
||||
//
|
||||
// Registered in front of the engine globals (last registration wins) and a
|
||||
// strict superset of them: every other slot tail-calls the engine function
|
||||
// with the untouched stack.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Cooldown.h"
|
||||
#include "item/Record.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::ItemState {
|
||||
|
||||
namespace {
|
||||
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
using ItemCount_t = uint32_t(__fastcall *)(uint32_t itemID);
|
||||
|
||||
int Engine(uintptr_t fn, void *L) {
|
||||
return reinterpret_cast<ScriptFn_t>(fn)(L);
|
||||
}
|
||||
|
||||
// True (and fills `info`) when stack[1] is a slot whose macro resolved to an item.
|
||||
bool ItemTarget(void *L, Macro::ShowTooltip::Info *info) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return false;
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
return Macro::ShowTooltip::ForSlot(slot0, info) &&
|
||||
info->target == Macro::ShowTooltip::Target::Item;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionCooldown(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_GET_ACTION_COOLDOWN, L);
|
||||
Item::Cooldown::PushCooldown(L, info.itemID);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionCount(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_GET_ACTION_COUNT, L);
|
||||
const uint32_t count = reinterpret_cast<ItemCount_t>(Offsets::FUN_ACTION_ITEM_COUNT)(
|
||||
static_cast<uint32_t>(info.itemID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(count));
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool IsConsumable(int itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const uint32_t invType = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
if (invType == Offsets::INVTYPE_AMMO || invType == Offsets::INVTYPE_THROWN)
|
||||
return true;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
const uint32_t spellID = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_ID + i * 4);
|
||||
const uint32_t trigger = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER + i * 4);
|
||||
const int32_t charges = Game::Read<int32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_CHARGES + i * 4);
|
||||
if (spellID != 0 && trigger == Offsets::ITEM_SPELLTRIGGER_ON_USE && charges < 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsConsumableAction(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_IS_CONSUMABLE_ACTION, L);
|
||||
if (IsConsumable(info.itemID))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCooldown", &Script_GetActionCooldown);
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCount", &Script_GetActionCount);
|
||||
Game::Lua::RegisterGlobalFunction("IsConsumableAction", &Script_IsConsumableAction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::ItemState
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "action/Slot.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Action::Slot {
|
||||
|
||||
int MacroSlotForID(uint32_t macroID) {
|
||||
if (macroID == 0)
|
||||
return 0;
|
||||
auto *map = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MACRO_SLOT_MAP));
|
||||
for (int i = 0; i < Offsets::MACRO_SLOT_MAP_COUNT; ++i) {
|
||||
if (map[i] == macroID)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t MacroIDForSlot(int slot0) {
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE));
|
||||
const uint32_t entry = table[slot0];
|
||||
if (entry == 0 || (entry & Offsets::ACTION_TYPE_MASK) != Offsets::ACTION_TYPE_BAG_OR_MACRO)
|
||||
return 0;
|
||||
const uint32_t key = entry & Offsets::ACTION_PAYLOAD_MASK_BAG_OR_MACRO;
|
||||
return MacroSlotForID(key) != 0 ? key : 0;
|
||||
}
|
||||
|
||||
} // namespace Action::Slot
|
||||
@@ -0,0 +1,36 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Action-slot ↔ macro decoding shared by every module that has to answer
|
||||
// "which macro sits in this action slot" (`GetActionInfo`, the
|
||||
// `#showtooltip` display overrides). The action table stores macroIDs under
|
||||
// the ambiguous `0x40000000` tag; the macro-slot map says which of those ids
|
||||
// are macros (the rest are bag items). Pure C — no Lua stack.
|
||||
|
||||
namespace Action::Slot {
|
||||
|
||||
// 1-based macro slot holding `macroID` — the same linear scan of
|
||||
// `VAR_MACRO_SLOT_MAP` the engine's `FUN_MACRO_ID_TO_SLOT` does — or 0 when
|
||||
// no macro has that id (0 is never a macroID).
|
||||
int MacroSlotForID(uint32_t macroID);
|
||||
|
||||
// The macroID in action slot `slot0` (0-based, `< ACTION_TABLE_MAX_SLOTS`),
|
||||
// or 0 when the slot is empty, isn't `0x40000000`-tagged, or holds a bag
|
||||
// item rather than a macro.
|
||||
uint32_t MacroIDForSlot(int slot0);
|
||||
|
||||
} // namespace Action::Slot
|
||||
+24
-10
@@ -26,6 +26,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/LoadState.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -251,19 +252,23 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
|
||||
|
||||
// `C_AddOns.IsAddOnLoaded(indexOrName)` → `(loadedOrLoading, loaded)`.
|
||||
//
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded" —
|
||||
// matters for LoD addons whose load is split across multiple
|
||||
// `LoadAddOn` callbacks. Vanilla 1.12's `FUN_0051F240` is fully
|
||||
// synchronous: the loaded byte at `entry+0x18` flips from 0 → 1
|
||||
// inside a single call, so the "loading" state is never observable
|
||||
// from Lua. We return the same boolean twice — `loadedOrLoading`
|
||||
// and `loaded` are identical here.
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded", and
|
||||
// the two really are distinguishable here: the engine's loaded byte
|
||||
// (`entry+0x18`) is set at the START of a load, before the addon's files
|
||||
// run, so the byte alone answers `loadedOrLoading`, not `loaded`. The
|
||||
// window is reachable from Lua — an addon's own file-scope code, and any
|
||||
// dependency pulled in during it, run while the byte already reads set.
|
||||
//
|
||||
// So `loadedOrLoading` is the engine's byte, and `loaded` additionally
|
||||
// requires the addon not to be mid-load, which `AddOns::LoadState`
|
||||
// tracks from the loader itself.
|
||||
//
|
||||
// Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and
|
||||
// string names. `IS_LOADED` accepts an entry pointer as its name arg
|
||||
// because the entry's first 12 bytes are the inline NUL-terminated
|
||||
// directory name (same as the other per-field accessors), so we can
|
||||
// feed it either an entry or a Lua-side string transparently.
|
||||
// feed it either an entry or a Lua-side string transparently — and that
|
||||
// same pointer is the name `LoadState` matches on.
|
||||
int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
@@ -273,8 +278,11 @@ int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
}
|
||||
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOADED);
|
||||
const bool loaded = isLoaded(entry) != 0;
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
const bool loadedOrLoading = isLoaded(entry) != 0;
|
||||
const bool loaded =
|
||||
loadedOrLoading &&
|
||||
!AddOns::LoadState::IsLoading(reinterpret_cast<const char *>(entry));
|
||||
Game::Lua::PushBool(L, loadedOrLoading);
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
return 2;
|
||||
}
|
||||
@@ -385,6 +393,12 @@ static void RegisterLuaFunctions() {
|
||||
&Script_DoesAddOnExist);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
|
||||
&Script_IsAddOnLoaded);
|
||||
// The stock global, mirrored into the namespace — the engine's own
|
||||
// handler already has the Lua C ABI and the modern return shape.
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_AddOns", "LoadAddOn",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_LOAD_ADDON)));
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnOptionalDependencies",
|
||||
&Script_GetAddOnOptionalDependencies);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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/>.
|
||||
|
||||
// Tracks in-progress addon loads so `C_AddOns.IsAddOnLoaded` can report
|
||||
// `loadedOrLoading` and `loaded` separately. See `LoadState.h` for why the
|
||||
// engine's own byte can't answer both.
|
||||
//
|
||||
// Loads nest — an addon's dependencies load inside its own call — so this
|
||||
// keeps a stack rather than a single name. The name is copied rather than
|
||||
// stored by pointer: `LoadAddOn` passes a Lua string, which does not
|
||||
// outlive the call.
|
||||
|
||||
#include "addons/LoadState.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace AddOns::LoadState {
|
||||
|
||||
namespace {
|
||||
|
||||
// Dependency chains are shallow; anything deeper simply isn't tracked
|
||||
// (the depth counter still balances, so the stack can't desync).
|
||||
constexpr int kMaxDepth = 16;
|
||||
constexpr int kMaxName = 64;
|
||||
|
||||
char g_stack[kMaxDepth][kMaxName];
|
||||
int g_depth = 0;
|
||||
|
||||
bool EqualsIgnoreCase(const char *a, const char *b) {
|
||||
for (; *a != '\0' && *b != '\0'; ++a, ++b) {
|
||||
char ca = *a;
|
||||
char cb = *b;
|
||||
if (ca >= 'A' && ca <= 'Z')
|
||||
ca = static_cast<char>(ca - 'A' + 'a');
|
||||
if (cb >= 'A' && cb <= 'Z')
|
||||
cb = static_cast<char>(cb - 'A' + 'a');
|
||||
if (ca != cb)
|
||||
return false;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using LoadOne_t = unsigned int(__fastcall *)(const char *name, char flag,
|
||||
void *progressCtx);
|
||||
LoadOne_t s_loadOne_o = nullptr;
|
||||
|
||||
unsigned int __fastcall LoadOne_h(const char *name, char flag, void *progressCtx) {
|
||||
const int slot = g_depth;
|
||||
if (slot >= 0 && slot < kMaxDepth)
|
||||
Common::BoundedCopy(g_stack[slot], name, kMaxName);
|
||||
++g_depth;
|
||||
|
||||
const unsigned int result = s_loadOne_o(name, flag, progressCtx);
|
||||
|
||||
--g_depth;
|
||||
if (g_depth < 0)
|
||||
g_depth = 0; // defensive: never let the stack run negative
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookLoadOne{
|
||||
Offsets::FUN_ADDON_LOAD_ONE,
|
||||
reinterpret_cast<void *>(&LoadOne_h),
|
||||
reinterpret_cast<void **>(&s_loadOne_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsLoading(const char *name) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
const int depth = (g_depth < kMaxDepth) ? g_depth : kMaxDepth;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
if (EqualsIgnoreCase(g_stack[i], name))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace AddOns::LoadState
|
||||
@@ -0,0 +1,33 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// `AddOns::LoadState` — tracks which addons are mid-load.
|
||||
//
|
||||
// The engine's own "loaded" byte (`entry+0x18`) is set at the START of a
|
||||
// load, before the addon's files run, so on its own it can't tell
|
||||
// "loading" from "loaded". That distinction is observable: an addon's
|
||||
// file-scope code, and every dependency pulled in during it, execute
|
||||
// while the byte already reads set.
|
||||
//
|
||||
// A co-hook on the per-addon loader keeps a stack of the names currently
|
||||
// being loaded, which is the missing half of
|
||||
// `C_AddOns.IsAddOnLoaded`'s `(loadedOrLoading, loaded)` pair.
|
||||
namespace AddOns::LoadState {
|
||||
|
||||
// True while `name` is inside the engine's per-addon load call. Matching
|
||||
// is case-insensitive, as addon-name matching is everywhere else.
|
||||
bool IsLoading(const char *name);
|
||||
|
||||
} // namespace AddOns::LoadState
|
||||
+135
-15
@@ -21,7 +21,8 @@
|
||||
// tokens separated by `|` and/or whitespace, each optionally negated with a
|
||||
// leading `!`. Whole-token matching, so `RAID_PLAYER_DISPELLABLE` is not
|
||||
// mistaken for `PLAYER` and `!PLAYER` is a negation rather than a match.
|
||||
// Honored: `HELPFUL` / `HARMFUL` (descriptor slot ranges), `PLAYER` /
|
||||
// Honored: `HELPFUL` / `HARMFUL` (the aura's real polarity, read however this
|
||||
// server encodes it — see `Aura::Data::IsSlotHarmful`), `PLAYER` /
|
||||
// `!PLAYER` (caster == / != the local player, from the Aura::Source cache),
|
||||
// `DISPELLABLE` / `!DISPELLABLE` (dispel type is / isn't one a
|
||||
// dispel/purge/steal can remove — Spell.dbc Dispel ∈ Magic/Curse/Disease/
|
||||
@@ -352,22 +353,135 @@ int __fastcall Script_GetPlayerAuraBySpellID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Iterates one slot range and pushes AuraData tables into `outer` at
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a
|
||||
// follow-up call can append to the same outer table.
|
||||
// C_UnitAuras.GetAuraSlots(unit [, filter [, maxSlots [, continuationToken]]])
|
||||
// -> continuationToken, slot1, slot2, ...
|
||||
//
|
||||
// Enumerates the opaque slot ids of every aura on `unit` matching `filter`, in
|
||||
// the order the by-index getters visit them, `maxSlots` at a time (nil / 0 =
|
||||
// all). The first return is the token to pass back for the next batch, or nil
|
||||
// when this batch reached the end — retail's batching contract, and what makes
|
||||
// AuraUtil.ForEachAura linear: one enumeration per batch plus a direct by-slot
|
||||
// fetch per aura, instead of a fresh from-slot-0 walk per index. The token is
|
||||
// the 1-based ordinal to resume at; opaque to callers. Encoding of the slot ids
|
||||
// is documented at `Data::OPAQUE_STRIDE`.
|
||||
int __fastcall Script_GetAuraSlots(void *L) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
const char *filterStr = ArgOptString(L, 2);
|
||||
const int maxSlots = ArgInt(L, 3);
|
||||
const int token = ArgInt(L, 4);
|
||||
// ClassicAPI extension: a table as the 5th argument selects FILL mode —
|
||||
// the slot ids are written into it (t[1..n], stale tail cleared, t.n = n)
|
||||
// and the call returns `continuationToken, n` instead of the vararg list.
|
||||
// Lua 5.0 builds an `arg` table for every vararg CALL, so a Lua helper
|
||||
// that receives the retail multi-return allocates one table per batch;
|
||||
// filling the caller's table here is what keeps a per-frame scan
|
||||
// allocation-free (AuraUtil and pfUI's ScanAuraSlots use this form).
|
||||
const bool fill = Game::Lua::Type(L, 5) == Game::Lua::TYPE_TABLE;
|
||||
|
||||
int slots[Data::OPAQUE_SLOTS_MAX];
|
||||
int total = 0;
|
||||
if (unitToken != nullptr) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const uint64_t guid = (unit == nullptr) ? GuidForOutOfRange(unitToken) : 0;
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
total = Data::CollectSlots(unit, guid, RangeFilter(pf), pf.ToMatch(), slots,
|
||||
Data::OPAQUE_SLOTS_MAX);
|
||||
}
|
||||
int start = (token > 0) ? token - 1 : 0;
|
||||
if (start > total)
|
||||
start = total;
|
||||
int n = total - start;
|
||||
if (maxSlots > 0 && n > maxSlots)
|
||||
n = maxSlots;
|
||||
const bool more = start + n < total;
|
||||
|
||||
if (fill) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slots[start + i]));
|
||||
Game::Lua::RawSet(L, 5);
|
||||
}
|
||||
// Clear what a previous, longer fill left past n. The array is dense
|
||||
// by construction, so stop at the first hole.
|
||||
for (int k = n + 1;; ++k) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(k));
|
||||
Game::Lua::RawGet(L, 5);
|
||||
const bool hole = Game::Lua::Type(L, -1) == Game::Lua::TYPE_NIL;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (hole)
|
||||
break;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(k));
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::RawSet(L, 5);
|
||||
}
|
||||
Game::Lua::PushString(L, "n");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
Game::Lua::RawSet(L, 5);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (more)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(start + n + 1));
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Vararg form. Everything below only pushes, so the args can go; a full
|
||||
// batch is up to OPAQUE_SLOTS_MAX + 1 values, past Lua's guaranteed C-call
|
||||
// headroom.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (Game::Lua::CheckStack(L, n + 1) == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (more)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(start + n + 1));
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
for (int i = 0; i < n; ++i)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slots[start + i]));
|
||||
return 1 + n;
|
||||
}
|
||||
|
||||
// Shared body of GetAuraDataBySlot (table) / UnitAuraBySlot (positional):
|
||||
// pushes the aura an opaque slot id from GetAuraSlots names, or a single nil
|
||||
// for an id that no longer names one.
|
||||
int PushAuraBySlot(void *L, Data::Emit emit) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
if (unitToken == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int slot = ArgInt(L, 2);
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const uint64_t guid = (unit == nullptr) ? GuidForOutOfRange(unitToken) : 0;
|
||||
if (Data::PushBySlot(L, unit, guid, slot, emit))
|
||||
return (emit == Data::Emit::Positional) ? 15 : 1;
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_UnitAuras.GetAuraDataBySlot(unit, slot) -> AuraData | nil
|
||||
int __fastcall Script_GetAuraDataBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Table);
|
||||
}
|
||||
|
||||
// C_UnitAuras.UnitAuraBySlot(unit, slot) -> the 15 positional UnitAura values | nil
|
||||
// Zero-allocation sibling of GetAuraDataBySlot, namespaced under C_UnitAuras
|
||||
// like C_UnitAuras.UnitAura. What AuraUtil's non-packed iteration reads.
|
||||
int __fastcall Script_UnitAuraBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Positional);
|
||||
}
|
||||
|
||||
// Iterates the auras of one polarity (in `SlotInFilterOrder`, the same order
|
||||
// the by-index getters use) and pushes AuraData tables into `outer` at
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a follow-up
|
||||
// call can append to the same outer table.
|
||||
void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
Data::Filter filter, int &nextKey, Data::Match match) {
|
||||
const int start = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_TOTAL
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!Data::IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (!Data::MatchesAura(match, Data::IsPlayerCast(unit, slot),
|
||||
Data::ReadSpellID(unit, slot)))
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
|
||||
const int slot = Data::SlotInFilterOrder(filter, i);
|
||||
if (!Data::SlotMatchesFilter(unit, slot, filter, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
Data::Push(L, unit, slot);
|
||||
@@ -497,6 +611,12 @@ static void RegisterLuaFunctions() {
|
||||
&Script_UnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
|
||||
&Script_GetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
|
||||
+368
-147
@@ -22,6 +22,7 @@
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/IsSelfBuff.h"
|
||||
#include "time/Clock.h"
|
||||
#include "turtle/Detect.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -272,6 +273,94 @@ bool IsSlotPopulated(const uint8_t *unit, int slot) {
|
||||
return IsVisible(SpellRecord(spellID));
|
||||
}
|
||||
|
||||
// Every nibble Turtle's SetAuraFlag can write for an occupied slot: HELPFUL,
|
||||
// HELPFUL|CANCELABLE, or HARMFUL alone. Nothing else is reachable there — a
|
||||
// negative aura gets exactly HARMFUL, and a positive one always carries HELPFUL.
|
||||
bool IsTurtlePolarityNibble(uint8_t nibble) {
|
||||
return nibble == Offsets::UNIT_AURA_FLAG_HELPFUL ||
|
||||
nibble == (Offsets::UNIT_AURA_FLAG_HELPFUL | Offsets::UNIT_AURA_FLAG_CANCELABLE) ||
|
||||
nibble == Offsets::UNIT_AURA_FLAG_HARMFUL;
|
||||
}
|
||||
|
||||
// Latched for the process once a nibble proves the flags are not polarity here.
|
||||
// A server does not change its encoding mid-session, so this never resets.
|
||||
bool g_nibbleContradicted = false;
|
||||
|
||||
bool IsSlotHarmful(const uint8_t *unit, int slot) {
|
||||
if (slot < 0 || slot >= Offsets::UNIT_AURA_TOTAL)
|
||||
return false;
|
||||
|
||||
// Normally the slot range IS the polarity: an aura is written to 0..31 when
|
||||
// helpful and 32..47 when harmful, and nothing ever crosses over. The flag
|
||||
// nibble cannot be used here, because in a stock descriptor those bits mean
|
||||
// something else entirely — one bit per spell effect index that carries an
|
||||
// aura, plus a cancelable bit. Reading bit 0x08 as "harmful" would then be
|
||||
// reading "effect index 0 has an aura", which is true of nearly every aura,
|
||||
// and would report almost every BUFF as a debuff.
|
||||
if (!Turtle::Detected())
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
|
||||
// Turtle lets a debuff spill into a buff slot once the harmful range is
|
||||
// full, so the range stops being reliable — and it repurposes those same
|
||||
// nibble bits to carry the real polarity (0x04 helpful, 0x08 harmful) so
|
||||
// the information survives the spill. There, the nibble is the answer.
|
||||
if (g_nibbleContradicted)
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
|
||||
auto *desc = Descriptor(unit);
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
const uint8_t byte = *(desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS + slot / 2);
|
||||
const uint8_t nibble = (byte >> ((slot & 1) * 4)) & 0xF;
|
||||
|
||||
// Fail safe. Detection says this server SHOULD be writing polarity here, so
|
||||
// a nibble it could not have produced means that assumption is stale — the
|
||||
// server changed how it writes the flags. Fall back to the slot range for
|
||||
// good rather than keep reading bits whose meaning we no longer know: the
|
||||
// range is merely incomplete for spilled debuffs, whereas a misread nibble
|
||||
// reports nearly every buff as a debuff. Revocation only, never promotion —
|
||||
// nothing here can turn nibble reading ON, because the agreeing case is not
|
||||
// provable (an all-0x08 run is equally consistent with single-effect auras
|
||||
// under the other encoding).
|
||||
if (nibble != 0 && !IsTurtlePolarityNibble(nibble)) {
|
||||
g_nibbleContradicted = true;
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
}
|
||||
return (nibble & Offsets::UNIT_AURA_FLAG_HARMFUL) != 0;
|
||||
}
|
||||
|
||||
bool IsSlotTooltipVisible(const uint8_t *unit, int slot) {
|
||||
if (slot < 0 || slot >= Offsets::UNIT_AURA_TOTAL)
|
||||
return false;
|
||||
auto *desc = Descriptor(unit);
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
const int spellID = *reinterpret_cast<const int *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_AURA + slot * 4);
|
||||
if (spellID <= 0) // the engine's own `0 < id` test (signed)
|
||||
return false;
|
||||
const uint8_t byte = *(desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS + slot / 2);
|
||||
const uint8_t nibble = (byte >> ((slot & 1) * 4)) & 0xF;
|
||||
if ((nibble & Offsets::UNIT_AURA_VISIBLE_MASK) == 0)
|
||||
return false;
|
||||
const uint8_t *rec = SpellRecord(static_cast<uint32_t>(spellID));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
using Fn = char(__fastcall *)(const uint8_t *);
|
||||
auto fn = reinterpret_cast<Fn>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GAMETOOLTIP_AURA_VISIBLE));
|
||||
return fn(rec) != 0;
|
||||
}
|
||||
|
||||
int SlotInFilterOrder(Filter filter, int i) {
|
||||
// Harmful: 32..47 first, then 0..31 (where the server parks debuffs once
|
||||
// the 16 are full). Helpful: 0..31 then 32..47 — positive auras never
|
||||
// spill downward in practice, so that tail is a no-op safety net.
|
||||
if (filter == Filter::Harmful)
|
||||
return (i + Offsets::UNIT_AURA_BUFF_COUNT) % Offsets::UNIT_AURA_TOTAL;
|
||||
return i;
|
||||
}
|
||||
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
if (spellID == 0)
|
||||
@@ -327,6 +416,30 @@ bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID) {
|
||||
CcMatches(match.cc, spellID);
|
||||
}
|
||||
|
||||
// `MatchesAura` for a descriptor slot the caller has ALREADY established is
|
||||
// populated. The caster lookup is the expensive predicate (a scan of the
|
||||
// Aura::Source cache per slot), and `CasterMatches` ignores its answer when the
|
||||
// match has no caster restriction — so skip the lookup entirely in that case.
|
||||
static bool PopulatedSlotMatches(const uint8_t *unit, int slot,
|
||||
const Match &match) {
|
||||
const bool castByPlayer =
|
||||
match.caster != CasterMode::Any && IsPlayerCast(unit, slot);
|
||||
return MatchesAura(match, castByPlayer, ReadSpellID(unit, slot));
|
||||
}
|
||||
|
||||
bool SlotMatches(const uint8_t *unit, int slot, const Match &match) {
|
||||
return IsSlotPopulated(unit, slot) && PopulatedSlotMatches(unit, slot, match);
|
||||
}
|
||||
|
||||
bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
|
||||
const Match &match) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
return false;
|
||||
if (IsSlotHarmful(unit, slot) != (filter == Filter::Harmful))
|
||||
return false;
|
||||
return PopulatedSlotMatches(unit, slot, match);
|
||||
}
|
||||
|
||||
// Group-array analog of IsPlayerCast: the member has no descriptor, so there is
|
||||
// no slot to attribute by and the cache is consulted by (guid, spellID) alone.
|
||||
// A miss counts as "not the player" (same as IsPlayerCast).
|
||||
@@ -340,17 +453,10 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
Match match) {
|
||||
if (unit == nullptr || oneBasedIndex < 1)
|
||||
return -1;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_TOTAL
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
int matches = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
|
||||
const int slot = SlotInFilterOrder(filter, i);
|
||||
if (!SlotMatchesFilter(unit, slot, filter, match))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex)
|
||||
return slot;
|
||||
@@ -362,18 +468,16 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter, Match match) {
|
||||
if (unit == nullptr || spellID == 0)
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter != nullptr && *filter == Filter::Helpful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: Offsets::UNIT_AURA_TOTAL;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
|
||||
const int slot = (filter != nullptr) ? SlotInFilterOrder(*filter, i) : i;
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (ReadSpellID(unit, slot) != spellID)
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
if (filter != nullptr &&
|
||||
IsSlotHarmful(unit, slot) != (*filter == Filter::Harmful))
|
||||
continue;
|
||||
if (!PopulatedSlotMatches(unit, slot, match))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -384,19 +488,17 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, Match match) {
|
||||
if (unit == nullptr || spellName == nullptr || *spellName == '\0')
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter != nullptr && *filter == Filter::Helpful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: Offsets::UNIT_AURA_TOTAL;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
|
||||
const int slot = (filter != nullptr) ? SlotInFilterOrder(*filter, i) : i;
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
const char *name = LocalizedSpellName(SpellRecord(ReadSpellID(unit, slot)));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
if (filter != nullptr &&
|
||||
IsSlotHarmful(unit, slot) != (*filter == Filter::Harmful))
|
||||
continue;
|
||||
if (!PopulatedSlotMatches(unit, slot, match))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -657,7 +759,7 @@ static Attribution CachedAttribution(const Aura::Source::CachedAura &c) {
|
||||
|
||||
void Push(void *L, const uint8_t *unit, int slot, Emit emit) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
const bool isHelpful = !IsSlotHarmful(unit, slot); // nibble, not slot range
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
PushEnriched(L, UnitGuid(unit), spellID, isHelpful, ReadStacks(unit, slot),
|
||||
unitLevel, unit != nullptr && unit == LocalPlayer(), slot,
|
||||
@@ -722,36 +824,16 @@ bool DescriptorHasVisibleAura(const uint8_t *unit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Counts populated descriptor slots matching `filter` (and `caster`).
|
||||
int CountSlots(const uint8_t *unit, Filter filter, Match match) {
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
int n = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Whether cache entry `c` should be surfaced as a fallback for `unit` under
|
||||
// `filter`/`caster`: the spell must be a user-visible aura, it must not
|
||||
// already be in a populated descriptor slot (no double-listing the same live
|
||||
// aura) and, when player-filtered, must have been cast by us. Superseded /
|
||||
// dispelled auras are kept out of the cache by `Aura::Source`'s removal
|
||||
// eviction, not filtered here.
|
||||
// Whether cache entry `c` may surface as a fallback for `unit` under `filter`:
|
||||
// the spell must be a user-visible aura and must not already be in a populated
|
||||
// descriptor slot (no double-listing the same live aura). The caller's
|
||||
// caster/dispel/CC predicates are applied separately by `CachedMatches`.
|
||||
// Superseded / dispelled auras are kept out of the cache by `Aura::Source`'s
|
||||
// removal eviction, not filtered here.
|
||||
bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
Filter filter, Match match) {
|
||||
Filter filter) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false; // hidden/internal aura (e.g. SPELL_ATTR_HIDDEN_CLIENTSIDE):
|
||||
// the descriptor path drops these via IsSlotPopulated, so
|
||||
@@ -759,37 +841,58 @@ bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
// they leak through as tooltip-less phantom buffs.
|
||||
if (FindSlotBySpellID(unit, c.spellId, &filter, Match{}) >= 0)
|
||||
return false; // still in the descriptor — returned via the slot path
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Applies `match` to a cache entry. The entry carries its caster, so the caster
|
||||
// test is a plain GUID compare — no scan, nothing to defer.
|
||||
bool CachedMatches(const Aura::Source::CachedAura &c, const Match &match) {
|
||||
return MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(),
|
||||
c.spellId);
|
||||
}
|
||||
|
||||
// The eligible cache-fallback entries for `unit` under `filter`, in `Enumerate`
|
||||
// order, unfiltered by any `Match` (callers apply `CachedMatches`). Applies the
|
||||
// fallback gates: reconcile the cache against the descriptor, then yield nothing
|
||||
// while the descriptor shows any visible aura (it is authoritative then — see
|
||||
// DescriptorHasVisibleAura). `buf` must hold kFallbackMax entries. Returns the
|
||||
// count. An entry's ordinal here is what an opaque cache slot id encodes, which
|
||||
// is why the sequence must not depend on the caller's match.
|
||||
int EligibleFallbacks(const uint8_t *unit, Filter filter,
|
||||
Aura::Source::CachedAura *buf) {
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return 0; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
Aura::Source::CachedAura raw[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, raw, kFallbackMax);
|
||||
int m = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (FallbackEligible(unit, raw[i], filter))
|
||||
buf[m++] = raw[i];
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, Match match, Emit emit) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return false; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
// The descriptor held `descCount` matches; the caller already found fewer
|
||||
// than `oneBasedIndex` there, so the cache supplies index
|
||||
// `oneBasedIndex - descCount` (1-based) of the entries it didn't.
|
||||
const int target = oneBasedIndex - CountSlots(unit, filter, match);
|
||||
if (target < 1)
|
||||
return false;
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return false;
|
||||
// The fallback yields only while the descriptor shows no visible aura, so
|
||||
// the descriptor held none of the caller's matches: the cache supplies index
|
||||
// `oneBasedIndex` (1-based) of the entries passing `match` directly.
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
const int n = EligibleFallbacks(unit, filter, buf);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
if (++seen == target) {
|
||||
if (++seen == oneBasedIndex) {
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful, emit);
|
||||
return true;
|
||||
}
|
||||
@@ -799,19 +902,10 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
Match match, int outerIdx, int &nextKey) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return;
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
const int n = EligibleFallbacks(unit, filter, buf);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
@@ -823,8 +917,10 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
|
||||
namespace {
|
||||
|
||||
// Absolute slot range [start, end) for a filter, matching the descriptor
|
||||
// convention (helpful 0..31, harmful 32..47).
|
||||
// Absolute slot range [start, end) for a filter in the GROUP ARRAY (helpful
|
||||
// 0..31, harmful 32..47). The array carries spell IDs only — no flag nibble —
|
||||
// so the seating range is the only polarity the out-of-range path has. (The
|
||||
// descriptor path reads the nibble instead; see SlotInFilterOrder.)
|
||||
void GroupRange(Filter filter, int &start, int &end) {
|
||||
start = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_BUFF_COUNT : 0;
|
||||
end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
|
||||
@@ -838,21 +934,27 @@ uint32_t GroupSpellID(const uint16_t *arr, int slot) {
|
||||
return arr[slot];
|
||||
}
|
||||
|
||||
// Whether the group-array slot holds a surfacable aura for this query. Mirrors
|
||||
// the engine's per-slot test in Script_UnitBuff/UnitDebuff — a non-zero spell
|
||||
// ID with a real Spell.dbc record that passes the visibility predicate (the
|
||||
// server only transmits *applied* auras, so there is no flag nibble to check).
|
||||
// `caster` additionally requires the `Aura::Source` cache to attribute the
|
||||
// cast to the local player (a miss excludes it — we can't confirm the caster
|
||||
// from the group array alone).
|
||||
bool GroupSlotEligible(uint64_t guid, const uint16_t *arr, int slot,
|
||||
Match match) {
|
||||
// Whether the group-array slot holds a surfacable aura. Mirrors the engine's
|
||||
// per-slot test in Script_UnitBuff/UnitDebuff — a non-zero spell ID with a real
|
||||
// Spell.dbc record that passes the visibility predicate (the server only
|
||||
// transmits *applied* auras, so there is no flag nibble to check).
|
||||
bool GroupSlotVisible(const uint16_t *arr, int slot) {
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
if (id == 0 || !IsVisible(SpellRecord(id)))
|
||||
return id != 0 && IsVisible(SpellRecord(id));
|
||||
}
|
||||
|
||||
// `GroupSlotVisible` plus the caller's `match` predicates. The caster answer
|
||||
// (`GroupIsPlayerCast` — a cache scan; a miss counts as not-player, since the
|
||||
// array alone can't confirm the caster) is computed only when `match` restricts
|
||||
// on caster.
|
||||
bool GroupSlotMatches(uint64_t guid, const uint16_t *arr, int slot,
|
||||
const Match &match) {
|
||||
if (!GroupSlotVisible(arr, slot))
|
||||
return false;
|
||||
if (!MatchesAura(match, GroupIsPlayerCast(guid, id), id))
|
||||
return false;
|
||||
return true;
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
const bool castByPlayer =
|
||||
match.caster != CasterMode::Any && GroupIsPlayerCast(guid, id);
|
||||
return MatchesAura(match, castByPlayer, id);
|
||||
}
|
||||
|
||||
// The groupmate's level, for Spell.dbc duration scaling (0 = skip). Cheap
|
||||
@@ -872,28 +974,52 @@ int GroupMemberLevel(uint64_t guid) {
|
||||
// still sit in the cache from when we observed them in range.
|
||||
bool GroupArrayHasVisibleAura(const uint16_t *arr) {
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
if (id != 0 && IsVisible(SpellRecord(id)))
|
||||
if (GroupSlotVisible(arr, slot))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether cache entry `c` may supplement the (empty) group array: a visible
|
||||
// aura, not already present in the array, and — when player-filtered — cast by
|
||||
// us. See GroupArrayHasVisibleAura for when this path runs.
|
||||
bool GroupFallbackEligible(const uint16_t *arr, const Aura::Source::CachedAura &c,
|
||||
Match match) {
|
||||
// aura not already present in the array. The caller's `match` is applied
|
||||
// separately by `CachedMatches`. See EligibleGroupFallbacks for when this path
|
||||
// runs.
|
||||
bool GroupFallbackEligible(const uint16_t *arr,
|
||||
const Aura::Source::CachedAura &c) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot)
|
||||
if (GroupSpellID(arr, slot) == c.spellId)
|
||||
return false;
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The eligible group cache-fallback entries for out-of-range member `guid`, in
|
||||
// `Enumerate` order, unfiltered by any `Match` (callers apply `CachedMatches`).
|
||||
// Yields nothing while the group array exposes ANY visible aura: the server's
|
||||
// out-of-range aura data is then present and authoritative, mirroring how the
|
||||
// descriptor path treats a populated descriptor (DescriptorHasVisibleAura).
|
||||
// Only an entirely empty array falls back to the cache — the server resends
|
||||
// out-of-range stats as deltas only, so a member seen in range and then moved
|
||||
// away again leaves an empty party-stats aura array (the array is cleared when
|
||||
// they become visible and never re-sent), yet their auras still sit in the
|
||||
// cache from when we observed them in range. `buf` must hold kFallbackMax
|
||||
// entries. An entry's ordinal here is what an opaque group-cache slot id
|
||||
// encodes, which is why the sequence must not depend on the caller's match.
|
||||
int EligibleGroupFallbacks(const uint16_t *arr, uint64_t guid, bool harmful,
|
||||
Aura::Source::CachedAura *buf) {
|
||||
if (arr == nullptr || GroupArrayHasVisibleAura(arr))
|
||||
return 0;
|
||||
Aura::Source::CachedAura raw[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(guid, harmful, raw, kFallbackMax);
|
||||
int m = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (GroupFallbackEligible(arr, raw[i]))
|
||||
buf[m++] = raw[i];
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// Nth (1-based) cache-fallback aura of `filter`'s kind for an out-of-range
|
||||
// member whose group array is empty. Pushes the AuraData table on a hit.
|
||||
bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
@@ -901,10 +1027,10 @@ bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
Emit emit = Emit::Table) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
EligibleGroupFallbacks(arr, guid, filter == Filter::Harmful, buf);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
if (++seen == oneBasedIndex) {
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
@@ -922,10 +1048,10 @@ void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
int &nextKey) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
EligibleGroupFallbacks(arr, guid, filter == Filter::Harmful, buf);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
@@ -941,7 +1067,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
bool harmful, uint32_t spellID,
|
||||
const char *spellName, Match match) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(guid, harmful, buf, kFallbackMax);
|
||||
const int n = EligibleGroupFallbacks(arr, guid, harmful, buf);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (spellID != 0 && buf[i].spellId != spellID)
|
||||
continue;
|
||||
@@ -950,7 +1076,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
if (nm == nullptr || std::strcmp(nm, spellName) != 0)
|
||||
continue;
|
||||
}
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
PushEnriched(L, guid, buf[i].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
@@ -974,7 +1100,7 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
GroupRange(filter, start, end);
|
||||
int matches = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex) {
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -985,10 +1111,8 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
}
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter,
|
||||
match, emit);
|
||||
return false;
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter, match,
|
||||
emit);
|
||||
}
|
||||
|
||||
bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
@@ -1008,23 +1132,20 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (GroupSpellID(arr, slot) != spellID)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, spellID, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false, Aura::Source::SLOT_UNBOUND);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, spellID, nullptr,
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, spellID, nullptr,
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, spellID, nullptr, match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, spellID, nullptr, match))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1049,23 +1170,20 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const char *name = LocalizedSpellName(SpellRecord(id));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, id, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false, Aura::Source::SLOT_UNBOUND);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, 0, spellName,
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, 0, spellName,
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, 0, spellName, match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, 0, spellName, match))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1081,7 +1199,7 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
GroupRange(filter, start, end);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -1090,9 +1208,112 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, match, outerIdx,
|
||||
nextKey);
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, match, outerIdx, nextKey);
|
||||
}
|
||||
|
||||
// ── Opaque aura slots ──────────────────────────────────────────────────────
|
||||
|
||||
int CollectSlots(const uint8_t *unit, uint64_t guid, Filter filter, Match match,
|
||||
int *out, int cap) {
|
||||
int n = 0;
|
||||
if (unit != nullptr) {
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL && n < cap; ++i) {
|
||||
const int slot = SlotInFilterOrder(filter, i);
|
||||
if (SlotMatchesFilter(unit, slot, filter, match))
|
||||
out[n++] = slot;
|
||||
}
|
||||
// The cache fallback yields only while the descriptor shows no visible
|
||||
// aura, so these never interleave with live slots — they follow them,
|
||||
// exactly as the by-index getters order them.
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int m = EligibleFallbacks(unit, filter, buf);
|
||||
const int base = (filter == Filter::Harmful) ? OPAQUE_CACHE_HARMFUL
|
||||
: OPAQUE_CACHE_HELPFUL;
|
||||
for (int k = 0; k < m && n < cap; ++k) {
|
||||
if (CachedMatches(buf[k], match))
|
||||
out[n++] = base + k;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
// No live CGUnit — the out-of-range group path.
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return 0;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
int start, end;
|
||||
GroupRange(filter, start, end);
|
||||
for (int slot = start; slot < end && n < cap; ++slot) {
|
||||
if (GroupSlotMatches(guid, arr, slot, match))
|
||||
out[n++] = OPAQUE_GROUP + slot;
|
||||
}
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int m = EligibleGroupFallbacks(arr, guid, filter == Filter::Harmful, buf);
|
||||
const int base = (filter == Filter::Harmful) ? OPAQUE_GROUP_CACHE_HARMFUL
|
||||
: OPAQUE_GROUP_CACHE_HELPFUL;
|
||||
for (int k = 0; k < m && n < cap; ++k) {
|
||||
if (CachedMatches(buf[k], match))
|
||||
out[n++] = base + k;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
bool PushBySlot(void *L, const uint8_t *unit, uint64_t guid, int slot, Emit emit) {
|
||||
if (slot < 0)
|
||||
return false;
|
||||
const int kind = slot / OPAQUE_STRIDE;
|
||||
const int k = slot % OPAQUE_STRIDE;
|
||||
switch (kind) {
|
||||
case 0: // live descriptor slot
|
||||
if (unit == nullptr || !IsSlotPopulated(unit, k))
|
||||
return false;
|
||||
Push(L, unit, k, emit);
|
||||
return true;
|
||||
case 1: // helpful cache fallback
|
||||
case 2: { // harmful cache fallback
|
||||
if (unit == nullptr || k >= kFallbackMax)
|
||||
return false;
|
||||
const Filter filter = (kind == 2) ? Filter::Harmful : Filter::Helpful;
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
if (k >= EligibleFallbacks(unit, filter, buf))
|
||||
return false;
|
||||
PushFromCache(L, unit, buf[k], filter == Filter::Helpful, emit);
|
||||
return true;
|
||||
}
|
||||
case 3: { // out-of-range group-array slot
|
||||
if (guid == 0 || k >= Offsets::UNIT_AURA_TOTAL)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return false;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
if (!GroupSlotVisible(arr, k))
|
||||
return false;
|
||||
PushEnriched(L, guid, GroupSpellID(arr, k),
|
||||
k < Offsets::UNIT_AURA_BUFF_COUNT, 1, GroupMemberLevel(guid),
|
||||
false, Aura::Source::SLOT_UNBOUND, /*known*/ nullptr, emit);
|
||||
return true;
|
||||
}
|
||||
case 4: // helpful group cache fallback
|
||||
case 5: { // harmful group cache fallback
|
||||
if (guid == 0 || k >= kFallbackMax)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return false;
|
||||
const bool harmful = (kind == 5);
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
if (k >= EligibleGroupFallbacks(arr, guid, harmful, buf))
|
||||
return false;
|
||||
const Attribution a = CachedAttribution(buf[k]);
|
||||
PushEnriched(L, guid, buf[k].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
false, buf[k].slot, &a, emit);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+104
-13
@@ -10,6 +10,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared aura-table primitives consumed by `C_UnitAuras.*` (and any
|
||||
@@ -18,19 +20,38 @@
|
||||
// `unit + OFF_CGUNIT_OBJECT_FIELDS`; the layout is documented in
|
||||
// `Offsets.h` under `OFF_UNIT_FIELD_AURA*`.
|
||||
//
|
||||
// Slot indexing convention used here matches the engine's:
|
||||
// Slots are absolute 0..47, and how polarity is read out of them DEPENDS ON THE
|
||||
// SERVER. `IsSlotHarmful` owns that decision; nothing else should look at either
|
||||
// signal directly.
|
||||
//
|
||||
// slot 0..31 → buffs (helpful)
|
||||
// slot 32..47 → debuffs (harmful)
|
||||
// Normally the slot range IS the polarity: positive auras are written to 0..31
|
||||
// and negative ones to 32..47 by a strict either/or, so nothing crosses over.
|
||||
// The flag nibble is no help — in a stock descriptor those bits record which
|
||||
// spell effect indices carry an aura, not whether the aura is good or bad.
|
||||
//
|
||||
// Callers that take a 1-based Lua index into "buffs" or "debuffs"
|
||||
// translate to the absolute 0..47 slot before calling in.
|
||||
// Turtle changes both halves. Once the 16 harmful slots are full it spills
|
||||
// further debuffs into 0..31 (setting UNIT_FLAG_AURAS_VISIBLE so the client
|
||||
// still renders them), which makes the range unreliable — and it repurposes the
|
||||
// nibble to carry the true polarity so the information survives the spill.
|
||||
// There, the nibble is authoritative. See `Offsets::UNIT_AURA_FLAG_HARMFUL`.
|
||||
//
|
||||
// Either way `Filter` selects on the aura's REAL polarity, so a spilled debuff
|
||||
// is reported as a debuff. The engine's own UnitBuff/UnitDebuff split purely by
|
||||
// slot range and would call it a buff; that is a 1.12 UI limitation we
|
||||
// deliberately do not mirror, since retail's contract is actual polarity. A
|
||||
// polarity walk visits its home range first and then the other range, so
|
||||
// indices are unchanged wherever no spill has happened — which is everywhere,
|
||||
// on a server that does not spill.
|
||||
//
|
||||
// Callers that take a 1-based Lua index into "buffs" or "debuffs" translate
|
||||
// to the absolute 0..47 slot before calling in.
|
||||
|
||||
namespace Aura::Data {
|
||||
|
||||
// Filter for slot iteration. The engine's `UnitBuff` / `UnitDebuff`
|
||||
// pick one direction; modern `C_UnitAuras.GetAuraDataByIndex` defaults
|
||||
// to helpful when no filter is specified.
|
||||
// Filter for slot iteration: the aura's polarity per its flag nibble (see the
|
||||
// file header). The engine's `UnitBuff` / `UnitDebuff` pick one direction;
|
||||
// modern `C_UnitAuras.GetAuraDataByIndex` defaults to helpful when no filter
|
||||
// is specified.
|
||||
enum class Filter { Helpful, Harmful };
|
||||
|
||||
// How a resolved aura is emitted onto the Lua stack by the single-aura push
|
||||
@@ -83,11 +104,33 @@ uint32_t ReadSpellID(const uint8_t *unit, int slot);
|
||||
// to decide whether to surface an aura through Lua.
|
||||
bool IsSlotPopulated(const uint8_t *unit, int slot);
|
||||
|
||||
// Finds the absolute slot of the `oneBasedIndex`-th populated aura
|
||||
// matching `filter`. Returns -1 if no such aura. Walks
|
||||
// 0..UNIT_AURA_BUFF_COUNT-1 for Helpful, UNIT_AURA_BUFF_COUNT..
|
||||
// UNIT_AURA_TOTAL-1 for Harmful. When `playerOnly` is true, only auras
|
||||
// the local player cast (per `IsPlayerCast`) are counted/returned.
|
||||
// The aura's real polarity, read the way this server encodes it: the slot range
|
||||
// normally, the flag nibble on Turtle (see the file header). False for an empty
|
||||
// slot or a null unit. This is what `Filter` selects on, and the ONE place the
|
||||
// two encodings are chosen between.
|
||||
bool IsSlotHarmful(const uint8_t *unit, int slot);
|
||||
|
||||
// The engine's TOOLTIP visibility gate for a slot: occupied, visible nibble,
|
||||
// and the spell passes `FUN_GAMETOOLTIP_AURA_VISIBLE` — exactly what
|
||||
// `SetUnitBuff` / `SetUnitDebuff` count while turning an index into a slot.
|
||||
// Differs from `IsSlotPopulated` (the UnitBuff gate), so use THIS to compute
|
||||
// the index the engine's tooltip methods expect for a given slot.
|
||||
bool IsSlotTooltipVisible(const uint8_t *unit, int slot);
|
||||
|
||||
// The i-th slot (0..UNIT_AURA_TOTAL-1) in the order a `filter` walk visits the
|
||||
// descriptor: the polarity's home range first (helpful 0..31, harmful 32..47),
|
||||
// then the other range, where spilled debuffs live. Every enumeration that
|
||||
// hands out indices or slot lists uses this so they agree on order.
|
||||
int SlotInFilterOrder(Filter filter, int i);
|
||||
|
||||
// `SlotMatches` plus the polarity test: populated, of `filter`'s polarity, and
|
||||
// passing `match`.
|
||||
bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
|
||||
const Match &match);
|
||||
|
||||
// Finds the absolute slot of the `oneBasedIndex`-th populated aura of
|
||||
// `filter`'s polarity that passes `match`. Returns -1 if no such aura. Walks
|
||||
// all 48 slots in `SlotInFilterOrder`.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
Match match = {});
|
||||
|
||||
@@ -124,6 +167,12 @@ bool IsDispellable(uint32_t spellID);
|
||||
// per-aura "was cast by the local player" answer and its spell ID.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID);
|
||||
|
||||
// True iff descriptor `slot` on `unit` is populated (per `IsSlotPopulated`) AND
|
||||
// passes `match`. The caster answer (`IsPlayerCast` — a scan of the
|
||||
// `Aura::Source` cache, ~1000 entries in a raid) is computed ONLY when `match`
|
||||
// restricts on caster, so the common filter-less walk never pays for it.
|
||||
bool SlotMatches(const uint8_t *unit, int slot, const Match &match);
|
||||
|
||||
// Display stack count for the given slot (engine stores stacks-1,
|
||||
// we add 1). Returns 0 if the unit is null.
|
||||
int ReadStacks(const uint8_t *unit, int slot);
|
||||
@@ -230,4 +279,46 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
int outerIdx, int &nextKey);
|
||||
|
||||
// ── Opaque aura slots (GetAuraSlots / GetAuraDataBySlot) ────────────────────
|
||||
//
|
||||
// The by-index getters re-walk the descriptor from slot 0 on every call, so a
|
||||
// full scan through them is quadratic in the aura count. `CollectSlots`
|
||||
// enumerates once into opaque slot ids and `PushBySlot` fetches one aura by id
|
||||
// with no walk — the modern `C_UnitAuras.GetAuraSlots` / `GetAuraDataBySlot`
|
||||
// batching contract. An id is valid for the frame it was enumerated in.
|
||||
//
|
||||
// Encoding (kind = id / OPAQUE_STRIDE, k = id % OPAQUE_STRIDE):
|
||||
// kind 0 live descriptor slot k (0..UNIT_AURA_TOTAL-1)
|
||||
// kind 1 k-th eligible helpful cache-fallback entry (OPAQUE_CACHE_HELPFUL)
|
||||
// kind 2 k-th eligible harmful cache-fallback entry (OPAQUE_CACHE_HARMFUL)
|
||||
// kind 3 out-of-range group-array slot k (0..47) (OPAQUE_GROUP)
|
||||
// kind 4 k-th helpful group cache-fallback entry (OPAQUE_GROUP_CACHE_HELPFUL)
|
||||
// kind 5 k-th harmful group cache-fallback entry (OPAQUE_GROUP_CACHE_HARMFUL)
|
||||
// Fallback ordinals count the UNFILTERED eligible sequence (visible, not
|
||||
// already in the live array), so `PushBySlot` needs no filter; `CollectSlots`
|
||||
// applies the caller's `match` only when choosing which ids to emit.
|
||||
constexpr int OPAQUE_STRIDE = 0x100;
|
||||
constexpr int OPAQUE_CACHE_HELPFUL = 1 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_CACHE_HARMFUL = 2 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP = 3 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP_CACHE_HELPFUL = 4 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP_CACHE_HARMFUL = 5 * OPAQUE_STRIDE;
|
||||
|
||||
// Upper bound on ids one `CollectSlots` call can yield: the live array (or the
|
||||
// group array) plus its cache fallback, each at most UNIT_AURA_TOTAL entries.
|
||||
constexpr int OPAQUE_SLOTS_MAX = 2 * Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Writes the opaque ids of every aura on the unit matching `filter` + `match`
|
||||
// into `out` (at most `cap`), in the order the by-index getters visit them:
|
||||
// live slots (or, with no live CGUnit, the group array) first, then the cache
|
||||
// fallback. `unit` null selects the out-of-range group path for `guid`. Returns
|
||||
// the count written.
|
||||
int CollectSlots(const uint8_t *unit, uint64_t guid, Filter filter, Match match,
|
||||
int *out, int cap);
|
||||
|
||||
// Pushes the aura an opaque id names — table or positional per `emit` — and
|
||||
// returns true; pushes nothing and returns false for an id that names nothing
|
||||
// (stale, malformed, or the wrong path for the unit's current state).
|
||||
bool PushBySlot(void *L, const uint8_t *unit, uint64_t guid, int slot, Emit emit);
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+143
-17
@@ -360,6 +360,12 @@ Entry *Claim(uint32_t now) {
|
||||
// refresh path too.
|
||||
void SignalAuraChanged(uint64_t guid);
|
||||
|
||||
// Defined with the duration-modifier machinery below. A trigger can CREATE the
|
||||
// aura it then edits, in which case the edit is armed at trigger time and
|
||||
// applied here, when the aura it was waiting for actually lands. See PendingMod.
|
||||
struct Entry;
|
||||
void ConsumePendingMods(Entry &e, uint32_t now);
|
||||
|
||||
// The SpellGo hook: authoritative caster + caster-modified (talented) timing.
|
||||
// Identity is `(target, spell, caster)`, so a second caster of the same spell
|
||||
// opens its own entry instead of overwriting the first's, and a recast by the
|
||||
@@ -387,6 +393,7 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
|
||||
now, SLOT_UNBOUND, KIND_UNKNOWN, true};
|
||||
ConsumePendingMods(*e, now);
|
||||
return;
|
||||
}
|
||||
e->stampMs = now; // refresh EvictAbsent grace on any touch
|
||||
@@ -399,6 +406,7 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
// ourselves (deferred to the world tick, coalesced). A NEW application
|
||||
// doesn't need this: its descriptor write fires the engine's own event.
|
||||
SignalAuraChanged(targetGuid);
|
||||
ConsumePendingMods(*e, now);
|
||||
}
|
||||
|
||||
// The OnAuraAdded / OnAuraStacksChanged application hooks: a descriptor slot,
|
||||
@@ -429,6 +437,7 @@ void StoreFromApplication(uint64_t targetGuid, uint32_t spellId,
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
static_cast<int16_t>(slot), kind, true};
|
||||
ConsumePendingMods(*e, now);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -440,6 +449,11 @@ void StoreFromApplication(uint64_t targetGuid, uint32_t spellId,
|
||||
// back to unknown.
|
||||
if (kind != KIND_UNKNOWN)
|
||||
e->kind = kind;
|
||||
// An armed server edit applies whoever owns the timing — the server made it
|
||||
// AFTER the cast this entry came from, so it is the newer truth. It also
|
||||
// adopts the entry, which makes the ownership guard below keep the edited
|
||||
// value instead of the application's base-duration guess.
|
||||
ConsumePendingMods(*e, now);
|
||||
if (e->casterGuid != 0)
|
||||
return; // SpellGo owns this entry; keep its caster + talented timing
|
||||
if (casterGuid != 0)
|
||||
@@ -523,8 +537,10 @@ void Evict(uint64_t targetGuid, uint32_t spellId, int slot) {
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder → reset to max)
|
||||
// A trigger is matched either by exact spellID (from the server's script
|
||||
// binding, stable across ranks) or by SpellFamilyName + school; 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
|
||||
// aura by SpellFamilyName plus a family-flag overlap and/or a SpellIconID —
|
||||
// rank-proof, exactly how the server's scripts find it, and the icon half is
|
||||
// what reaches a custom spell with no family flags at all (see
|
||||
// AffectedMatchesRaw). 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
|
||||
@@ -549,9 +565,23 @@ int g_modCount = 0;
|
||||
|
||||
// The affected-aura selector alone, shared with the Lua-facing by-family
|
||||
// refresh so the two cannot disagree about what they match.
|
||||
//
|
||||
// SpellFamilyName always has to match. Past that a rule discriminates by a
|
||||
// SpellFamilyFlags overlap, by SpellIconID, or by both; a zero mask or a zero
|
||||
// icon drops that half of the test. The icon-only form is what names an aura
|
||||
// whose SpellFamilyFlags are ZERO — every one of Turtle's custom spells is
|
||||
// (308 in the hunter family alone), so the mask cannot select them while the
|
||||
// icon still can, and an icon is shared by a spell's whole rank ladder, which
|
||||
// keeps it rank-proof exactly like a family flag. Registrars reject the
|
||||
// degenerate mask-0 + icon-0 rule, which would match a whole class family.
|
||||
bool AffectedMatchesRaw(const uint8_t *rec, uint32_t family, uint64_t mask,
|
||||
uint32_t icon) {
|
||||
if (!Spell::Lookup::IsFitToFamily(rec, family, mask))
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != family)
|
||||
return false;
|
||||
if (mask != 0 && !Spell::Lookup::IsFitToFamily(rec, family, mask))
|
||||
return false;
|
||||
if (icon != 0 &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
@@ -616,8 +646,8 @@ void FlushAuraSignals() {
|
||||
g_pendingSignalCount = 0;
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
switch (m.op) {
|
||||
void ApplyModOp(Entry &e, int32_t op, int32_t valueMs, uint32_t now) {
|
||||
switch (op) {
|
||||
case MOD_REFRESH:
|
||||
if (e.durationMs > 0) {
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → reset to max
|
||||
@@ -625,14 +655,14 @@ void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
}
|
||||
break;
|
||||
case MOD_SET:
|
||||
e.durationMs = static_cast<uint32_t>(m.valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(m.valueMs);
|
||||
e.durationMs = static_cast<uint32_t>(valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
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);
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
} else {
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
@@ -645,6 +675,88 @@ void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
ApplyModOp(e, m.op, m.valueMs, now);
|
||||
}
|
||||
|
||||
// ---- Deferred duration edits (the trigger creates the aura it edits) ------
|
||||
//
|
||||
// A trigger does not always act on an aura that is ALREADY there. Bestial
|
||||
// Wrath's effect 0 is TRIGGER_SPELL(Scent of Blood), and the server's script
|
||||
// then stretches the very holder that trigger just created (tortoise-wow
|
||||
// spell_hunter_bestial_wrath). The trigger's own SMSG_SPELL_GO reaches us
|
||||
// first, so at trigger time either nothing matches yet, or something matches
|
||||
// and is overwritten moments later when the triggered cast stores its own base
|
||||
// duration. Either way the edit is lost.
|
||||
//
|
||||
// So a trigger also ARMS its targets, and the next matching store inside the
|
||||
// window re-applies the edit — from the cast path or the application path,
|
||||
// whichever the triggered aura arrives on.
|
||||
//
|
||||
// Only the IDEMPOTENT ops arm. Re-running refresh or set lands on the same
|
||||
// value, while reduce would shave a second time and remove would kill an aura
|
||||
// that was legitimately re-applied. That keeps Conflagrate (reduce) behaving
|
||||
// exactly as before.
|
||||
bool OpIsIdempotent(int32_t op) {
|
||||
return op == MOD_REFRESH || op == MOD_SET;
|
||||
}
|
||||
|
||||
struct PendingMod {
|
||||
uint64_t target;
|
||||
uint64_t caster;
|
||||
uint32_t affectedFamily;
|
||||
uint64_t affectedMask;
|
||||
uint32_t affectedIcon;
|
||||
int32_t op;
|
||||
int32_t valueMs;
|
||||
uint32_t untilMs;
|
||||
bool used;
|
||||
};
|
||||
// The triggered cast follows its trigger within one packet burst, so the window
|
||||
// only has to cover network jitter. Sized like the other arm tables.
|
||||
constexpr int kPendingModMax = 8;
|
||||
constexpr uint32_t kPendingModTtlMs = 2000;
|
||||
PendingMod g_pendingMods[kPendingModMax];
|
||||
|
||||
void ArmPendingMod(uint64_t target, uint64_t caster, const DurationMod &m,
|
||||
uint32_t now) {
|
||||
PendingMod *slot = nullptr;
|
||||
for (auto &p : g_pendingMods) {
|
||||
// Re-arming the same rule on the same target refreshes it in place.
|
||||
if (p.used && p.target == target && p.affectedFamily == m.affectedFamily &&
|
||||
p.affectedMask == m.affectedMask && p.affectedIcon == m.affectedIcon) {
|
||||
slot = &p;
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr && (!p.used || Time::Clock::Reached(now, p.untilMs)))
|
||||
slot = &p;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_pendingMods[0]; // all live — steal the first
|
||||
*slot = {target, caster, m.affectedFamily, m.affectedMask,
|
||||
m.affectedIcon, m.op, m.valueMs, now + kPendingModTtlMs, true};
|
||||
}
|
||||
|
||||
void ConsumePendingMods(Entry &e, uint32_t now) {
|
||||
for (auto &p : g_pendingMods) {
|
||||
if (!p.used || p.target != e.targetGuid ||
|
||||
Time::Clock::Reached(now, p.untilMs))
|
||||
continue;
|
||||
// Caster-scoped exactly like the immediate path, adoption included.
|
||||
if (e.casterGuid != 0 && e.casterGuid != p.caster)
|
||||
continue;
|
||||
if (!AffectedMatchesRaw(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId)),
|
||||
p.affectedFamily, p.affectedMask, p.affectedIcon))
|
||||
continue;
|
||||
if (e.casterGuid == 0)
|
||||
e.casterGuid = p.caster;
|
||||
ApplyModOp(e, p.op, p.valueMs, now);
|
||||
p.used = false; // one-shot, like the server's single-holder edit
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -681,6 +793,11 @@ void ApplyDurationModifiers(uint32_t triggerSpellId, uint64_t caster,
|
||||
ApplyMod(e, m, now);
|
||||
break; // one matching aura per (rule, target), like the server
|
||||
}
|
||||
// The aura may not be here yet, or may be about to be overwritten by
|
||||
// the cast this trigger sets off. Arm the idempotent ops so the edit
|
||||
// lands when it arrives — see PendingMod.
|
||||
if (OpIsIdempotent(m.op))
|
||||
ArmPendingMod(targets[t], caster, m, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,8 +806,11 @@ 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 ||
|
||||
// Trigger must be identified one way or the other, and the affected aura
|
||||
// needs at least one discriminator past its family — a flag overlap, an
|
||||
// icon, or both. Neither would match every aura of a class.
|
||||
if ((triggerSpellId == 0 && triggerFamily == 0) ||
|
||||
(affectedMask == 0 && affectedIcon == 0) ||
|
||||
op < MOD_REFRESH || op > MOD_REMOVE)
|
||||
return false;
|
||||
for (int i = 0; i < g_modCount; ++i) { // replace an identical rule
|
||||
@@ -1395,10 +1515,14 @@ const Net::PacketDispatch::AutoSubscribe _spellGoSub{&SpellGoSub};
|
||||
|
||||
// ---- Aura-application co-hooks (timing for proc / triggered auras) -------
|
||||
|
||||
// Classify by the absolute aura slot: 0..BUFF_COUNT-1 = buff (helpful),
|
||||
// BUFF_COUNT..TOTAL-1 = debuff (harmful).
|
||||
int8_t KindForSlot(int slot) {
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
// Defer to `Aura::Data::IsSlotHarmful` rather than testing the range here — how
|
||||
// polarity is encoded differs between servers, and that function is the one
|
||||
// place that decides. The descriptor write pass has already landed both the
|
||||
// slot and its flags by the time the application hooks fire.
|
||||
int8_t KindForSlot(const void *unit, int slot) {
|
||||
return Aura::Data::IsSlotHarmful(static_cast<const uint8_t *>(unit), slot)
|
||||
? KIND_HARMFUL
|
||||
: KIND_HELPFUL;
|
||||
}
|
||||
|
||||
// Stamp expiration for an aura that just landed/refreshed in `slot` on `unit`.
|
||||
@@ -1454,7 +1578,7 @@ void StampApplication(void *unit, uint32_t spellId, int slot) {
|
||||
}
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
StoreFromApplication(unitGuid, spellId, caster, expirationMs, durationMs,
|
||||
slot, KindForSlot(slot));
|
||||
slot, KindForSlot(unit, slot));
|
||||
}
|
||||
|
||||
// Bump the player-stat-inputs signal when an aura change hits the LOCAL
|
||||
@@ -1641,7 +1765,9 @@ int RefreshJudgements(uint64_t unitGuid, uint64_t attackerGuid) {
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid) {
|
||||
if (unitGuid == 0 || mask == 0 || casterGuid == 0)
|
||||
// Same selector invariant the rule registrar enforces: family plus at
|
||||
// least one of flag-overlap / icon.
|
||||
if (unitGuid == 0 || (mask == 0 && icon == 0) || casterGuid == 0)
|
||||
return 0;
|
||||
const uint32_t now = NowMs();
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
|
||||
+9
-6
@@ -61,10 +61,11 @@ bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// Refreshes `casterGuid`'s aura on `unitGuid` matching the same selector the
|
||||
// duration rules use — SpellFamilyName + family-flag overlap + optional icon
|
||||
// (0 = any) — to a full duration from now. Returns the spellID refreshed, or 0
|
||||
// if nothing matched. For a mechanic whose duration edit reaches the client
|
||||
// too late to be attributed to a cast packet, so no rule can express it.
|
||||
// duration rules use — SpellFamilyName plus a family-flag overlap and/or a
|
||||
// SpellIconID, at least one of the two — to a full duration from now. Returns
|
||||
// the spellID refreshed, or 0 if nothing matched. For a mechanic whose
|
||||
// duration edit reaches the client too late to be attributed to a cast packet,
|
||||
// so no rule can express it.
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid);
|
||||
@@ -108,8 +109,10 @@ enum DurationModOp {
|
||||
};
|
||||
|
||||
// Register a server duration-modifier rule from C++. The trigger is matched by
|
||||
// exact `triggerSpellId`; the affected aura by SpellFamilyName + a family-flag
|
||||
// overlap (+ optional `affectedIcon`, 0 = any). `valueMs` is the reduce/set
|
||||
// exact `triggerSpellId`; the affected aura by SpellFamilyName plus an
|
||||
// `affectedMask` family-flag overlap and/or an `affectedIcon`, at least one of
|
||||
// the two (0 drops that half of the test; a custom spell with no family flags
|
||||
// is named by its icon alone). `valueMs` is the reduce/set
|
||||
// amount in milliseconds (ignored by refresh/remove). Used by src/turtle
|
||||
// modules for the server's built-in mods (the family/school-matched variant is
|
||||
// still Lua-registerable via C_UnitAuras.RegisterAuraDurationModifierByTrigger).
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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/>.
|
||||
|
||||
// `success, ... = xpcall(f, msgh [, arg1, ...])` — call `f` under `msgh`,
|
||||
// forwarding the trailing arguments to it.
|
||||
//
|
||||
// 1.12's Lua takes only `xpcall(f, msgh)`. Its implementation is explicit
|
||||
// about it: `lua_settop(L, 2)` throws any further arguments away, then
|
||||
// `lua_pcall(L, 0, ...)` calls `f` with none. So the argument-forwarding form
|
||||
// doesn't merely fail — it silently calls `f` with nothing, and a function
|
||||
// that reads its parameters sees nil. (Note this is NOT a 5.0-vs-5.1
|
||||
// difference: both take `(f, msgh)`. Forwarding arrived later.)
|
||||
//
|
||||
// Callers work around it with a capturing closure —
|
||||
// `xpcall(function() return f(a, b) end, msgh)` — which is what
|
||||
// `ExecuteFrameScript` in the embedded addon does. That stays correct; this
|
||||
// just removes the need for it.
|
||||
//
|
||||
// Why a C function and not a Lua wrapper over the original: a Lua wrapper
|
||||
// would allocate a closure AND an `arg` table on every call (see the vararg
|
||||
// note in FunctionUtil.lua), putting an allocation on any error-guarded path,
|
||||
// including per-frame ones. `HookSecureFunc`'s `kUnhookableNames` also lists
|
||||
// `xpcall` precisely because shadowing a core primitive with a Lua closure
|
||||
// diverges from what C-side callers get. Replacing it with a real C function
|
||||
// has neither problem, and matches how `select` and `unpack` are backported
|
||||
// in this directory.
|
||||
//
|
||||
// A two-argument call is bit-for-bit the old behavior (zero forwarded args),
|
||||
// so this is purely additive for existing code.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::XPCall {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_xpcall(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (top < 2) {
|
||||
Game::Lua::Error(L, "bad argument #2 to 'xpcall' (value expected)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
|
||||
// `lua_pcall` wants the callee and its arguments contiguous on top, and
|
||||
// the handler at a fixed index. Incoming order is
|
||||
// `f(1), msgh(2), a1(3)..aN(top)`, so the handler sits between the two —
|
||||
// shuffle it below `f`:
|
||||
//
|
||||
// PushValue(2) → f, msgh, a1..aN, msgh
|
||||
// Insert(1) → msgh, f, msgh, a1..aN (top moved down to index 1)
|
||||
// Remove(3) → msgh, f, a1..aN
|
||||
//
|
||||
// 5.2 does the same reshuffle with a single `lua_rotate`, which this Lua
|
||||
// has no equivalent of.
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::Insert(L, 1);
|
||||
Game::Lua::Remove(L, 3);
|
||||
|
||||
const int status =
|
||||
Game::Lua::PCall(L, /*nargs*/ top - 2, Game::Lua::MULTRET, /*errfunc*/ 1);
|
||||
|
||||
// The stack now holds the handler plus either f's results or the handler's
|
||||
// return. Both cases want the status boolean in front of them, and the
|
||||
// handler itself dropped from the return set — which returning
|
||||
// `GetTop - 1` does, since results are taken from the top.
|
||||
Game::Lua::PushBool(L, status == 0);
|
||||
Game::Lua::Insert(L, 2);
|
||||
return Game::Lua::GetTop(L) - 1;
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("xpcall", &Script_xpcall);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("xpcall", &Script_xpcall);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::XPCall
|
||||
+30
-20
@@ -11,28 +11,32 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Inline-texture (`|T`) chat anti-spoof.
|
||||
// Inline-texture (`|T`, `|A`) chat anti-spoof.
|
||||
//
|
||||
// Vanilla neutralizes player-*typed* escapes in chat (a typed `|cff…|r`,
|
||||
// `|Hitem…|h` or `|T…|t` shows as raw text) — only *trusted* text renders them
|
||||
// (addon `AddMessage`, shift-clicked links). Our Text::InlineTexture backport
|
||||
// breaks that parity for `|T` alone: it detects the neutralized `||T` form and
|
||||
// draws the icon anyway, so a player could `/say` `|Tpath|t` and have a real
|
||||
// icon appear in everyone's chat AND in the speech bubble over their head.
|
||||
// breaks that parity for the two texture markers: it detects the neutralized
|
||||
// `||T` / `||A` form and draws the art anyway, so a player could `/say`
|
||||
// `|Tpath|t` and have a real icon appear in everyone's chat AND in the speech
|
||||
// bubble over their head.
|
||||
//
|
||||
// This restores vanilla's behavior for `|T` by defanging it in the chat path,
|
||||
// while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw for typed
|
||||
// input). It CANNOT be done in the text emitter: addon `|T` and player `|T`
|
||||
// reach the emitter as byte-identical `||T` (vanilla doubles the unrecognized
|
||||
// `|T` for both), so the only place they're distinguishable is the source —
|
||||
// server-delivered chat flows through the chat dispatcher, addon `print` does
|
||||
// not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
// This restores vanilla's behavior for both markers by defanging them in the
|
||||
// chat path, while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw
|
||||
// for typed input). It CANNOT be done in the text emitter: addon `|T` and player
|
||||
// `|T` reach the emitter as byte-identical `||T` (vanilla doubles the
|
||||
// unrecognized escape for both), so the only place they're distinguishable is
|
||||
// the source — server-delivered chat flows through the chat dispatcher, addon
|
||||
// `print` does not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
//
|
||||
// Defang = replace the `|` of each `|T` with a space, breaking the pipe→T
|
||||
// Defang = replace the `|` of each opener with a space, breaking the pipe→letter
|
||||
// adjacency the inline-texture detector keys on (`|T` and `||T` both contain the
|
||||
// `|T` substring, so this catches the doubled form too). The path text stays
|
||||
// readable, the closer `|t` is inert without an opener, and every other escape
|
||||
// is untouched.
|
||||
// `|T` substring, so this catches the doubled form too). The payload text stays
|
||||
// readable, the closer is inert without an opener, and every other escape is
|
||||
// untouched.
|
||||
//
|
||||
// ANY new marker the emitter learns MUST be added here in the same change, or it
|
||||
// becomes a chat-spoof vector.
|
||||
|
||||
#include "IconFilter.h"
|
||||
|
||||
@@ -40,10 +44,16 @@ namespace Chat::IconFilter {
|
||||
|
||||
namespace {
|
||||
|
||||
// True if `s` contains an inline-texture opener (`|T`) anywhere.
|
||||
// True if `s` contains an inline-texture opener anywhere. Both markers count:
|
||||
// `|T` names a texture path and `|A` names an atlas, and either one would render
|
||||
// art from player-typed text if it survived to the fontstring.
|
||||
bool IsIconOpener(const char *s) {
|
||||
return s[0] == '|' && (s[1] == 'T' || s[1] == 'A');
|
||||
}
|
||||
|
||||
bool HasIconEscape(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
if (s[0] == '|' && s[1] == 'T')
|
||||
if (IsIconOpener(s))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@@ -68,12 +78,12 @@ const char *Sanitize(const char *msg, char *buf, size_t bufSize) {
|
||||
// opener survives for the inline-texture detector to fire on.
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (msg[i] == '|' && msg[i + 1] == 'T') {
|
||||
if (IsIconOpener(msg + i)) {
|
||||
const bool doubledPipe = (i > 0 && msg[i - 1] == '|');
|
||||
buf[j++] = doubledPipe ? '|' : ' '; // keep pipe only if it completes `||`
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' '; // blank the `T`
|
||||
i += 2; // consumed `|T`
|
||||
buf[j++] = ' '; // blank the `T` / `A`
|
||||
i += 2; // consumed the opener
|
||||
} else {
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
|
||||
@@ -86,13 +86,18 @@ std::string StripMarkup(const char *s, unsigned len) {
|
||||
++i;
|
||||
break; // the |h is dropped by the 'h' case on the next iteration
|
||||
case 't': // stray texture closer — drop
|
||||
case 'a': // stray atlas closer — drop
|
||||
i += 2;
|
||||
break;
|
||||
case 'T': // texture opener — skip through to the next |t
|
||||
case 'A': // atlas opener — skip through to the next |a
|
||||
{
|
||||
const char closer = (s[i + 1] == 'A') ? 'a' : 't';
|
||||
i += 2;
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == 't'))
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == closer))
|
||||
++i;
|
||||
break; // the |t is dropped by the 't' case on the next iteration
|
||||
break; // the closer is dropped by the 't'/'a' case next iteration
|
||||
}
|
||||
default: // unknown sequence — keep the pipe literally
|
||||
out.push_back('|');
|
||||
++i;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "CommandLine.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace Config::CommandLine {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsSpace(char c) { return c == ' ' || c == '\t'; }
|
||||
|
||||
char Lower(char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c; }
|
||||
|
||||
// Stops at `a`'s terminator rather than reading `len` bytes unconditionally, so
|
||||
// a switch name at the very end of the command line cannot read past it.
|
||||
bool EqualsIgnoreCase(const char *a, const char *b, size_t len) {
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (a[i] == '\0' || Lower(a[i]) != Lower(b[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t Length(const char *s) {
|
||||
size_t n = 0;
|
||||
while (s[n] != '\0')
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Start of the value token for `-name`, or null when the switch is absent.
|
||||
const char *FindValue(const char *name) {
|
||||
const char *cmdLine = ::GetCommandLineA();
|
||||
if (cmdLine == nullptr || name == nullptr)
|
||||
return nullptr;
|
||||
const size_t nameLen = Length(name);
|
||||
if (nameLen == 0)
|
||||
return nullptr;
|
||||
|
||||
for (const char *p = cmdLine; *p != '\0'; ++p) {
|
||||
if (*p != '-' && *p != '/')
|
||||
continue;
|
||||
// Only at a token boundary, so a path containing the name cannot match.
|
||||
if (p != cmdLine && !IsSpace(p[-1]) && p[-1] != '"')
|
||||
continue;
|
||||
if (!EqualsIgnoreCase(p + 1, name, nameLen))
|
||||
continue;
|
||||
|
||||
const char *after = p + 1 + nameLen;
|
||||
if (*after == '=') {
|
||||
++after;
|
||||
} else if (IsSpace(*after)) {
|
||||
while (IsSpace(*after))
|
||||
++after;
|
||||
} else {
|
||||
continue; // a longer switch that merely starts with `name`
|
||||
}
|
||||
return after;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Walks the token at `p`, honoring double quotes so a value containing spaces
|
||||
// arrives whole. `emit` takes each character and returns whether it was kept;
|
||||
// the walk stops as a failure the moment one is not.
|
||||
template <typename Emit> bool ReadToken(const char *p, Emit emit) {
|
||||
const bool quoted = (*p == '"');
|
||||
if (quoted)
|
||||
++p;
|
||||
size_t n = 0;
|
||||
while (*p != '\0' && (quoted ? (*p != '"') : !IsSpace(*p))) {
|
||||
if (!emit(*p++))
|
||||
return false;
|
||||
++n;
|
||||
}
|
||||
return n > 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Value(const char *name, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const char *p = FindValue(name);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
|
||||
size_t n = 0;
|
||||
const bool ok = ReadToken(p, [&](char c) {
|
||||
if (n + 1 >= outSize)
|
||||
return false;
|
||||
out[n++] = c;
|
||||
return true;
|
||||
});
|
||||
out[n] = '\0';
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Value(const char *name, std::string &out) {
|
||||
out.clear();
|
||||
const char *p = FindValue(name);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
return ReadToken(p, [&](char c) {
|
||||
out.push_back(c);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Config::CommandLine
|
||||
@@ -0,0 +1,42 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
// Launch-switch parsing, shared by every module that reads one.
|
||||
//
|
||||
// A switch is written `-name value` or `-name=value`, and the `/name` form
|
||||
// Windows tools also accept is honored. The value may be double-quoted so it can
|
||||
// contain spaces, which a Lua snippet passed on the command line always will.
|
||||
// The name is matched case-insensitively and only at a token boundary, so a path
|
||||
// that happens to contain it cannot be mistaken for the switch itself.
|
||||
//
|
||||
// One parser for all of them on purpose: a second copy would drift, and the
|
||||
// quoting and boundary rules are exactly where that drift would go unnoticed.
|
||||
|
||||
namespace Config::CommandLine {
|
||||
|
||||
// Copies the value of `-name` into `out`. False when the switch is absent, its
|
||||
// value is empty, or the value does not fit -- a value too long to hold is a
|
||||
// failure rather than a silent truncation, since a half-copied filename or
|
||||
// snippet would be acted on as though it were complete.
|
||||
bool Value(const char *name, char *out, size_t outSize);
|
||||
|
||||
// Same, bounded only by the command line itself. For values with no natural
|
||||
// length, such as a script body.
|
||||
bool Value(const char *name, std::string &out);
|
||||
|
||||
} // namespace Config::CommandLine
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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.
|
||||
|
||||
// `-config <name>` — a launch switch that swaps which settings file the client
|
||||
// uses, so one install can hold several profiles.
|
||||
//
|
||||
// The engine already names the config file through a single pointer: boot passes
|
||||
// `"Config.wtf"` to the config initializer, which keeps it and uses it to both
|
||||
// read the file at startup and write it at exit. Supplying a different string is
|
||||
// therefore the whole feature — no second code path, and no way for reading and
|
||||
// writing to disagree. See `Offsets::PATCH_CONFIG_FILENAME_PTR`.
|
||||
|
||||
#include "FileSwitch.h"
|
||||
|
||||
#include "CommandLine.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Config::FileSwitch {
|
||||
|
||||
namespace {
|
||||
|
||||
// The engine holds this pointer for the life of the process and reads through it
|
||||
// again at exit to save, so the storage has to outlive everything — hence a
|
||||
// file-static buffer rather than anything owned by a scope.
|
||||
char g_configName[64];
|
||||
|
||||
// A filename is required to be a plain name. The engine builds the save path as
|
||||
// `WTF\` + this, and a subdirectory that does not exist makes the save fail
|
||||
// silently, which would throw away a session's settings with nothing to show for
|
||||
// it. Refusing the switch is the loud failure that a lost config file is not.
|
||||
bool IsPlainFilename(const char *name) {
|
||||
if (*name == '\0')
|
||||
return false;
|
||||
for (const char *p = name; *p != '\0'; ++p) {
|
||||
if (*p == '\\' || *p == '/' || *p == ':')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Apply() {
|
||||
if (!Config::CommandLine::Value("config", g_configName, sizeof g_configName))
|
||||
return;
|
||||
if (!IsPlainFilename(g_configName))
|
||||
return;
|
||||
|
||||
void *target = reinterpret_cast<void *>(Offsets::PATCH_CONFIG_FILENAME_PTR);
|
||||
DWORD previous = 0;
|
||||
if (!::VirtualProtect(target, sizeof(const char *), PAGE_EXECUTE_READWRITE, &previous))
|
||||
return;
|
||||
*reinterpret_cast<const char **>(target) = g_configName;
|
||||
::VirtualProtect(target, sizeof(const char *), previous, &previous);
|
||||
}
|
||||
|
||||
} // namespace Config::FileSwitch
|
||||
@@ -0,0 +1,37 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// `-config <name>` — makes the client read and write `WTF\<name>` in place of
|
||||
// `WTF\Config.wtf`, so several settings profiles can live side by side.
|
||||
namespace Config::FileSwitch {
|
||||
|
||||
// Reads the switch from the process command line and, when present, points the
|
||||
// engine's config filename at it.
|
||||
//
|
||||
// MUST be called from DllMain, and is the one thing in this project that is.
|
||||
// The engine loads the config during boot, several calls before the point where
|
||||
// VanillaFixes invokes our `Load` export, so by the time the normal
|
||||
// initialization path runs the file has already been read — see
|
||||
// `Offsets::PATCH_CONFIG_FILENAME_PTR` for the full call ordering. DllMain is
|
||||
// early enough because VanillaFixes injects every DLL while the game process is
|
||||
// still suspended and resumes it only afterwards.
|
||||
//
|
||||
// This does not install a hook. It is a single 4-byte store into an immediate
|
||||
// operand, so it costs none of what makes hooking from DllMain a problem: no
|
||||
// trampoline, no thread suspension, no loader-lock work, and nothing for another
|
||||
// DLL's hook to collide with.
|
||||
void Apply();
|
||||
|
||||
} // namespace Config::FileSwitch
|
||||
@@ -0,0 +1,170 @@
|
||||
// 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/>.
|
||||
|
||||
// Lua from the launch command line:
|
||||
//
|
||||
// -gluescript <code> run at the glue screens
|
||||
// -gluescriptFile <path> the file's contents, run at the glue screens
|
||||
// -gamescript <code> run once in-game
|
||||
// -gamescriptFile <path> the file's contents, run once in-game
|
||||
//
|
||||
// None of these exist in 1.12: the strings do not appear anywhere in the
|
||||
// binary, so there is nothing to parse them and nothing to un-gate. The whole
|
||||
// feature is therefore ours -- read the command line, and hand the source to
|
||||
// the engine's own compile-and-run funnel at the right moment.
|
||||
//
|
||||
// WHEN. Registration time is too early for either state. The in-game module
|
||||
// callback runs from FUN_LOAD_SCRIPT_FUNCTIONS, before FrameXML is loaded, and
|
||||
// the glue callback runs from FUN_LOAD_GLUE_SCRIPT_FUNCTIONS, before GlueXML
|
||||
// is. A script that ran there could not touch a frame, call a FrameXML
|
||||
// function, or see an addon, which is most of what anyone would write one for.
|
||||
// So each script is armed at registration and run from a tick, where the UI is
|
||||
// demonstrably up:
|
||||
//
|
||||
// glue -> Tick::FrameTick, whose hook is the UI render root and so fires at
|
||||
// the login and character-select screens. Armed by the glue module
|
||||
// callback, which runs once per glue boot -- initial launch and every
|
||||
// return from the world on logout -- so the glue script runs each
|
||||
// time the glue screens come up, matching what "run at the glue
|
||||
// screens" says. A script that logs in automatically therefore keeps
|
||||
// working after a logout, which is the point of it.
|
||||
// game -> Tick::WorldTick, which fires only in the world, so its first tick
|
||||
// is past FrameXML, past the addon load pass, and past enter-world
|
||||
// init. Latched for the life of the process, never re-armed on
|
||||
// /reload or on a second login, because "run once in-game" says once.
|
||||
//
|
||||
// HOW. FUN_LUA_RUN_STRING resolves the Lua state itself, so one call reaches
|
||||
// whichever state is current and the two paths differ only in when they fire.
|
||||
// It also installs the FrameScript error handler as the pcall's errfunc, so a
|
||||
// faulty script reports itself the way any script error does instead of failing
|
||||
// silently. Because every chunk goes through the shared compile chokepoint,
|
||||
// these scripts get the same 5.1 syntax support the rest of the client's Lua
|
||||
// has.
|
||||
//
|
||||
// A `*File` is read with Win32 rather than through the engine's file layer, on
|
||||
// purpose. The engine resolves relative paths against a loose-file index built
|
||||
// once at boot, so it cannot see a file created afterwards and cannot open an
|
||||
// absolute path at all -- and an absolute path is exactly what someone puts in
|
||||
// a shortcut. Reading it ourselves also means both switches share one execution
|
||||
// path, so inline code and file contents cannot diverge in behaviour.
|
||||
|
||||
#include "CommandLine.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "Game.h"
|
||||
#include "tick/FrameTick.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace Config::LaunchScript {
|
||||
|
||||
namespace {
|
||||
|
||||
using RunString_t = int(__fastcall *)(const char *source, uint32_t size, const char *chunkName,
|
||||
void *errCtx);
|
||||
|
||||
// A chunk name carries a leading `=` or `@` to tell Lua how to present it in an
|
||||
// error. That marker is for Lua, not for a reader, so the log drops it.
|
||||
const char *Displayed(const std::string &chunkName) {
|
||||
const char *s = chunkName.c_str();
|
||||
return (*s == '=' || *s == '@') ? s + 1 : s;
|
||||
}
|
||||
|
||||
void Run(const std::string &source, const std::string &chunkName) {
|
||||
if (source.empty())
|
||||
return;
|
||||
const int ok = reinterpret_cast<RunString_t>(Offsets::FUN_LUA_RUN_STRING)(
|
||||
source.c_str(), static_cast<uint32_t>(source.size()), chunkName.c_str(), nullptr);
|
||||
// The engine's error handler has already reported the detail to the user by
|
||||
// now; this only records that the switch was the source of it.
|
||||
if (ok == 0)
|
||||
Debug::Log::Printf("[launchscript] %s did not run to completion", Displayed(chunkName));
|
||||
}
|
||||
|
||||
// Whole-file read. Refuses a file larger than the compiler's own size parameter
|
||||
// can describe, which is the engine's bound rather than one of ours.
|
||||
bool ReadWholeFile(const char *path, std::string &out) {
|
||||
out.clear();
|
||||
const HANDLE file = ::CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
|
||||
bool ok = false;
|
||||
LARGE_INTEGER size{};
|
||||
if (::GetFileSizeEx(file, &size) != 0 && size.QuadPart >= 0 &&
|
||||
size.QuadPart <= static_cast<long long>(0xFFFFFFFFu)) {
|
||||
if (size.QuadPart == 0) {
|
||||
ok = true; // an empty file is not a failure, just nothing to run
|
||||
} else {
|
||||
out.assign(static_cast<size_t>(size.QuadPart), '\0');
|
||||
DWORD read = 0;
|
||||
ok = ::ReadFile(file, &out[0], static_cast<DWORD>(out.size()), &read, nullptr) != 0 &&
|
||||
read == out.size();
|
||||
}
|
||||
}
|
||||
::CloseHandle(file);
|
||||
if (!ok)
|
||||
out.clear();
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Runs the inline switch and then the file switch, so a launch that passes both
|
||||
// gets both, in the order they are written above.
|
||||
void RunPair(const char *codeSwitch, const char *fileSwitch) {
|
||||
std::string code;
|
||||
if (Config::CommandLine::Value(codeSwitch, code))
|
||||
Run(code, std::string("=-") + codeSwitch); // `=` shows the switch, not the source
|
||||
|
||||
std::string path;
|
||||
if (!Config::CommandLine::Value(fileSwitch, path))
|
||||
return;
|
||||
std::string contents;
|
||||
if (!ReadWholeFile(path.c_str(), contents)) {
|
||||
Debug::Log::Printf("[launchscript] -%s could not read '%s'", fileSwitch, path.c_str());
|
||||
return;
|
||||
}
|
||||
Run(contents, "@" + path); // `@` shows the path, so errors carry line numbers
|
||||
}
|
||||
|
||||
bool g_glueArmed = false;
|
||||
bool g_gameRan = false;
|
||||
|
||||
void OnFrameTick() {
|
||||
if (!g_glueArmed)
|
||||
return;
|
||||
g_glueArmed = false;
|
||||
RunPair("gluescript", "gluescriptFile");
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
if (g_gameRan)
|
||||
return;
|
||||
g_gameRan = true;
|
||||
RunPair("gamescript", "gamescriptFile");
|
||||
}
|
||||
|
||||
void ArmGlue() { g_glueArmed = true; }
|
||||
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&ArmGlue};
|
||||
const Tick::FrameTick::AutoSubscribe _frameTick{&OnFrameTick};
|
||||
const Tick::WorldTick::AutoSubscribe _worldTick{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Config::LaunchScript
|
||||
@@ -0,0 +1,197 @@
|
||||
// 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/>.
|
||||
|
||||
// `ConsoleGetAllCommands()` — every console command and console variable the
|
||||
// client currently has, as `ConsoleCommandInfo` tables.
|
||||
//
|
||||
// A plain global, not a `C_Console` member: the wiki files this under
|
||||
// `C_Console.GetAllCommands`, but on a live client that namespace does not
|
||||
// exist and `ConsoleGetAllCommands` is the real name.
|
||||
//
|
||||
// The engine keeps them in one registry, filled by
|
||||
// FUN_CONSOLE_COMMAND_REGISTER: a Storm intrusive list for iteration plus a
|
||||
// by-name hash for dispatch. This walks the list the same way the `help`
|
||||
// command does, which is the only other thing that enumerates it.
|
||||
//
|
||||
// ONE LIST, NOT TWO. Commands and CVars do not live separately. The CVar
|
||||
// registrar's last act is to register the CVar as a console command as well —
|
||||
// `FUN_CONSOLE_COMMAND_REGISTER(cvar->name, <shared handler>, categoryId,
|
||||
// help)` — which is what makes `/console <cvarName> <value>` work. So the
|
||||
// command list already holds every CVar, and walking the CVar list too (the one
|
||||
// `cvarlist` and the config saver use) would report each one twice.
|
||||
//
|
||||
// That also gives an exact `commandType`: every CVar shares one handler
|
||||
// address, so a node pointing at it is a CVar and anything else is a command.
|
||||
// It is a pointer comparison against a value the registrar itself writes, not a
|
||||
// name or category heuristic.
|
||||
//
|
||||
// FIELDS WE CANNOT FILL. `scriptContents` and `scriptParameters` describe
|
||||
// console macros and scripts, which this client has no notion of — there is no
|
||||
// registration path that could produce one. They are always the empty string,
|
||||
// so a caller reading them gets a string rather than a nil surprise. `category`
|
||||
// is the engine's own number, and the engine's table (debug, graphics, console,
|
||||
// combat, game, default, net, sound, gm) is Enum.ConsoleCategory's 0..8 exactly,
|
||||
// so it needs no translation.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Console::Commands {
|
||||
|
||||
namespace {
|
||||
|
||||
// Enum.ConsoleCommandType. Macro (2) and Script (3) exist in the contract but
|
||||
// cannot occur here.
|
||||
constexpr int kCommandTypeCvar = 0;
|
||||
constexpr int kCommandTypeCommand = 1;
|
||||
|
||||
using ConsoleWrite_t = void(__fastcall *)(const char *line, int colorType);
|
||||
|
||||
// A list node, or 0 once the walk reaches the end. The low bit tags the
|
||||
// sentinel, the same convention the addon registry and the parser lists use.
|
||||
bool IsNode(uintptr_t node) { return node != 0 && (node & 1) == 0; }
|
||||
|
||||
char Lower(char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c; }
|
||||
|
||||
bool StartsWithIgnoreCase(const char *name, const char *prefix) {
|
||||
for (size_t i = 0; prefix[i] != '\0'; ++i) {
|
||||
if (name[i] == '\0' || Lower(name[i]) != Lower(prefix[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetStringField(void *L, const char *key, const char *value) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::PushString(L, value != nullptr ? value : "");
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetIntField(void *L, const char *key, int value) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(value));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// `ConsoleGetAllCommands() -> ConsoleCommandInfo[]`
|
||||
int __fastcall Script_GetAllCommands(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const uintptr_t linkOffset =
|
||||
Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LINK_OFFSET);
|
||||
uintptr_t node = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LIST_HEAD);
|
||||
|
||||
int count = 0;
|
||||
while (IsNode(node)) {
|
||||
const auto *name =
|
||||
Game::Read<const char *>(node + Offsets::OFF_CONSOLE_COMMAND_NAME);
|
||||
const auto *help =
|
||||
Game::Read<const char *>(node + Offsets::OFF_CONSOLE_COMMAND_HELP);
|
||||
const uintptr_t handler =
|
||||
Game::Read<uint32_t>(node + Offsets::OFF_CONSOLE_COMMAND_HANDLER);
|
||||
const int category =
|
||||
Game::Read<int32_t>(node + Offsets::OFF_CONSOLE_COMMAND_CATEGORY);
|
||||
|
||||
// Step before building the entry: the walk stays correct even if a
|
||||
// field read below is skipped.
|
||||
node = Game::Read<uint32_t>(node + linkOffset + 4);
|
||||
|
||||
if (name == nullptr)
|
||||
continue; // a node the registrar never finished; nothing to report
|
||||
|
||||
Game::Lua::PushNumber(L, ++count);
|
||||
Game::Lua::NewTable(L);
|
||||
SetStringField(L, "command", name);
|
||||
SetStringField(L, "help", help);
|
||||
SetIntField(L, "category", category);
|
||||
SetIntField(L, "commandType",
|
||||
handler == Offsets::FUN_CONSOLE_CVAR_COMMAND_HANDLER
|
||||
? kCommandTypeCvar
|
||||
: kCommandTypeCommand);
|
||||
SetStringField(L, "scriptContents", "");
|
||||
SetStringField(L, "scriptParameters", "");
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Both enums are Blizzard's, stated in full rather than derived from the
|
||||
// engine's own category table. They are an API contract, not a description of
|
||||
// what this client happens to have: an addon asking for a value we can never
|
||||
// return still deserves the number instead of a nil. `Macro` and `Script` have
|
||||
// no registration path here, and `Reveal` / `None` are not among the engine's
|
||||
// nine categories -- everything else lines up, which is the point.
|
||||
// `ConsolePrintAllMatchingCommands(prefix)` — write every command whose name
|
||||
// starts with `prefix` to the console, the way its own tab completion lists
|
||||
// candidates. Case-insensitive, matching how the engine resolves a command
|
||||
// name.
|
||||
//
|
||||
// Prints nothing for an empty prefix rather than dumping all 500-odd entries;
|
||||
// ConsoleGetAllCommands is the way to ask for everything.
|
||||
int __fastcall Script_ConsolePrintAllMatchingCommands(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return 0;
|
||||
const char *prefix = Game::Lua::ToString(L, 1);
|
||||
if (prefix == nullptr || prefix[0] == '\0')
|
||||
return 0;
|
||||
|
||||
auto write = reinterpret_cast<ConsoleWrite_t>(Offsets::FUN_CONSOLE_WRITE);
|
||||
const uintptr_t linkOffset =
|
||||
Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LINK_OFFSET);
|
||||
uintptr_t node = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LIST_HEAD);
|
||||
while (IsNode(node)) {
|
||||
const auto *name =
|
||||
Game::Read<const char *>(node + Offsets::OFF_CONSOLE_COMMAND_NAME);
|
||||
node = Game::Read<uint32_t>(node + linkOffset + 4);
|
||||
if (name != nullptr && StartsWithIgnoreCase(name, prefix))
|
||||
write(name, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kCommandTypeEntries[] = {
|
||||
{"Cvar", kCommandTypeCvar},
|
||||
{"Command", kCommandTypeCommand},
|
||||
{"Macro", 2},
|
||||
{"Script", 3},
|
||||
};
|
||||
|
||||
// The engine's table at VAR_CONSOLE_CATEGORY_TABLE holds ids 0..8 named debug,
|
||||
// graphics, console, combat, game, default, net, sound, gm, in that order --
|
||||
// so a `category` read off a command node is already one of these.
|
||||
const Game::Lua::EnumIntegerEntry kCategoryEntries[] = {
|
||||
{"Debug", 0}, {"Graphics", 1}, {"Console", 2}, {"Combat", 3},
|
||||
{"Game", 4}, {"Default", 5}, {"Net", 6}, {"Sound", 7},
|
||||
{"Gm", 8}, {"Reveal", 9}, {"None", 10},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands);
|
||||
Game::Lua::RegisterGlobalFunction("ConsolePrintAllMatchingCommands",
|
||||
&Script_ConsolePrintAllMatchingCommands);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries,
|
||||
sizeof(kCommandTypeEntries) /
|
||||
sizeof(kCommandTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries,
|
||||
sizeof(kCategoryEntries) /
|
||||
sizeof(kCategoryEntries[0]));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Console::Commands
|
||||
@@ -0,0 +1,240 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The developer console, as Lua functions: running commands, writing lines,
|
||||
// and reading the console's own state and appearance.
|
||||
//
|
||||
// `ConsoleExec` is the one with real reach. Nothing in this client's Lua could
|
||||
// run a console command before, so every entry ConsoleGetAllCommands returns
|
||||
// was visible and unreachable at once. It is the engine's own executor, the
|
||||
// same one that replays each `SET` line of Config.wtf at boot.
|
||||
//
|
||||
// THAT INCLUDES `set`, AND `set` IS NOT SetCVar. The console's set handler
|
||||
// looks a cvar up with the unfiltered lookup and performs no read-only check,
|
||||
// while Script_SetCVar tests flag bit 0x4 first and refuses. So
|
||||
// `ConsoleExec("set <name> <value>")` can write a cvar that
|
||||
// `SetCVar` rejects, and can reach cvars no Lua getter can see (Offsets.h at
|
||||
// CVAR_FLAG_READ_ONLY has the mechanism). That is inherent to exposing the
|
||||
// console rather than something added here -- an addon already runs arbitrary
|
||||
// Lua -- but it does mean the read-only guarantee C_CVar.GetCVarInfo reports
|
||||
// stops being absolute once this exists. Deliberate: a console you cannot run
|
||||
// commands through is not a console.
|
||||
//
|
||||
// Everything else is state the console module already keeps. The colours are
|
||||
// its own nine-entry table, the font height its own global, and the toggle key
|
||||
// its own byte, so each is read or written where the engine reads or writes it
|
||||
// rather than being modelled again here.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Console::Shell {
|
||||
|
||||
namespace {
|
||||
|
||||
using ConsoleExec_t = void(__fastcall *)(const char *line, int addToHistory);
|
||||
using ConsoleWrite_t = void(__fastcall *)(const char *line, int colorType);
|
||||
using ConsoleSetKey_t = void(__fastcall *)(int keyCode);
|
||||
|
||||
// `ConsoleExec(command)` — run a console command line.
|
||||
//
|
||||
// Passes addToHistory = 1 so a command run from Lua appears in the console's
|
||||
// own history, which is what someone stepping through the console afterwards
|
||||
// expects to find.
|
||||
int __fastcall Script_ConsoleExec(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return 0;
|
||||
reinterpret_cast<ConsoleExec_t>(Offsets::FUN_CONSOLE_EXEC)(Game::Lua::ToString(L, 1), 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `ConsoleEcho(message [, colorType])` — write one line to the console.
|
||||
//
|
||||
// The engine no-ops cleanly when the graphics device is not up, so this is safe
|
||||
// to call at any time, and the line is kept whether or not the console is
|
||||
// currently open.
|
||||
int __fastcall Script_ConsoleEcho(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return 0;
|
||||
int colorType = 0;
|
||||
if (Game::Lua::IsNumber(L, 2)) {
|
||||
colorType = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (colorType < 0 || colorType >= Offsets::CONSOLE_COLOR_COUNT)
|
||||
colorType = 0;
|
||||
}
|
||||
reinterpret_cast<ConsoleWrite_t>(Offsets::FUN_CONSOLE_WRITE)(Game::Lua::ToString(L, 1),
|
||||
colorType);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `ConsoleIsActive()` — whether the console is open right now.
|
||||
//
|
||||
// False when the client was started without `-console`, since the toggle key
|
||||
// is gated on that and the console can then never open.
|
||||
int __fastcall Script_ConsoleIsActive(void *L) {
|
||||
const uint32_t enabled = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_ENABLED);
|
||||
const uint32_t visible = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_VISIBLE);
|
||||
Game::Lua::PushBool(L, enabled != 0 && visible != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `ConsoleGetColorFromType(colorType)` -> r, g, b, a
|
||||
//
|
||||
// The colour the console draws that type of line in, as four 0..1 components to
|
||||
// match every other colour in this API. Nil for a type outside the table.
|
||||
int __fastcall Script_ConsoleGetColorFromType(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int type = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (type < 0 || type >= Offsets::CONSOLE_COLOR_COUNT) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t argb = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_CONSOLE_COLORS + static_cast<uintptr_t>(type) * sizeof(uint32_t));
|
||||
Game::Lua::PushNumber(L, ((argb >> 16) & 0xFF) / 255.0);
|
||||
Game::Lua::PushNumber(L, ((argb >> 8) & 0xFF) / 255.0);
|
||||
Game::Lua::PushNumber(L, (argb & 0xFF) / 255.0);
|
||||
Game::Lua::PushNumber(L, ((argb >> 24) & 0xFF) / 255.0);
|
||||
return 4;
|
||||
}
|
||||
|
||||
// `ConsoleGetFontHeight()` — the console's text height, as a fraction of the
|
||||
// screen height.
|
||||
int __fastcall Script_ConsoleGetFontHeight(void *L) {
|
||||
Game::Lua::PushNumber(L, *reinterpret_cast<const float *>(Offsets::VAR_CONSOLE_FONT_HEIGHT));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// There is deliberately no ConsoleSetFontHeight. Writing the height global
|
||||
// alone changes nothing, since the font was already built at the old size, and
|
||||
// the only thing that rebuilds it is the console's font init — which has no
|
||||
// code callers at all, just one entry in a table at 0x0080E170. It runs once,
|
||||
// during startup, before a single console line exists.
|
||||
//
|
||||
// Re-running it crashes, on a later frame rather than in the call: it frees the
|
||||
// old font handle out from under the cached, font-dependent draw object every
|
||||
// existing line holds at +0x20, and the next console render walks those. The
|
||||
// console's own shutdown frees both together, which is what that pairing is
|
||||
// for. Making a setter safe would mean replaying that teardown across every
|
||||
// line, to change a text size nothing asks to change.
|
||||
|
||||
// `SetConsoleKey(keyCode)` — the key that opens and closes the console.
|
||||
//
|
||||
// Takes the engine's own key code, the value its key events carry, rather than
|
||||
// a key name: the console module stores and compares exactly that, and nothing
|
||||
// in it translates names.
|
||||
int __fastcall Script_SetConsoleKey(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
reinterpret_cast<ConsoleSetKey_t>(Offsets::FUN_CONSOLE_SET_KEY)(
|
||||
static_cast<int>(Game::Lua::ToNumber(L, 1)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `CalculateStringEditDistance(a, b)` — Levenshtein distance, the number of
|
||||
// single-character insertions, deletions or substitutions between two strings.
|
||||
// The console uses it to suggest a command when one is mistyped.
|
||||
//
|
||||
// Two rolling rows rather than a full matrix, so the cost is bounded by the
|
||||
// shorter string; the longer one only ever drives the loop.
|
||||
int __fastcall Script_CalculateStringEditDistance(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const char *a = Game::Lua::ToString(L, 1);
|
||||
const char *b = Game::Lua::ToString(L, 2);
|
||||
|
||||
size_t lenA = 0, lenB = 0;
|
||||
while (a[lenA] != '\0')
|
||||
++lenA;
|
||||
while (b[lenB] != '\0')
|
||||
++lenB;
|
||||
if (lenA == 0 || lenB == 0) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(lenA + lenB));
|
||||
return 1;
|
||||
}
|
||||
// Index the rows by the shorter string so the buffers stay small whichever
|
||||
// way round the arguments came in.
|
||||
if (lenB > lenA) {
|
||||
const char *t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
const size_t n = lenA;
|
||||
lenA = lenB;
|
||||
lenB = n;
|
||||
}
|
||||
constexpr size_t kMaxRow = 256; // beyond this the answer is of no practical use
|
||||
if (lenB >= kMaxRow) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t prev[kMaxRow], curr[kMaxRow];
|
||||
for (size_t j = 0; j <= lenB; ++j)
|
||||
prev[j] = j;
|
||||
for (size_t i = 1; i <= lenA; ++i) {
|
||||
curr[0] = i;
|
||||
for (size_t j = 1; j <= lenB; ++j) {
|
||||
const size_t cost = (a[i - 1] == b[j - 1]) ? 0u : 1u;
|
||||
size_t best = prev[j] + 1; // deletion
|
||||
if (curr[j - 1] + 1 < best)
|
||||
best = curr[j - 1] + 1; // insertion
|
||||
if (prev[j - 1] + cost < best)
|
||||
best = prev[j - 1] + cost; // substitution
|
||||
curr[j] = best;
|
||||
}
|
||||
for (size_t j = 0; j <= lenB; ++j)
|
||||
prev[j] = curr[j];
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(prev[lenB]));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleExec", &Script_ConsoleExec);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleEcho", &Script_ConsoleEcho);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleIsActive", &Script_ConsoleIsActive);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetColorFromType",
|
||||
&Script_ConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlobalFunction("SetConsoleKey", &Script_SetConsoleKey);
|
||||
Game::Lua::RegisterGlobalFunction("CalculateStringEditDistance",
|
||||
&Script_CalculateStringEditDistance);
|
||||
}
|
||||
|
||||
// The console exists before login, so the same surface is registered on the
|
||||
// glue state.
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("ConsoleExec", &Script_ConsoleExec);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleEcho", &Script_ConsoleEcho);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleIsActive", &Script_ConsoleIsActive);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleGetColorFromType",
|
||||
&Script_ConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlueFunction("SetConsoleKey", &Script_SetConsoleKey);
|
||||
Game::Lua::RegisterGlueFunction("CalculateStringEditDistance",
|
||||
&Script_CalculateStringEditDistance);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Console::Shell
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])` — move an item
|
||||
// and let the SERVER choose the destination slot. A ClassicAPI-only
|
||||
// extension, like its siblings `SwapItems` and `MoveItem`; the modern
|
||||
// API has no equivalent, because it has no need for one.
|
||||
//
|
||||
// The reason to want it: the server does not just find an empty slot.
|
||||
// It first merges the stack into existing non-full stacks of the same
|
||||
// item, splitting it across several of them when that is what fits, and
|
||||
// only places a remainder if any is left over. So this is a one-call
|
||||
// stack consolidator that needs no stack-size lookup — the caller never
|
||||
// has to know what fits. A merge computed on this side has to size
|
||||
// every stack first, and `C_Item.GetItemMaxStackSizeByID` is nil until
|
||||
// that item's data has arrived, so a cold item cache silently skips
|
||||
// exactly the stacks it could not size.
|
||||
//
|
||||
// `dstBag` defaults to 0, which means "anywhere in the main inventory
|
||||
// that it fits" rather than "the backpack" — on the wire the backpack
|
||||
// IS the player container, so the two cannot be distinguished. Pass
|
||||
// 1..4 to confine the item to one equipped bag. Bank destinations are
|
||||
// rejected (see [[item/Swap.h]] `AutoStore`).
|
||||
//
|
||||
// Returns true once the packet is away — like the other two, the send
|
||||
// is fire-and-forget and the server confirms through the normal
|
||||
// BAG_UPDATE / SMSG_INVENTORY_CHANGE_FAILURE flow.
|
||||
|
||||
#include "Game.h"
|
||||
#include "item/Swap.h"
|
||||
|
||||
namespace Container::AutoStoreItem {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Container_AutoStoreItem(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])");
|
||||
return 0;
|
||||
}
|
||||
// Read every arg before calling AutoStore — its source lookup goes
|
||||
// through ResolveBag, which stomps the Lua stack via PackBagSlot.
|
||||
const int srcBag = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int srcSlot = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
const int dstBag = Game::Lua::IsNumber(L, 3)
|
||||
? static_cast<int>(Game::Lua::ToNumber(L, 3))
|
||||
: 0;
|
||||
|
||||
const bool ok = Item::Swap::AutoStore(L, srcBag, srcSlot, dstBag);
|
||||
Game::Lua::PushBool(L, ok);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
|
||||
&Script_C_Container_AutoStoreItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::AutoStoreItem
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_Container.GetContainerItemEquipmentSetInfo(containerIndex, slotIndex)`
|
||||
// — whether the item in a bag slot belongs to an equipment set, and the
|
||||
// names of the sets it belongs to:
|
||||
//
|
||||
// inSet, setList = C_Container.GetContainerItemEquipmentSetInfo(bag, slot)
|
||||
//
|
||||
// The lookup is by the item's per-instance GUID against the GUIDs
|
||||
// `EquipmentSet::Data` saved into each set, so it names the one physical
|
||||
// item that was saved rather than every copy of that itemID — a spare
|
||||
// of the same item sitting in the next slot reports `false`. The set's
|
||||
// reserved empty / ignored slot markers can never match a real GUID,
|
||||
// and `Data::SetsContainingItem` rejects them outright.
|
||||
//
|
||||
// `setList` is the set names joined with `", "`, in set order, which is
|
||||
// the shape a caller formats straight into a tooltip line. It is `nil`
|
||||
// rather than an empty string when the item is in no set, so the usual
|
||||
// `if inSet then … setList … end` guard is all a caller needs.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Data.h"
|
||||
#include "equipmentset/Set.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Container::EquipmentSetInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
// The separator between names in `setList`. One place, so the string a
|
||||
// caller splits on is stated once.
|
||||
constexpr const char *kSeparator = ", ";
|
||||
|
||||
uint64_t ItemGuid(const uint8_t *cgItem) {
|
||||
auto *instance = Item::InstanceBlock(cgItem);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemEquipmentSetInfo(containerIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
// `ResolveBag` stomps stack slots 1 and 2 through the engine's
|
||||
// `PackBagSlot`, so both arguments are already in locals above and
|
||||
// nothing we need survives on the stack across it.
|
||||
const uint64_t guid =
|
||||
ItemGuid(Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
|
||||
const std::vector<const EquipmentSet::Set *> sets =
|
||||
EquipmentSet::Data::SetsContainingItem(guid);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (sets.empty()) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::string setList;
|
||||
for (size_t i = 0; i < sets.size(); ++i) {
|
||||
if (i > 0)
|
||||
setList += kSeparator;
|
||||
setList += sets[i]->name;
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, true);
|
||||
Game::Lua::PushString(L, setList.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Container", "GetContainerItemEquipmentSetInfo",
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::EquipmentSetInfo
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Container::FreeSlots {
|
||||
|
||||
@@ -115,6 +116,46 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_Container.GetContainerFreeSlots(bagID)` — which slots are empty,
|
||||
// as an array of slot numbers in ascending order, rather than
|
||||
// `GetContainerNumFreeSlots`'s count of them. A full bag returns an
|
||||
// empty table; a bag that isn't there returns nothing, which is how
|
||||
// callers tell "no free slots" from "no such bag".
|
||||
//
|
||||
// `bagID` is 0..4 like the rest of this file (0 = backpack, 1..4 =
|
||||
// equipped bag slots); bank and keyring indices are the "no such bag"
|
||||
// case.
|
||||
int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerFreeSlots(bagID)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount <= 0)
|
||||
return 0; // nothing — no such bag
|
||||
|
||||
// Collect before building the table. Each `ResolveBag` stomps stack
|
||||
// slots 1 and 2, so the table cannot be on the stack during the walk.
|
||||
std::vector<int> freeSlots;
|
||||
freeSlots.reserve(static_cast<size_t>(info.slotCount));
|
||||
for (int slot = 1; slot <= info.slotCount; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
freeSlots.push_back(slot);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < freeSlots.size(); ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(freeSlots[i]));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Container.CalculateTotalNumberOfFreeBagSlots()` — the total number of
|
||||
// free slots across the player's *general-purpose* bags (backpack + equipped
|
||||
// bags 0..4 whose BagFamily is 0). Specialty bags (quivers, soul bags, herb
|
||||
@@ -146,6 +187,8 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
|
||||
&Script_C_Container_GetContainerNumFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
|
||||
&Script_C_Container_GetContainerFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
|
||||
}
|
||||
|
||||
@@ -49,32 +49,20 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Icon.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/StatAccum.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Container::ItemInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
out[0] = '\0';
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr || iconName[0] == '\0')
|
||||
return false;
|
||||
std::snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mirrors the engine's `readable` decision (0x004F97BD..0x004F97CF): a slot
|
||||
// is readable when its static page text is set OR the per-instance
|
||||
// readable-text id is non-zero (letters / server-authored notes).
|
||||
@@ -137,7 +125,7 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
if (record != nullptr) {
|
||||
const uint32_t displayInfoID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
haveIcon = BuildIconPath(displayInfoID, iconPath, sizeof(iconPath));
|
||||
haveIcon = Item::Icon::PathForDisplayInfoID(displayInfoID, iconPath, sizeof(iconPath));
|
||||
quality = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
haveQuality = true;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_Container.GetContainerItemQuestInfo(containerIndex, slotIndex)` —
|
||||
// the quest state of a bag slot, as one table:
|
||||
//
|
||||
// { isQuestItem = bool, questID = number|nil, isActive = bool }
|
||||
//
|
||||
// This is what draws the two overlays on a bag button: a `questID` the
|
||||
// player has not accepted yet gets the exclamation mark, and either an
|
||||
// accepted `questID` or a plain quest item gets the question-mark
|
||||
// border.
|
||||
//
|
||||
// The three fields come from three places:
|
||||
//
|
||||
// isQuestItem the item's class is Quest — an item that exists to be
|
||||
// a quest objective.
|
||||
// questID the quest the item BEGINS (`m_startQuest` in the
|
||||
// cached item record). Absent, not zero, when the item
|
||||
// starts nothing — callers test `if questInfo.questID`.
|
||||
// isActive that started quest is already in the quest log.
|
||||
//
|
||||
// The table is always returned, even for an empty slot or an item whose
|
||||
// data has not arrived yet, so callers can index it without a nil test.
|
||||
// An uncached item warms the cache, so the next bag update draws the
|
||||
// overlay the first hover could not.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "quest/Log.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Container::QuestInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
// ItemClass.dbc row 12. Only this file cares which class means "quest",
|
||||
// so the code stays local per the single-use rule in the offsets policy.
|
||||
constexpr uint32_t kItemClassQuest = 12;
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemQuestInfo(containerIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
// Resolve before building the table: `ResolveBag` stomps stack
|
||||
// slots 1 and 2 through the engine's `PackBagSlot`, so nothing we
|
||||
// want to keep may be on the stack across this call.
|
||||
const int itemID =
|
||||
Item::ID::FromCGItem(Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
|
||||
bool isQuestItem = false;
|
||||
uint32_t startQuest = 0;
|
||||
if (itemID != 0) {
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
// Cold cache. Report the empty answer now and fetch, so the
|
||||
// overlay is right on the next bag update.
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
} else {
|
||||
isQuestItem = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS) ==
|
||||
kItemClassQuest;
|
||||
startQuest = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_START_QUEST);
|
||||
}
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldBool(L, "isQuestItem", isQuestItem);
|
||||
if (startQuest > 0) {
|
||||
Game::Lua::SetFieldNumber(L, "questID",
|
||||
static_cast<double>(startQuest));
|
||||
Game::Lua::SetFieldBool(
|
||||
L, "isActive", Quest::Log::IsOnQuest(static_cast<int>(startQuest)));
|
||||
} else {
|
||||
// `questID` stays absent rather than 0 — the field is nilable and
|
||||
// every caller tests it for truth.
|
||||
Game::Lua::SetFieldBool(L, "isActive", false);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
|
||||
&Script_C_Container_GetContainerItemQuestInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::QuestInfo
|
||||
@@ -0,0 +1,936 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_Container.SortBags()` — arrange the player's bags.
|
||||
//
|
||||
// Two phases, because every item move is a server round trip and the
|
||||
// second phase has to read slot contents the first phase changed.
|
||||
//
|
||||
// PHASE ONE consolidates partial stacks, and hands that entirely to the
|
||||
// server: one `CMSG_AUTOSTORE_BAG_ITEM` per redundant stack, which
|
||||
// merges into existing stacks of the same item and splits across several
|
||||
// of them when that is what fits. Nothing here computes what fits, so
|
||||
// nothing here needs an item's max stack size — which matters, because
|
||||
// that number is unavailable until the item's data has arrived from the
|
||||
// server, and a merge that sizes stacks first silently skips the ones it
|
||||
// could not size.
|
||||
//
|
||||
// PHASE TWO places items. It runs on the next `Bag::UpdateDelayed` — the
|
||||
// engine's own "a bag changed this frame" signal — so the merges are
|
||||
// settled before anything is read. It plans the whole layout up front
|
||||
// and then tracks each item's live position as swaps fire, rather than
|
||||
// re-reading a layout it is in the middle of mutating. Our swap is
|
||||
// fire-and-forget and sets no client-side item lock, so the whole batch
|
||||
// goes out in one frame.
|
||||
//
|
||||
// Neither phase may touch the Lua stack: phase two runs from a bag-update
|
||||
// callback, outside any Lua call, where the engine may have its own
|
||||
// values on the stack. Hence the `*From` senders and the direct invMgr
|
||||
// walk rather than `ResolveBag`.
|
||||
//
|
||||
// THE ORDERING IS OURS. Retail computes it in code we cannot read, and
|
||||
// its categories (per-bag assignment flags, expansion filters) have no
|
||||
// counterpart here, so this is a defensible order rather than a
|
||||
// reproduction of Blizzard's: hearthstone, gear by descending quality,
|
||||
// consumables, reagents, trade goods, quest items, everything else by
|
||||
// quality, junk last. Within a category, by item class, subclass, name,
|
||||
// then fuller stacks first.
|
||||
//
|
||||
// An item whose data has NOT arrived is pinned: left where it is, with
|
||||
// its slot withheld from the destination pool so nothing else is planned
|
||||
// into it. It cannot be ranked without a class, quality and name, and
|
||||
// guessing scatters it. A later call places it once the data lands. Phase
|
||||
// one is unaffected, since it needs only the item ID.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bag/UpdateDelayed.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Swap.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Container::SortBags {
|
||||
|
||||
namespace {
|
||||
|
||||
// The two sets this can arrange. Both go through the same snapshot,
|
||||
// comparator and placement; they differ only in how phase one merges
|
||||
// (see ConsolidateViaServer / ConsolidateLocally).
|
||||
constexpr int kInventoryBags[] = {0, 1, 2, 3, 4};
|
||||
constexpr int kBankBags[] = {-1, 5, 6, 7, 8, 9, 10};
|
||||
|
||||
// Fill direction, persisted like every other engine setting: the cvar
|
||||
// carries it to Config.wtf and `GetCVar("sortBagsRightToLeft")` reads it,
|
||||
// so it survives a session the way the retail setting does.
|
||||
CVar::Factory::Handle g_rightToLeftCVar = nullptr;
|
||||
|
||||
bool RightToLeft() {
|
||||
return CVar::Factory::GetInt(g_rightToLeftCVar, 0) != 0;
|
||||
}
|
||||
|
||||
// The two containers a sort can be told to leave alone. An equipped bag
|
||||
// is excluded per-bag through its own `DisableAutoSort` slot flag; the
|
||||
// backpack and the main bank container have no such slot, so each gets
|
||||
// its own setting. Persisted as cvars for the same reason the fill
|
||||
// direction is.
|
||||
CVar::Factory::Handle g_backpackAutosortCVar = nullptr;
|
||||
CVar::Factory::Handle g_bankAutosortCVar = nullptr;
|
||||
|
||||
bool BackpackAutosortDisabled() {
|
||||
return CVar::Factory::GetInt(g_backpackAutosortCVar, 0) != 0;
|
||||
}
|
||||
|
||||
bool BankAutosortDisabled() {
|
||||
return CVar::Factory::GetInt(g_bankAutosortCVar, 0) != 0;
|
||||
}
|
||||
|
||||
// Merge packets sent by the most recent phase one, reset per sort.
|
||||
int g_mergePackets = 0;
|
||||
|
||||
// The source slots phase one asked the server to empty. Phase two counts
|
||||
// how many are still occupied when it runs: any at all means it is
|
||||
// planning against a half-merged snapshot, because the server's replies
|
||||
// did not all arrive in the bag update that woke it.
|
||||
struct SourceSlot {
|
||||
int bag, slot;
|
||||
};
|
||||
std::vector<SourceSlot> g_mergeSources;
|
||||
|
||||
// Item.dbc `m_class` values we rank on. Per-module codes, the way
|
||||
// `Item::BagFamily` keeps its own.
|
||||
constexpr uint32_t kClassConsumable = 0;
|
||||
constexpr uint32_t kClassWeapon = 2;
|
||||
constexpr uint32_t kClassArmor = 4;
|
||||
constexpr uint32_t kClassReagent = 5;
|
||||
constexpr uint32_t kClassTradeGoods = 7;
|
||||
constexpr uint32_t kClassQuest = 12;
|
||||
|
||||
constexpr uint32_t kQualityPoor = 0;
|
||||
constexpr uint32_t kQualityUncommon = 2;
|
||||
constexpr uint32_t kQualityRare = 3;
|
||||
constexpr uint32_t kQualityEpic = 4;
|
||||
|
||||
// Primary key. Lower sorts nearer the first slot.
|
||||
enum Category : uint8_t {
|
||||
kCatHearthstone = 0,
|
||||
kCatGearEpic,
|
||||
kCatGearRare,
|
||||
kCatGearUncommon,
|
||||
kCatGearPlain,
|
||||
kCatConsumable,
|
||||
kCatReagent,
|
||||
kCatTradeGoods,
|
||||
kCatQuest,
|
||||
kCatOtherEpic,
|
||||
kCatOtherRare,
|
||||
kCatOtherUncommon,
|
||||
kCatOtherPlain,
|
||||
kCatJunk,
|
||||
};
|
||||
|
||||
uint8_t CategoryFor(uint32_t itemID, uint32_t itemClass, uint32_t quality) {
|
||||
if (itemID == Offsets::HEARTHSTONE_ITEM_ID)
|
||||
return kCatHearthstone;
|
||||
if (quality == kQualityPoor)
|
||||
return kCatJunk;
|
||||
if (itemClass == kClassWeapon || itemClass == kClassArmor) {
|
||||
if (quality >= kQualityEpic)
|
||||
return kCatGearEpic;
|
||||
if (quality == kQualityRare)
|
||||
return kCatGearRare;
|
||||
if (quality == kQualityUncommon)
|
||||
return kCatGearUncommon;
|
||||
return kCatGearPlain;
|
||||
}
|
||||
if (itemClass == kClassConsumable)
|
||||
return kCatConsumable;
|
||||
if (itemClass == kClassReagent)
|
||||
return kCatReagent;
|
||||
if (itemClass == kClassTradeGoods)
|
||||
return kCatTradeGoods;
|
||||
if (itemClass == kClassQuest)
|
||||
return kCatQuest;
|
||||
if (quality >= kQualityEpic)
|
||||
return kCatOtherEpic;
|
||||
if (quality == kQualityRare)
|
||||
return kCatOtherRare;
|
||||
if (quality == kQualityUncommon)
|
||||
return kCatOtherUncommon;
|
||||
return kCatOtherPlain;
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
const void *item; // CGItem — stable across the swap batch
|
||||
int curBag, curSlot; // live position, updated as swaps fire
|
||||
|
||||
// Destination, or unassigned. The unassigned sentinel is destSLOT,
|
||||
// never destBag: slots are 1-based so a negative one is impossible,
|
||||
// whereas bagID -1 is the main bank. Testing destBag < 0 here treats
|
||||
// every item bound for the main bank as unplaced and silently drops
|
||||
// its move.
|
||||
int destBag, destSlot;
|
||||
uint32_t itemID;
|
||||
uint32_t family; // the item's own bag family (0 = fits anywhere)
|
||||
uint32_t itemClass, subClass, count;
|
||||
const char *name;
|
||||
uint8_t category;
|
||||
};
|
||||
|
||||
struct Cell {
|
||||
int bag, slot;
|
||||
};
|
||||
|
||||
char Lower(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
||||
}
|
||||
|
||||
// A missing name sorts after every present one. Substituting a
|
||||
// placeholder string instead (libbagsort uses "zzz") wedges the unnamed
|
||||
// item in among items actually named with those letters; this keeps it
|
||||
// deterministic and at the end. In practice a nameless item is pinned
|
||||
// before it reaches here, so this is the belt to that braces.
|
||||
int CompareNames(const char *a, const char *b) {
|
||||
if (a == b)
|
||||
return 0;
|
||||
if (a == nullptr)
|
||||
return 1;
|
||||
if (b == nullptr)
|
||||
return -1;
|
||||
for (size_t i = 0;; ++i) {
|
||||
const char ca = Lower(a[i]), cb = Lower(b[i]);
|
||||
if (ca != cb)
|
||||
return ca < cb ? -1 : 1;
|
||||
if (ca == '\0')
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool Precedes(const Entry &a, const Entry &b) {
|
||||
if (a.category != b.category)
|
||||
return a.category < b.category;
|
||||
if (a.itemClass != b.itemClass)
|
||||
return a.itemClass < b.itemClass;
|
||||
if (a.subClass != b.subClass)
|
||||
return a.subClass < b.subClass;
|
||||
const int byName = CompareNames(a.name, b.name);
|
||||
if (byName != 0)
|
||||
return byName < 0;
|
||||
if (a.count != b.count)
|
||||
return a.count > b.count; // fuller stacks first
|
||||
return a.itemID < b.itemID; // total order, so the result is stable
|
||||
}
|
||||
|
||||
// A bag's storage, addressed without Lua: the invMgr holding its GUID
|
||||
// array, the linear index its first slot sits at, and the family it
|
||||
// accepts. The backpack lives inside the player's own invMgr; bags 1..4
|
||||
// each have one of their own.
|
||||
struct BagView {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// The backpack and the main bank both live inside the player's own
|
||||
// invMgr, at different linear bases. Equipped bags and bank bags each
|
||||
// have an invMgr of their own, reached through the bag item.
|
||||
bool ViewForBag(int bagID, BagView *out) {
|
||||
if (bagID == 0) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
return true;
|
||||
}
|
||||
if (bagID == -1) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the bag item that owns the container. Bank bags resolve
|
||||
// only once the bank window has been opened this session, which is
|
||||
// the right gate: the server refuses bank moves without it anyway,
|
||||
// so a bank sort attempted with the bank closed finds no cells and
|
||||
// does nothing rather than firing moves that would all be rejected.
|
||||
int equipSlot = 0;
|
||||
if (bagID >= 1 && bagID <= 4)
|
||||
equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
else if (bagID >= 5 && bagID <= 10)
|
||||
equipSlot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT + 1 + (bagID - 5);
|
||||
else
|
||||
return false;
|
||||
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return false;
|
||||
auto *bagInv = static_cast<const uint8_t *>(
|
||||
Item::Location::ContainerInventory(bagItem));
|
||||
if (bagInv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(bagInv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// A specialty bag only accepts its own family, and Turtle leaves the
|
||||
// raw field empty on bags — `BitmaskForRecord` derives it from the
|
||||
// container subclass, so a quiver reports a family here rather than
|
||||
// reading as general-purpose. Shared with `C_Item.GetItemFamily`, so
|
||||
// a bag and the items it holds agree.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bagItem);
|
||||
if (bagItemID > 0) {
|
||||
auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID));
|
||||
if (rec != nullptr)
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {bagInv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t *ItemAt(const BagView &v, int slotIndex0) {
|
||||
auto *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
v.invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr)
|
||||
return nullptr;
|
||||
const uint64_t guid = guids[v.firstLinear + slotIndex0];
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_ITEM, guid, "SortBags", 0));
|
||||
}
|
||||
|
||||
uint32_t StackCount(const uint8_t *item) {
|
||||
auto *fields = Item::ObjectFields(item);
|
||||
if (fields == nullptr)
|
||||
return 1;
|
||||
const uint32_t n =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
return n == 0 ? 1 : n;
|
||||
}
|
||||
|
||||
// --- phase one: ask the server to consolidate -------------------------------
|
||||
|
||||
// Returns true if any packet went out. Every stack after the first of
|
||||
// its item ID is offered to the server, which merges what fits and
|
||||
// rehomes the rest.
|
||||
//
|
||||
// When the item's record is cached we skip the ones that provably cannot
|
||||
// stack, purely to save a packet. When it is NOT cached we send anyway:
|
||||
// the alternative is silently skipping a merge we could not size, which
|
||||
// is the failure this whole design avoids.
|
||||
// One stack per group of the same item, as gathered for either merge.
|
||||
struct Stack {
|
||||
const uint8_t *item;
|
||||
int bag, slot;
|
||||
uint32_t count;
|
||||
};
|
||||
struct Group {
|
||||
uint32_t itemID;
|
||||
uint32_t maxStack; // 0 when the item's data has not arrived
|
||||
std::vector<Stack> stacks;
|
||||
};
|
||||
|
||||
// Collects every stackable item across `bags`, grouped by item ID.
|
||||
// `maxStack` is 0 for an item whose data has not arrived; the callers
|
||||
// differ in what they do about that.
|
||||
std::vector<Group> GatherGroups(const int *bags, int bagCount) {
|
||||
std::vector<Group> groups;
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
const uint8_t *item = ItemAt(view, i);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
const int itemID = Item::ID::FromCGItem(item);
|
||||
if (itemID <= 0)
|
||||
continue;
|
||||
const uint32_t id = static_cast<uint32_t>(itemID);
|
||||
|
||||
uint32_t maxStack = 0;
|
||||
auto *rec = Item::PeekRecord(id);
|
||||
if (rec != nullptr) {
|
||||
maxStack =
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
if (maxStack <= 1)
|
||||
continue; // cannot stack at all, so never a merge candidate
|
||||
}
|
||||
|
||||
Group *group = nullptr;
|
||||
for (auto &candidate : groups) {
|
||||
if (candidate.itemID == id) {
|
||||
group = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (group == nullptr) {
|
||||
groups.push_back({id, maxStack, {}});
|
||||
group = &groups.back();
|
||||
}
|
||||
group->stacks.push_back({item, bag, i + 1, StackCount(item)});
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// Why this is computed here rather than delegated to the server's own
|
||||
// autostore, which can merge a stack into several destinations for one
|
||||
// packet and would otherwise be cheaper:
|
||||
//
|
||||
// `CMSG_AUTOSTORE_BAG_ITEM` names its destination with a container byte,
|
||||
// and the only value the engine's builder will emit for "the player" is
|
||||
// `INVENTORY_SLOT_BAG_0` (255). Server-side that is NOT the search-every-
|
||||
// bag path — that one is `NULL_BAG` (0), which the builder cannot
|
||||
// express. 255 takes the specific-container branch, which searches the
|
||||
// keyring and the backpack and nothing else. So autostore can never
|
||||
// merge a stack in one equipped bag with a stack in another, which is
|
||||
// most of the real cases.
|
||||
//
|
||||
// Addressing both endpoints explicitly costs a packet per transfer
|
||||
// instead of per emptied source — occasionally one more, when a stack
|
||||
// has to be split across several destinations — and works everywhere.
|
||||
|
||||
// Bank consolidation has to be computed here. `CMSG_AUTOSTORE_BANK_ITEM`
|
||||
// always CROSSES the inventory/bank boundary — a bank-sourced item
|
||||
// travels out to the bags — so there is no "find this a fuller stack
|
||||
// inside the bank" packet to delegate to. This is the one place the
|
||||
// protocol forces a client-side merge, and it carries the cost that
|
||||
// comes with that: it needs each item's max stack size, which is
|
||||
// unavailable until that item's data has arrived, so a stack we cannot
|
||||
// size is left alone.
|
||||
//
|
||||
// Two rolling pointers per item: stacks fullest-first, then poured from
|
||||
// the back into the front until they meet.
|
||||
bool Consolidate(const int *bags, int bagCount) {
|
||||
std::vector<Group> groups = GatherGroups(bags, bagCount);
|
||||
|
||||
bool fired = false;
|
||||
for (auto &group : groups) {
|
||||
if (group.stacks.size() < 2)
|
||||
continue;
|
||||
|
||||
// Pack fully, not just enough to free a slot: 9 and 2 with a cap
|
||||
// of 10 become 10 and 1, even though that still occupies two
|
||||
// slots. Topping stacks up is the point of consolidating.
|
||||
//
|
||||
// This settles after one pass and does not re-fire on the next
|
||||
// call, because a maximally packed group has every stack full
|
||||
// except the last — so `space == 0` walks `lo` straight up to
|
||||
// `hi` and nothing is sent.
|
||||
std::sort(group.stacks.begin(), group.stacks.end(),
|
||||
[](const Stack &a, const Stack &b) { return a.count > b.count; });
|
||||
size_t lo = 0, hi = group.stacks.size() - 1;
|
||||
while (lo < hi) {
|
||||
const uint32_t space = group.maxStack - group.stacks[lo].count;
|
||||
if (space == 0) {
|
||||
++lo;
|
||||
continue;
|
||||
}
|
||||
const bool wholeStack = space >= group.stacks[hi].count;
|
||||
const uint32_t moved =
|
||||
wholeStack ? group.stacks[hi].count : space;
|
||||
|
||||
// Choose the packet from OUR model, never from the item's
|
||||
// live stack count. The whole batch goes out in one frame,
|
||||
// so no reply has arrived and every descriptor still holds
|
||||
// its original count while the model has moved on. Asking
|
||||
// the descriptor would send a split for what is really the
|
||||
// last of a stack, and the server rejects a split that would
|
||||
// empty its source ("Couldn't split those items").
|
||||
const bool sent =
|
||||
wholeStack
|
||||
? Item::Swap::ContainersFrom(
|
||||
group.stacks[hi].item, group.stacks[hi].bag,
|
||||
group.stacks[hi].slot, group.stacks[lo].bag,
|
||||
group.stacks[lo].slot)
|
||||
: Item::Swap::MoveCountFrom(
|
||||
group.stacks[hi].item, group.stacks[hi].bag,
|
||||
group.stacks[hi].slot, group.stacks[lo].bag,
|
||||
group.stacks[lo].slot, static_cast<int>(moved));
|
||||
if (sent) {
|
||||
fired = true;
|
||||
++g_mergePackets;
|
||||
// Only a whole-stack transfer empties its source, and
|
||||
// `mergesPending` means "sources that should be empty by
|
||||
// now but are not".
|
||||
if (wholeStack)
|
||||
g_mergeSources.push_back(
|
||||
{group.stacks[hi].bag, group.stacks[hi].slot});
|
||||
}
|
||||
group.stacks[lo].count += moved;
|
||||
group.stacks[hi].count -= moved;
|
||||
if (group.stacks[hi].count == 0)
|
||||
--hi; // that stack is gone
|
||||
else
|
||||
++lo; // this one is full
|
||||
}
|
||||
}
|
||||
return fired;
|
||||
}
|
||||
|
||||
// --- phase two: plan a layout and fire the swaps ----------------------------
|
||||
|
||||
// Counters from the most recent Place(), read back by
|
||||
// `_classicapi_SortBagsStats()`. They exist to tell apart the three ways
|
||||
// a pass can fail to converge: swaps the engine refused to send, items
|
||||
// the planner found no cell for, and a plan that was complete and fully
|
||||
// sent yet still left the bags unsorted (which would mean a swap did
|
||||
// something other than swap).
|
||||
struct Stats {
|
||||
int entries;
|
||||
int cells;
|
||||
int pinned;
|
||||
int planned;
|
||||
int unplanned;
|
||||
int swapsSent;
|
||||
int swapsFailed;
|
||||
int sameItemAvoided;
|
||||
int generalCells; // destination cells in general-purpose containers
|
||||
int specialtyCells; // cells confined to one bag family
|
||||
int familyItems; // items that want a specialty bag
|
||||
int mergesPending; // phase-one sources still occupied at plan time
|
||||
};
|
||||
Stats g_lastRun{};
|
||||
|
||||
void Place(const int *bags, int bagCount) {
|
||||
Stats stats{};
|
||||
std::vector<Entry> items;
|
||||
std::vector<Cell> general;
|
||||
|
||||
// Vanilla items carry at most one family bit, so a flat list keyed by
|
||||
// family is enough — no bitmask intersection needed.
|
||||
struct FamilyPool {
|
||||
uint32_t family;
|
||||
std::vector<Cell> cells;
|
||||
size_t next;
|
||||
};
|
||||
std::vector<FamilyPool> specialty;
|
||||
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
const int slot = i + 1;
|
||||
const uint8_t *item = ItemAt(view, i);
|
||||
|
||||
const uint8_t *rec = nullptr;
|
||||
int itemID = 0;
|
||||
if (item != nullptr) {
|
||||
itemID = Item::ID::FromCGItem(item);
|
||||
if (itemID > 0)
|
||||
rec = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (rec == nullptr) {
|
||||
++stats.pinned;
|
||||
continue; // pinned: unrankable, and its slot is withheld
|
||||
}
|
||||
}
|
||||
|
||||
if (view.family == 0) {
|
||||
general.push_back({bag, slot});
|
||||
} else {
|
||||
FamilyPool *pool = nullptr;
|
||||
for (auto &p : specialty) {
|
||||
if (p.family == view.family) {
|
||||
pool = &p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pool == nullptr) {
|
||||
specialty.push_back({view.family, {}, 0});
|
||||
pool = &specialty.back();
|
||||
}
|
||||
pool->cells.push_back({bag, slot});
|
||||
}
|
||||
|
||||
if (item == nullptr)
|
||||
continue; // an empty slot contributes a cell and nothing else
|
||||
|
||||
Entry e{};
|
||||
e.item = item;
|
||||
e.curBag = bag;
|
||||
e.curSlot = slot;
|
||||
e.destBag = -1;
|
||||
e.destSlot = -1;
|
||||
e.itemID = static_cast<uint32_t>(itemID);
|
||||
e.family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
e.itemClass = Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_CLASS);
|
||||
e.subClass = Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
e.count = StackCount(item);
|
||||
e.name = *reinterpret_cast<const char *const *>(
|
||||
rec + Offsets::OFF_ITEMSTATS_NAME);
|
||||
e.category = CategoryFor(
|
||||
e.itemID, e.itemClass,
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_QUALITY));
|
||||
items.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
stats.entries = static_cast<int>(items.size());
|
||||
stats.generalCells = static_cast<int>(general.size());
|
||||
for (const auto &p : specialty)
|
||||
stats.specialtyCells += static_cast<int>(p.cells.size());
|
||||
stats.cells = stats.generalCells + stats.specialtyCells;
|
||||
for (const auto &e : items)
|
||||
if (e.family != 0)
|
||||
++stats.familyItems;
|
||||
|
||||
// Any source phase one asked the server to empty that is still
|
||||
// occupied means this snapshot predates some of the merges.
|
||||
for (const auto &src : g_mergeSources) {
|
||||
for (const auto &e : items) {
|
||||
if (e.curBag == src.bag && e.curSlot == src.slot) {
|
||||
++stats.mergesPending;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.empty()) {
|
||||
g_lastRun = stats;
|
||||
return;
|
||||
}
|
||||
|
||||
std::sort(items.begin(), items.end(), Precedes);
|
||||
|
||||
// Right-to-left is expressed entirely as reversed cell lists: the
|
||||
// passes below still fill from the "front" and the "far end", those
|
||||
// ends have just swapped which physical slot they mean. So the
|
||||
// sorted run starts at the last slot of the last bag and junk
|
||||
// collects at the first slot of the first, both flipping together.
|
||||
if (RightToLeft()) {
|
||||
std::reverse(general.begin(), general.end());
|
||||
for (auto &p : specialty)
|
||||
std::reverse(p.cells.begin(), p.cells.end());
|
||||
}
|
||||
|
||||
// Forward pass for everything but junk: a matching specialty bag
|
||||
// first, overflowing into the general cells. Junk then fills the
|
||||
// general cells from the far end, so it collects away from the rest
|
||||
// and the two passes cannot claim the same cell.
|
||||
size_t genFront = 0;
|
||||
size_t genBack = general.size();
|
||||
for (auto &e : items) {
|
||||
if (e.category == kCatJunk)
|
||||
continue;
|
||||
Cell *cell = nullptr;
|
||||
if (e.family != 0) {
|
||||
for (auto &p : specialty) {
|
||||
if (p.family == e.family && p.next < p.cells.size()) {
|
||||
cell = &p.cells[p.next++];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cell == nullptr && genFront < genBack)
|
||||
cell = &general[genFront++];
|
||||
if (cell == nullptr)
|
||||
continue; // nowhere left to put it; leave it where it is
|
||||
e.destBag = cell->bag;
|
||||
e.destSlot = cell->slot;
|
||||
}
|
||||
for (auto it = items.rbegin(); it != items.rend(); ++it) {
|
||||
if (it->category != kCatJunk)
|
||||
continue;
|
||||
if (genBack <= genFront)
|
||||
break;
|
||||
const Cell &cell = general[--genBack];
|
||||
it->destBag = cell.bag;
|
||||
it->destSlot = cell.slot;
|
||||
}
|
||||
|
||||
// NEVER plan a swap between two stacks of the same item. The server's
|
||||
// swap handler tries a MERGE first and only falls through to a real
|
||||
// swap if the merge is impossible: if the two counts fit one stack it
|
||||
// collapses them into a single slot, and if they do not it
|
||||
// redistributes the counts and leaves both items where they are.
|
||||
// Neither is a swap, so the bookkeeping below would be wrong from
|
||||
// that point on and every later move in the batch would compound it.
|
||||
//
|
||||
// Two stacks of one item are interchangeable, though, so this costs
|
||||
// nothing: within each same-item run, hand each destination cell to
|
||||
// whichever member already occupies it. The run is contiguous because
|
||||
// identical items agree on every key ahead of count.
|
||||
for (size_t i = 0; i < items.size();) {
|
||||
size_t j = i;
|
||||
while (j < items.size() && items[j].itemID == items[i].itemID)
|
||||
++j;
|
||||
for (size_t m = i; m < j; ++m) {
|
||||
for (size_t n = i; n < j; ++n) {
|
||||
if (n == m)
|
||||
continue; // its own destination matching is just "home"
|
||||
if (items[n].destSlot < 0)
|
||||
continue; // unassigned; nothing to trade
|
||||
if (items[n].destBag == items[m].curBag &&
|
||||
items[n].destSlot == items[m].curSlot) {
|
||||
std::swap(items[m].destBag, items[n].destBag);
|
||||
std::swap(items[m].destSlot, items[n].destSlot);
|
||||
++stats.sameItemAvoided;
|
||||
break; // m now targets the slot it is already in
|
||||
}
|
||||
}
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
|
||||
// Fire the batch. Each entry carries its live position, so the plan
|
||||
// stays valid as slots shuffle and nothing re-reads the bags. The
|
||||
// occupant of a destination takes the mover's old slot, which is what
|
||||
// the engine's swap does anyway.
|
||||
for (auto &e : items) {
|
||||
if (e.destSlot < 0) { // see the sentinel note on Entry
|
||||
++stats.unplanned;
|
||||
continue;
|
||||
}
|
||||
++stats.planned;
|
||||
if (e.destBag == e.curBag && e.destSlot == e.curSlot)
|
||||
continue;
|
||||
Entry *occupant = nullptr;
|
||||
for (auto &o : items) {
|
||||
if (&o != &e && o.curBag == e.destBag && o.curSlot == e.destSlot) {
|
||||
occupant = &o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int fromBag = e.curBag, fromSlot = e.curSlot;
|
||||
if (!Item::Swap::ContainersFrom(e.item, fromBag, fromSlot, e.destBag,
|
||||
e.destSlot)) {
|
||||
++stats.swapsFailed;
|
||||
continue;
|
||||
}
|
||||
++stats.swapsSent;
|
||||
e.curBag = e.destBag;
|
||||
e.curSlot = e.destSlot;
|
||||
if (occupant != nullptr) {
|
||||
occupant->curBag = fromBag;
|
||||
occupant->curSlot = fromSlot;
|
||||
}
|
||||
}
|
||||
g_lastRun = stats;
|
||||
}
|
||||
|
||||
// --- sequencing -------------------------------------------------------------
|
||||
|
||||
enum Phase { kIdle, kAwaitingMerge };
|
||||
Phase g_phase = kIdle;
|
||||
|
||||
// Which set the in-flight sort is arranging, so the deferred placement
|
||||
// pass knows what to walk. Points at one of the two static lists, so
|
||||
// there is nothing to own or free.
|
||||
const int *g_activeBags = nullptr;
|
||||
int g_activeBagCount = 0;
|
||||
|
||||
void OnBagUpdate() {
|
||||
if (g_phase != kAwaitingMerge)
|
||||
return;
|
||||
// Clear the phase BEFORE placing: the swaps we are about to fire will
|
||||
// signal this callback again next frame, and we must be idle by then
|
||||
// or the sort would restart forever.
|
||||
g_phase = kIdle;
|
||||
Place(g_activeBags, g_activeBagCount);
|
||||
}
|
||||
|
||||
const Bag::UpdateDelayed::AutoSubscribe _bagSub{&OnBagUpdate};
|
||||
|
||||
// Shared entry point. Bags and bank differ only in the list.
|
||||
void StartSort(const int *bags, int bagCount) {
|
||||
if (g_phase != kIdle)
|
||||
return; // a sort is already in flight
|
||||
g_activeBags = bags;
|
||||
g_activeBagCount = bagCount;
|
||||
g_mergePackets = 0;
|
||||
g_mergeSources.clear();
|
||||
if (Consolidate(bags, bagCount))
|
||||
g_phase = kAwaitingMerge; // let the merges land, then place
|
||||
else
|
||||
Place(bags, bagCount); // nothing to merge; the layout reads clean
|
||||
}
|
||||
|
||||
// `C_Container.SortBags()` / `C_Container.SortBankBags()` — no
|
||||
// arguments, no return, matching the modern signatures. Fire-and-forget:
|
||||
// the work spans at least two frames. A call while a sort is already in
|
||||
// flight is ignored rather than queued, so a held-down keybind cannot
|
||||
// stack them up — and neither can sorting bags and bank at once.
|
||||
// Runs a sort over `bags`, dropping `excluded` when its autosort setting
|
||||
// says to leave that container alone. Dropping a bag from the set is all
|
||||
// it takes: the snapshot, the comparator and the placement only ever see
|
||||
// the bags they are handed, so an excluded bag is neither read from nor
|
||||
// written to.
|
||||
void StartSortExcluding(const int *bags, int count, int excluded,
|
||||
bool excludeIt) {
|
||||
if (!excludeIt) {
|
||||
StartSort(bags, count);
|
||||
return;
|
||||
}
|
||||
std::vector<int> kept;
|
||||
kept.reserve(static_cast<size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (bags[i] != excluded)
|
||||
kept.push_back(bags[i]);
|
||||
}
|
||||
if (!kept.empty())
|
||||
StartSort(kept.data(), static_cast<int>(kept.size()));
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_SortBags(void * /*L*/) {
|
||||
StartSortExcluding(kInventoryBags,
|
||||
sizeof(kInventoryBags) / sizeof(kInventoryBags[0]),
|
||||
/*excluded*/ 0, BackpackAutosortDisabled());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_SortBankBags(void * /*L*/) {
|
||||
StartSortExcluding(kBankBags, sizeof(kBankBags) / sizeof(kBankBags[0]),
|
||||
/*excluded*/ -1, BankAutosortDisabled());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `_classicapi_SortBagsStats()` -> entries, cells, pinned, planned,
|
||||
// unplanned, swapsSent, swapsFailed, sameItemAvoided — from the last
|
||||
// placement pass. Diagnostic only; not part of the API surface.
|
||||
int __fastcall Script_SortBagsStats(void *L) {
|
||||
Game::Lua::PushNumber(L, g_lastRun.entries);
|
||||
Game::Lua::PushNumber(L, g_lastRun.cells);
|
||||
Game::Lua::PushNumber(L, g_lastRun.pinned);
|
||||
Game::Lua::PushNumber(L, g_lastRun.planned);
|
||||
Game::Lua::PushNumber(L, g_lastRun.unplanned);
|
||||
Game::Lua::PushNumber(L, g_lastRun.swapsSent);
|
||||
Game::Lua::PushNumber(L, g_lastRun.swapsFailed);
|
||||
Game::Lua::PushNumber(L, g_lastRun.sameItemAvoided);
|
||||
Game::Lua::PushNumber(L, g_lastRun.generalCells);
|
||||
Game::Lua::PushNumber(L, g_lastRun.specialtyCells);
|
||||
Game::Lua::PushNumber(L, g_lastRun.familyItems);
|
||||
Game::Lua::PushNumber(L, g_mergePackets);
|
||||
Game::Lua::PushNumber(L, g_lastRun.mergesPending);
|
||||
return 13;
|
||||
}
|
||||
|
||||
// `_classicapi_SortBagsBags(which)` -> bagID, slotCount, family for each
|
||||
// bag that resolves in that set (0 = the inventory set, 1 = the bank
|
||||
// set). A bag that does not resolve is skipped entirely, so a missing ID
|
||||
// in the output is itself the answer. Diagnostic only.
|
||||
int __fastcall Script_SortBagsBags(void *L) {
|
||||
const bool bank = Game::Lua::IsNumber(L, 1) &&
|
||||
static_cast<int>(Game::Lua::ToNumber(L, 1)) == 1;
|
||||
const int *bags = bank ? kBankBags : kInventoryBags;
|
||||
const int count = bank
|
||||
? sizeof(kBankBags) / sizeof(kBankBags[0])
|
||||
: sizeof(kInventoryBags) / sizeof(kInventoryBags[0]);
|
||||
int pushed = 0;
|
||||
for (int b = 0; b < count; ++b) {
|
||||
BagView view;
|
||||
if (!ViewForBag(bags[b], &view))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, bags[b]);
|
||||
Game::Lua::PushNumber(L, view.slotCount);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(view.family));
|
||||
pushed += 3;
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
|
||||
// `C_Container.SetSortBagsRightToLeft(enable)` — no return, matching the
|
||||
// modern signature. Takes effect on the next sort; it does not re-sort.
|
||||
int __fastcall Script_SetSortBagsRightToLeft(void *L) {
|
||||
CVar::Factory::SetInt(g_rightToLeftCVar,
|
||||
Game::Lua::ToBoolean(L, 1) != 0 ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `C_Container.GetSortBagsRightToLeft()` -> isEnabled
|
||||
int __fastcall Script_GetSortBagsRightToLeft(void *L) {
|
||||
Game::Lua::PushBool(L, RightToLeft());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Container.SetBackpackAutosortDisabled(disable)` /
|
||||
// `C_Container.SetBankAutosortDisabled(disable)` — keep that container
|
||||
// out of `SortBags` / `SortBankBags`. No return, matching the modern
|
||||
// signatures; the paired getters report the stored setting.
|
||||
int __fastcall Script_SetBackpackAutosortDisabled(void *L) {
|
||||
CVar::Factory::SetInt(g_backpackAutosortCVar,
|
||||
Game::Lua::ToBoolean(L, 1) != 0 ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBackpackAutosortDisabled(void *L) {
|
||||
Game::Lua::PushBool(L, BackpackAutosortDisabled());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBankAutosortDisabled(void *L) {
|
||||
CVar::Factory::SetInt(g_bankAutosortCVar,
|
||||
Game::Lua::ToBoolean(L, 1) != 0 ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBankAutosortDisabled(void *L) {
|
||||
Game::Lua::PushBool(L, BankAutosortDisabled());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// Dedups by name, so re-registering each reload is harmless.
|
||||
g_rightToLeftCVar = CVar::Factory::Register("sortBagsRightToLeft", "0");
|
||||
g_backpackAutosortCVar =
|
||||
CVar::Factory::Register("backpackAutosortDisabled", "0");
|
||||
g_bankAutosortCVar = CVar::Factory::Register("bankAutosortDisabled", "0");
|
||||
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SortBags",
|
||||
&Script_C_Container_SortBags);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetSortBagsRightToLeft",
|
||||
&Script_SetSortBagsRightToLeft);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetSortBagsRightToLeft",
|
||||
&Script_GetSortBagsRightToLeft);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SortBankBags",
|
||||
&Script_C_Container_SortBankBags);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetBackpackAutosortDisabled",
|
||||
&Script_SetBackpackAutosortDisabled);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetBackpackAutosortDisabled",
|
||||
&Script_GetBackpackAutosortDisabled);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetBankAutosortDisabled",
|
||||
&Script_SetBankAutosortDisabled);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetBankAutosortDisabled",
|
||||
&Script_GetBankAutosortDisabled);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_SortBagsStats",
|
||||
&Script_SortBagsStats);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_SortBagsBags",
|
||||
&Script_SortBagsBags);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::SortBags
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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/>.
|
||||
|
||||
// `CURSOR_CHANGED` — fires when what the cursor holds changes, carrying
|
||||
// the old and new cursor types:
|
||||
//
|
||||
// CURSOR_CHANGED: isDefault, newCursorType, oldCursorType, oldCursorVirtualID
|
||||
//
|
||||
// Plus the `Enum.UICursorType` table the two type arguments index.
|
||||
//
|
||||
// The trigger is a once-per-frame diff of the cursor globals, NOT the
|
||||
// engine's own `CURSOR_UPDATE`, because that event does not cover the
|
||||
// cursor. Its only three fire sites are:
|
||||
//
|
||||
// FUN_00495190 the cursor CLEAR — fires for every type, but only
|
||||
// after it has already zeroed the type global, so the
|
||||
// read at that point always says "empty"
|
||||
// FUN_00494b60 bag-item set — fires only when a GUID is present
|
||||
// FUN_004950f0 generic-item set — fires only when an itemID is
|
||||
// present
|
||||
//
|
||||
// So picking up money, a spell, a macro, a pet action or a stabled pet
|
||||
// produces exactly one `CURSOR_UPDATE`, from the clear, describing an
|
||||
// empty cursor; the write that puts the new content in place announces
|
||||
// nothing. Only the two item paths fire again after the set, which is
|
||||
// why an interceptor on that event reported item cursors correctly and
|
||||
// nothing else. There is no common choke point on the set side — each
|
||||
// type writes `VAR_CURSOR_TYPE` in its own path — so a state diff is
|
||||
// what covers every type, including any path not enumerated above.
|
||||
//
|
||||
// `Info::ReadRaw` is the cheap half of that diff: plain global reads, no
|
||||
// object-manager lookup. Only once it differs do we pay for
|
||||
// `Info::Current()`, which resolves a bag item's itemID.
|
||||
//
|
||||
// `oldCursorVirtualID` is the one argument that is a judgement call: the
|
||||
// modern client's notion of a "virtual ID" is not something this client
|
||||
// has, so it carries the previous content's identifying number instead
|
||||
// (see `Cursor::Info::State::virtualID`). No consumer is known to read
|
||||
// it — Blizzard's own `CURSOR_CHANGED` users all treat the event as a
|
||||
// bare notification and re-check state themselves.
|
||||
|
||||
#include "Game.h"
|
||||
#include "cursor/Info.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Changed {
|
||||
|
||||
namespace {
|
||||
|
||||
const Event::Custom::AutoReserve _evtCursorChanged{"CURSOR_CHANGED"};
|
||||
|
||||
// The full modern enum, not just the values this client can produce. An
|
||||
// addon comparing against `Enum.UICursorType.BattlePet` should read a
|
||||
// number it can never match, rather than index a nil field.
|
||||
const Game::Lua::EnumIntegerEntry kCursorTypeEntries[] = {
|
||||
{"Default", 0}, {"Item", 1},
|
||||
{"Money", 2}, {"Spell", 3},
|
||||
{"PetAction", 4}, {"Merchant", 5},
|
||||
{"ActionBar", 6}, {"Macro", 7},
|
||||
{"Ammo", 8}, {"Pet", 9},
|
||||
{"GuildBank", 10}, {"GuildBankMoney", 11},
|
||||
{"EquipmentSet", 12}, {"Currency", 13},
|
||||
{"Flyout", 14}, {"VoidItem", 15},
|
||||
{"BattlePet", 16}, {"Mount", 17},
|
||||
{"Toy", 18}, {"ConduitCollectionItem", 19},
|
||||
{"PerksProgramVendorItem", 20},
|
||||
};
|
||||
|
||||
// The cursor as of the last event we fired — the raw form for the cheap
|
||||
// per-frame compare, and the reported form to supply the `old*` arguments
|
||||
// of the next fire. Deliberately NOT reset on `/reload`: the DLL and the
|
||||
// engine's cursor globals both outlive the reload, so keeping them means
|
||||
// a cursor still holding something across a reload reports its real
|
||||
// previous type rather than a phantom change back from `Default`.
|
||||
Info::Raw g_lastRaw{};
|
||||
Info::State g_lastState{0, 0};
|
||||
|
||||
void OnTick() {
|
||||
const Info::Raw raw = Info::ReadRaw();
|
||||
if (Info::Same(raw, g_lastRaw))
|
||||
return; // cursor untouched this frame — the common case
|
||||
g_lastRaw = raw;
|
||||
|
||||
const Info::State now = Info::Current();
|
||||
const Info::State previous = g_lastState;
|
||||
g_lastState = now;
|
||||
|
||||
const int slot = _evtCursorChanged.Slot();
|
||||
if (slot < 0)
|
||||
return;
|
||||
|
||||
// The dispatcher has no boolean code, and pushing `%d` 0 would hand
|
||||
// Lua the number 0 — truthy. `%s` with a null string tail-jumps to
|
||||
// pushnil instead, so `if isDefault then` reads correctly either
|
||||
// way. Same trick the PLAYER_ENTERING_WORLD payload uses.
|
||||
auto *const kNil = static_cast<const char *>(nullptr);
|
||||
if (now.uiType == 0) {
|
||||
Event::Custom::Fire(slot, "%d%d%d%u", 1, now.uiType, previous.uiType,
|
||||
previous.virtualID);
|
||||
} else {
|
||||
Event::Custom::Fire(slot, "%s%d%d%u", kNil, now.uiType,
|
||||
previous.uiType, previous.virtualID);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "UICursorType", kCursorTypeEntries,
|
||||
sizeof(kCursorTypeEntries) / sizeof(kCursorTypeEntries[0]));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Cursor::Changed
|
||||
+101
-7
@@ -97,10 +97,25 @@ constexpr uint32_t TYPE_GENERIC_ITEM_LO = 6;
|
||||
constexpr uint32_t TYPE_GENERIC_ITEM_HI = 7;
|
||||
constexpr uint32_t TYPE_MACRO = 8;
|
||||
constexpr uint32_t TYPE_INVENTORY_ITEM = 9;
|
||||
// Type 10 is stable-pet pickup (`FUN_00495010`, the source of the
|
||||
// earlier misidentification as "merchant"). Modern WoW has no
|
||||
// equivalent `GetCursorInfo` return for stable pets, so we leave it
|
||||
// as nil.
|
||||
// Type 4 is pet-action pickup and type 10 is stable-pet pickup
|
||||
// (`FUN_00495010`, the source of the earlier misidentification as
|
||||
// "merchant"). Neither has an equivalent `GetCursorInfo` return, so
|
||||
// that function leaves both as nil — but `Enum.UICursorType` does name
|
||||
// them, so `Current()` below reports them.
|
||||
constexpr uint32_t TYPE_PET_ACTION = 4;
|
||||
constexpr uint32_t TYPE_STABLE_PET = 10;
|
||||
|
||||
// `Enum.UICursorType` values, for `Current()`. The full enum is
|
||||
// registered in [[cursor/Changed.cpp]]; only the handful the 1.12
|
||||
// cursor can actually hold are named here.
|
||||
constexpr int UI_DEFAULT = 0;
|
||||
constexpr int UI_ITEM = 1;
|
||||
constexpr int UI_MONEY = 2;
|
||||
constexpr int UI_SPELL = 3;
|
||||
constexpr int UI_PET_ACTION = 4;
|
||||
constexpr int UI_MERCHANT = 5;
|
||||
constexpr int UI_MACRO = 7;
|
||||
constexpr int UI_PET = 9;
|
||||
|
||||
constexpr uint32_t GUID_TYPE_FILTER_ITEM = 0x2;
|
||||
|
||||
@@ -108,6 +123,23 @@ uint32_t ReadVar(uintptr_t va) {
|
||||
return *reinterpret_cast<const uint32_t *>(va);
|
||||
}
|
||||
|
||||
// The macro cursor stores a macroID, not the slot Lua deals in (see
|
||||
// `VAR_CURSOR_MACRO_ID`). Hand it back through the engine's own reverse
|
||||
// scan of the slot map — the same conversion the legacy `CreateMacro`
|
||||
// wrapper does to produce the index it returns to Lua. 0 when the
|
||||
// macroID no longer maps to a slot, which is what an empty cursor
|
||||
// reports too.
|
||||
uint32_t CursorMacroSlot() {
|
||||
const uint32_t macroID = ReadVar(Offsets::VAR_CURSOR_MACRO_ID);
|
||||
if (macroID == 0)
|
||||
return 0;
|
||||
using IdToSlot_t = uint32_t(__fastcall *)(int macroID);
|
||||
const uint32_t slot0 = reinterpret_cast<IdToSlot_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_MACRO_ID_TO_SLOT))(
|
||||
static_cast<int>(macroID));
|
||||
return (slot0 == 0xFFFFFFFFu) ? 0 : slot0 + 1;
|
||||
}
|
||||
|
||||
int PushItem(void *L, const uint8_t *cgItem) {
|
||||
if (cgItem == nullptr)
|
||||
return 0;
|
||||
@@ -222,11 +254,13 @@ int PushSpellCursor(void *L) {
|
||||
}
|
||||
|
||||
int PushMacroCursor(void *L) {
|
||||
const uint32_t index = ReadVar(Offsets::VAR_CURSOR_MACRO_INDEX);
|
||||
if (index == 0)
|
||||
// The 1-based slot, so the value feeds straight into `GetMacroInfo`
|
||||
// — the cursor global itself holds a macroID.
|
||||
const uint32_t slot = CursorMacroSlot();
|
||||
if (slot == 0)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, "macro");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(index));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot));
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -270,4 +304,64 @@ bool HasItem() {
|
||||
ReadVar(Offsets::VAR_CURSOR_ITEM_GUID_HI) != 0;
|
||||
}
|
||||
|
||||
State Current() {
|
||||
const uint32_t type = ReadVar(Offsets::VAR_CURSOR_TYPE);
|
||||
switch (type) {
|
||||
case TYPE_BAG_ITEM: {
|
||||
const uint32_t lo = ReadVar(Offsets::VAR_CURSOR_ITEM_GUID_LO);
|
||||
const uint32_t hi = ReadVar(Offsets::VAR_CURSOR_ITEM_GUID_HI);
|
||||
if (lo == 0 && hi == 0)
|
||||
break; // type set but storage not written yet
|
||||
// Same resolve `Script_GetCursorInfo` does for this type —
|
||||
// FrameXML already calls into it from a CURSOR_UPDATE
|
||||
// handler, so the object manager is queryable at this point
|
||||
// in the dispatch.
|
||||
auto resolver =
|
||||
reinterpret_cast<ObjectFromGUID_t>(Offsets::FUN_OBJECT_FROM_GUID);
|
||||
const uint8_t *cgItem = resolver(GUID_TYPE_FILTER_ITEM, lo, hi, 0);
|
||||
const int itemID =
|
||||
(cgItem != nullptr) ? Item::ID::FromCGItem(cgItem) : 0;
|
||||
return {UI_ITEM, itemID > 0 ? static_cast<uint32_t>(itemID) : 0u};
|
||||
}
|
||||
case TYPE_GENERIC_ITEM_LO:
|
||||
case TYPE_GENERIC_ITEM_HI:
|
||||
case TYPE_INVENTORY_ITEM:
|
||||
return {UI_ITEM, ReadVar(Offsets::VAR_CURSOR_GENERIC_SLOT)};
|
||||
case TYPE_MONEY:
|
||||
return {UI_MONEY, ReadVar(Offsets::VAR_CURSOR_MONEY_COPPER)};
|
||||
case TYPE_SPELL:
|
||||
return {UI_SPELL, ReadVar(Offsets::VAR_CURSOR_SPELL_ID)};
|
||||
case TYPE_MERCHANT:
|
||||
// 1-based to match what `GetCursorInfo` pushes for this type.
|
||||
return {UI_MERCHANT,
|
||||
ReadVar(Offsets::VAR_CURSOR_GENERIC_SOURCE) + 1};
|
||||
case TYPE_MACRO:
|
||||
return {UI_MACRO, CursorMacroSlot()};
|
||||
case TYPE_PET_ACTION:
|
||||
return {UI_PET_ACTION, 0};
|
||||
case TYPE_STABLE_PET:
|
||||
return {UI_PET, 0};
|
||||
case TYPE_EMPTY:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {UI_DEFAULT, 0};
|
||||
}
|
||||
|
||||
Raw ReadRaw() {
|
||||
return {ReadVar(Offsets::VAR_CURSOR_TYPE),
|
||||
ReadVar(Offsets::VAR_CURSOR_ITEM_GUID_LO),
|
||||
ReadVar(Offsets::VAR_CURSOR_ITEM_GUID_HI),
|
||||
ReadVar(Offsets::VAR_CURSOR_GENERIC_SLOT),
|
||||
ReadVar(Offsets::VAR_CURSOR_MONEY_COPPER),
|
||||
ReadVar(Offsets::VAR_CURSOR_SPELL_ID),
|
||||
ReadVar(Offsets::VAR_CURSOR_MACRO_ID)};
|
||||
}
|
||||
|
||||
bool Same(const Raw &a, const Raw &b) {
|
||||
return a.type == b.type && a.guidLo == b.guidLo && a.guidHi == b.guidHi &&
|
||||
a.genericSlot == b.genericSlot && a.money == b.money &&
|
||||
a.spellID == b.spellID && a.macroID == b.macroID;
|
||||
}
|
||||
|
||||
} // namespace Cursor::Info
|
||||
|
||||
@@ -13,8 +13,50 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Info {
|
||||
|
||||
// What the cursor is holding, in the terms `CURSOR_CHANGED` reports.
|
||||
struct State {
|
||||
// `Enum.UICursorType` value — `Default` (0) when the cursor is empty.
|
||||
// Agrees with the type string `GetCursorInfo` returns wherever that
|
||||
// function returns one; the pet-action and stable-pet cursors have no
|
||||
// `GetCursorInfo` string but do have an enum value, so they report
|
||||
// `PetAction` / `Pet` here while `GetCursorInfo` stays nil.
|
||||
int uiType;
|
||||
// The held content's identifying number: itemID, spellID, 1-based
|
||||
// macro slot, 1-based merchant slot, or a money amount in copper. 0
|
||||
// for an empty cursor and for the two pet cursors, whose payload the
|
||||
// engine does not keep anywhere readable.
|
||||
uint32_t virtualID;
|
||||
};
|
||||
|
||||
// Reads the live cursor globals. Side-effect free. Resolving a bag item's
|
||||
// itemID needs an object-manager lookup, so prefer `ReadRaw` when all you
|
||||
// need is "did the cursor change".
|
||||
State Current();
|
||||
|
||||
// The identity of whatever the cursor holds, as the raw stored values —
|
||||
// no object-manager lookup, so this is cheap enough to read every frame.
|
||||
// Two reads that compare equal mean the cursor has not changed.
|
||||
//
|
||||
// Every global that identifies a holding is included, so this catches a
|
||||
// change of type and a change of content within one type, including
|
||||
// swapping between two stacks of the same item (their GUIDs differ).
|
||||
struct Raw {
|
||||
uint32_t type;
|
||||
uint32_t guidLo;
|
||||
uint32_t guidHi;
|
||||
uint32_t genericSlot;
|
||||
uint32_t money;
|
||||
uint32_t spellID;
|
||||
uint32_t macroID; // the stored macroID, not the slot — cheaper here
|
||||
};
|
||||
|
||||
Raw ReadRaw();
|
||||
bool Same(const Raw &a, const Raw &b);
|
||||
|
||||
// True if a GUID-bearing item is currently on the cursor —
|
||||
// `VAR_CURSOR_ITEM_GUID != 0`, the same globals `GetCursorInfo`'s bag-item
|
||||
// path reads and the exact signal the engine's item-pickup path guards on.
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_CVar.GetCVarBitfield(name, index)` / `C_CVar.SetCVarBitfield(name, index,
|
||||
// value)` — treat a cvar's numeric value as a bitfield and read or write one
|
||||
// bit of it, so a single cvar carries many independent booleans. Upstream this
|
||||
// backs the record of which tutorial popups a player has dismissed: one cvar,
|
||||
// one bit per popup, persisted by the config machinery for free.
|
||||
//
|
||||
// The index is 1-based. Blizzard types it `luaIndex` rather than `number`,
|
||||
// which is what settles that.
|
||||
//
|
||||
// WIDTH. The value is parsed and rewritten as a uint64 in C++, never handed
|
||||
// through a Lua number, so all 64 bits are exact — Lua 5.0's doubles are not in
|
||||
// the path. The valid index range is 1..64 because that is the width of the
|
||||
// type the arithmetic runs in, not a limit chosen here.
|
||||
//
|
||||
// Setting refuses, returning false, in the cases Blizzard's own documentation
|
||||
// names as requirements: an unknown cvar, a read-only one, and an index out of
|
||||
// range. Both of the first two are things this client can genuinely answer --
|
||||
// the lookup is the same filtering one behind C_CVar.DoesCVarExist, so a cvar
|
||||
// preserved out of Config.wtf without being implemented is refused here exactly
|
||||
// as it is by every other Lua cvar function. Nothing here is ever secure, which
|
||||
// is the remaining documented requirement.
|
||||
//
|
||||
// Writing goes through CVar::Factory::SetString rather than the engine setter
|
||||
// directly: that wrapper already declares FUN_SET_CVAR_VALUE as the __thiscall
|
||||
// it is, and getting that wrong corrupts the stack silently rather than
|
||||
// crashing. It also means a bitfield write fires the cvar's change callback and
|
||||
// marks the config dirty, exactly as SetCVar would.
|
||||
|
||||
#include "Factory.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace CVar::Bitfield {
|
||||
|
||||
namespace {
|
||||
|
||||
// The arithmetic type's own width, which is what bounds a valid index.
|
||||
constexpr int kBitCount = 64;
|
||||
|
||||
// Parses the cvar's value as an unsigned base-10 integer. A value that is not a
|
||||
// number reads as 0, which is how an unset bitfield behaves anyway: every bit
|
||||
// clear. Stops at the first non-digit rather than rejecting, so a value with
|
||||
// trailing text still yields its leading number.
|
||||
uint64_t ValueOf(CVar::Factory::Handle cvar) {
|
||||
const char *s = CVar::Factory::GetString(cvar);
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
while (*s == ' ' || *s == '\t')
|
||||
++s;
|
||||
uint64_t v = 0;
|
||||
for (; *s >= '0' && *s <= '9'; ++s) {
|
||||
const uint64_t digit = static_cast<uint64_t>(*s - '0');
|
||||
if (v > (UINT64_MAX - digit) / 10)
|
||||
return UINT64_MAX; // saturate rather than wrap
|
||||
v = v * 10 + digit;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// True when `index` names a bit this can address. 1-based.
|
||||
bool IndexInRange(double index) {
|
||||
return index >= 1.0 && index <= static_cast<double>(kBitCount) &&
|
||||
index == static_cast<double>(static_cast<int>(index));
|
||||
}
|
||||
|
||||
// `C_CVar.GetCVarBitfield(name, index) -> value`
|
||||
//
|
||||
// nil for an unknown cvar or an index out of range, matching the contract's
|
||||
// nilable return, so "no such bit" stays distinguishable from "bit is false".
|
||||
int __fastcall Script_GetCVarBitfield(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const double index = Game::Lua::ToNumber(L, 2);
|
||||
CVar::Factory::Handle cvar = CVar::Factory::Find(Game::Lua::ToString(L, 1));
|
||||
if (cvar == nullptr || !IndexInRange(index)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint64_t bit = uint64_t{1} << (static_cast<int>(index) - 1);
|
||||
Game::Lua::PushBool(L, (ValueOf(cvar) & bit) != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CVar.SetCVarBitfield(name, index, value) -> success`
|
||||
int __fastcall Script_SetCVarBitfield(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const double index = Game::Lua::ToNumber(L, 2);
|
||||
CVar::Factory::Handle cvar = CVar::Factory::Find(Game::Lua::ToString(L, 1));
|
||||
if (cvar == nullptr || !IndexInRange(index)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<const uint8_t *>(cvar) + Offsets::OFF_CVAR_FLAGS);
|
||||
if ((flags & Offsets::CVAR_FLAG_READ_ONLY) != 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint64_t bit = uint64_t{1} << (static_cast<int>(index) - 1);
|
||||
const uint64_t before = ValueOf(cvar);
|
||||
const uint64_t after = Game::Lua::ToBoolean(L, 3) != 0 ? (before | bit) : (before & ~bit);
|
||||
if (after != before) {
|
||||
char buf[24]; // 20 digits for a uint64 at most, plus the terminator
|
||||
std::snprintf(buf, sizeof buf, "%llu", static_cast<unsigned long long>(after));
|
||||
CVar::Factory::SetString(cvar, buf);
|
||||
}
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "GetCVarBitfield", &Script_GetCVarBitfield);
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "SetCVarBitfield", &Script_SetCVarBitfield);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CVar::Bitfield
|
||||
+10
-2
@@ -47,11 +47,15 @@ using FindCVar_t = void *(__fastcall *)(const char *name);
|
||||
using SetCVarValue_t = char(__thiscall *)(void *cvar, const char *value,
|
||||
int a3, int a4, int a5, int a6);
|
||||
|
||||
const char *ReadValue(void *cvar) {
|
||||
const char *ReadStringField(void *cvar, uintptr_t offset) {
|
||||
if (cvar == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(
|
||||
static_cast<const uint8_t *>(cvar) + Offsets::OFF_CVAR_VALUE_STR);
|
||||
static_cast<const uint8_t *>(cvar) + offset);
|
||||
}
|
||||
|
||||
const char *ReadValue(void *cvar) {
|
||||
return ReadStringField(cvar, Offsets::OFF_CVAR_VALUE_STR);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -76,6 +80,10 @@ const char *GetString(Handle cvar) {
|
||||
return ReadValue(cvar);
|
||||
}
|
||||
|
||||
const char *GetDefaultString(Handle cvar) {
|
||||
return ReadStringField(cvar, Offsets::OFF_CVAR_DEFAULT_STR);
|
||||
}
|
||||
|
||||
int GetInt(Handle cvar, int fallback) {
|
||||
const char *value = ReadValue(cvar);
|
||||
if (value == nullptr || value[0] == '\0')
|
||||
|
||||
@@ -66,6 +66,14 @@ Handle Find(const char *name);
|
||||
// valid for the cvar's lifetime.
|
||||
const char *GetString(Handle cvar);
|
||||
|
||||
// Read the value the cvar was REGISTERED with, which the engine keeps
|
||||
// alongside the live value and never rewrites. Comparing the two is how the
|
||||
// engine's own `cvarlist` decides whether to print "(default: %s)", and it is
|
||||
// the only way to tell "nobody has ever set this" from "set to exactly the
|
||||
// default" without a marker of our own. Returns null if `cvar` is null or the
|
||||
// cvar was registered without a default.
|
||||
const char *GetDefaultString(Handle cvar);
|
||||
|
||||
// Read the value parsed as a base-10 integer, or `fallback` if `cvar` is
|
||||
// null or the value isn't numeric.
|
||||
int GetInt(Handle cvar, int fallback = 0);
|
||||
@@ -75,6 +83,13 @@ int GetInt(Handle cvar, int fallback = 0);
|
||||
void SetString(Handle cvar, const char *value);
|
||||
|
||||
// Convenience: set from an integer.
|
||||
//
|
||||
// Both setters go through the engine's own value setter, which REFUSES the
|
||||
// write when the cvar carries `CVAR_FLAG_READ_ONLY` — silently, since the
|
||||
// innermost writer opens with `if (flags & 0x4) return;`. Some cvars gain
|
||||
// that bit partway through boot (`scriptMemory` is writable on the glue
|
||||
// state and read-only once in the world), so a write that has to land needs
|
||||
// to happen before the bit appears rather than fight it.
|
||||
void SetInt(Handle cvar, int value);
|
||||
|
||||
} // namespace CVar::Factory
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_CVar.GetCVarInfo(name)` — a cvar's value, its default, and what may be
|
||||
// done to it.
|
||||
//
|
||||
// value, defaultValue, isStoredServerAccount, isStoredServerCharacter,
|
||||
// isLockedFromUser, isSecure, isReadOnly
|
||||
//
|
||||
// Two of the seven are real here and five are structurally false, which is
|
||||
// worth stating precisely rather than leaving as "not implemented":
|
||||
//
|
||||
// isReadOnly REAL. Flag bit 0x4. Script_SetCVar tests it before
|
||||
// anything else and raises `"%s" is read only`, so a
|
||||
// caller checking this gets exactly the answer that
|
||||
// decides whether SetCVar will work.
|
||||
// isStoredServerAccount Always false. These describe a cvar the server
|
||||
// isStoredServerCharacter stores per account or per character, synchronised
|
||||
// through the `synchronizeConfig` cvar. That whole
|
||||
// mechanism postdates this client — there is no
|
||||
// server cvar sync and no flag bit for one, so false
|
||||
// is the accurate answer, not a placeholder.
|
||||
// isLockedFromUser Always false. No flag governs it.
|
||||
// isSecure Always false. Marks cvars that cannot be set while
|
||||
// in combat, a protection this client has no notion
|
||||
// of.
|
||||
//
|
||||
// Derived by reading 3.3.5's own GetCVarInfo, which returns the first four of
|
||||
// these: value and default from the cvar struct, then flag bits 4 and 5 for the
|
||||
// two server-stored booleans. The FLAG OFFSET transfers — +0x1C in both — but
|
||||
// the string offsets do not (3.3.5 keeps them at +0x28 and +0x40), so the ones
|
||||
// used here come from this client's own `cvarlist` handler instead.
|
||||
//
|
||||
// Unknown names return nil rather than raising, so a caller can tell "no such
|
||||
// cvar" from "set to empty". FUN_FIND_CVAR searches the cvar registry alone, so
|
||||
// a console COMMAND's name finds nothing and returns nil — which is the
|
||||
// contract's "only accepts console variables" without needing a test for it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CVar::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
using FindCVar_t = const uint8_t *(__fastcall *)(const char *name);
|
||||
|
||||
int __fastcall Script_C_CVar_GetCVarInfo(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_CVar.GetCVarInfo(\"cvar\")");
|
||||
return 0;
|
||||
}
|
||||
const char *name = Game::Lua::ToString(L, 1);
|
||||
|
||||
auto findCVar = reinterpret_cast<FindCVar_t>(Offsets::FUN_FIND_CVAR);
|
||||
const uint8_t *cvar = findCVar(name);
|
||||
if (cvar == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *value =
|
||||
*reinterpret_cast<const char *const *>(cvar + Offsets::OFF_CVAR_VALUE_STR);
|
||||
const char *defaultValue =
|
||||
*reinterpret_cast<const char *const *>(cvar + Offsets::OFF_CVAR_DEFAULT_STR);
|
||||
const uint32_t flags =
|
||||
*reinterpret_cast<const uint32_t *>(cvar + Offsets::OFF_CVAR_FLAGS);
|
||||
|
||||
// A cvar registered with no default leaves the pointer null; report the
|
||||
// empty string so every return keeps its declared type.
|
||||
Game::Lua::PushString(L, value != nullptr ? value : "");
|
||||
Game::Lua::PushString(L, defaultValue != nullptr ? defaultValue : "");
|
||||
Game::Lua::PushBool(L, false); // isStoredServerAccount
|
||||
Game::Lua::PushBool(L, false); // isStoredServerCharacter
|
||||
Game::Lua::PushBool(L, false); // isLockedFromUser
|
||||
Game::Lua::PushBool(L, false); // isSecure
|
||||
Game::Lua::PushBool(L, (flags & Offsets::CVAR_FLAG_READ_ONLY) != 0);
|
||||
return 7;
|
||||
}
|
||||
|
||||
// `C_CVar.DoesCVarExist(name)` — a ClassicAPI addition, named for the
|
||||
// `DoesAddOnExist` / `DoesSpellExist` pair. It answers the question callers
|
||||
// actually have, which is whether GetCVar and SetCVar will work on this name,
|
||||
// so it uses the same lookup they do. That makes the invariant exact:
|
||||
//
|
||||
// C_CVar.DoesCVarExist(name) == (C_CVar.GetCVarInfo(name) ~= nil)
|
||||
//
|
||||
// and it holds for a name preserved out of Config.wtf without the client
|
||||
// implementing it: such a cvar is real and reachable from the console, but no
|
||||
// Lua getter can see it, so reporting it as existing would be a lie to every
|
||||
// caller who then tried to read it.
|
||||
//
|
||||
// Returns false rather than raising for a non-string, matching the two
|
||||
// Does*Exist functions it is named after -- a predicate should answer, not
|
||||
// throw.
|
||||
int __fastcall Script_C_CVar_DoesCVarExist(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto findCVar = reinterpret_cast<FindCVar_t>(Offsets::FUN_FIND_CVAR);
|
||||
Game::Lua::PushBool(L, findCVar(Game::Lua::ToString(L, 1)) != nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CVar.AreCVarsLoaded()` — whether the cvar registry is up.
|
||||
//
|
||||
// Reports the registry's own initialised state rather than a constant, so it
|
||||
// states a fact instead of an assumption. In practice it is always true by the
|
||||
// time anything can call it: the config system starts from the boot path
|
||||
// (FUN_0063D380, reached straight out of the entry point), which registers
|
||||
// commands and replays Config.wtf long before a Lua state exists. So there is
|
||||
// no window in which Lua could observe false.
|
||||
//
|
||||
// It is a real question upstream, where an account's cvars can be stored
|
||||
// server-side and arrive after login. Nothing here is fetched over the network,
|
||||
// so once the boot path has run there is nothing further to wait for.
|
||||
int __fastcall Script_C_CVar_AreCVarsLoaded(void *L) {
|
||||
const uint32_t mask = *reinterpret_cast<const uint32_t *>(Offsets::VAR_CVAR_HASH_MASK);
|
||||
Game::Lua::PushBool(L, mask != Offsets::CVAR_HASH_MASK_UNINITIALIZED);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "GetCVarInfo", &Script_C_CVar_GetCVarInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "DoesCVarExist",
|
||||
&Script_C_CVar_DoesCVarExist);
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "AreCVarsLoaded",
|
||||
&Script_C_CVar_AreCVarsLoaded);
|
||||
}
|
||||
|
||||
// Cvar storage is process-global, so the same surface works pre-login.
|
||||
void RegisterGlueFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "GetCVarInfo", &Script_C_CVar_GetCVarInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "DoesCVarExist",
|
||||
&Script_C_CVar_DoesCVarExist);
|
||||
Game::Lua::RegisterTableFunction("C_CVar", "AreCVarsLoaded",
|
||||
&Script_C_CVar_AreCVarsLoaded);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&RegisterGlueFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CVar::Info
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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/>.
|
||||
|
||||
// `CVar::ScriptMemory` — turn the interface memory watchdog off by default.
|
||||
//
|
||||
// The engine polls Lua's live allocation against the `scriptMemory` cvar
|
||||
// (KB; registered with default "49152", so 48 MB) in `FUN_00495590`:
|
||||
//
|
||||
// limit = *(int *)(scriptMemoryCVar + 0x28); // parsed-int cache
|
||||
// if (enabled && limit > 0) {
|
||||
// if (luaKB() > limit) ForceFullGC();
|
||||
// if (luaKB() > limit) {
|
||||
// if (!deadline) { deadline = now + 15000;
|
||||
// Fire(MEMORY_EXHAUSTED, "%d", mb); }
|
||||
// else if (now >= deadline) Quit(); // FUN_00401560(0)
|
||||
// } else if (deadline) { deadline = 0; Fire(MEMORY_RECOVERED); }
|
||||
// }
|
||||
//
|
||||
// FrameXML turns MEMORY_EXHAUSTED into the "disable some interface add-ons
|
||||
// and restart" popup, and 15 seconds later the client EXITS. So the cap is
|
||||
// not advisory: it ends the session.
|
||||
//
|
||||
// 48 MB was a reasonable budget for the addons of the day and is not one for
|
||||
// a modern set, so the popup and the shutdown land on people whose addons
|
||||
// are working correctly. The gate is `limit > 0`, so writing 0 disables the
|
||||
// poll outright — the value Blizzard's own AddOns dialog at character select
|
||||
// lets a user type, via `SetScriptMemory` (which multiplies by 1024;
|
||||
// verified `SHL EAX,0xa` at 0x0046DA8D).
|
||||
//
|
||||
// Nothing else caps Lua memory. `lmemPool.cpp`'s allocator (`FUN_006FAE90`)
|
||||
// is six size classes that fall through to the Storm heap for anything
|
||||
// larger, with no ceiling of its own; its "not enough memory" is a genuine
|
||||
// allocation failure. So 0 hands the bound to the process address space
|
||||
// (this client is Large Address Aware) rather than to a number picked in
|
||||
// 2006 — the engine's real limit instead of an invented one.
|
||||
//
|
||||
// TIMING IS THE WHOLE TRICK. The cvar becomes READ-ONLY partway through boot:
|
||||
// in the world, `SetCVar("scriptMemory", "49152")` raises `"scriptMemory" is
|
||||
// read only` and `GetCVarInfo` reports `isReadOnly`, which is bit
|
||||
// `CVAR_FLAG_READ_ONLY` at `+0x1C` — and the innermost setter
|
||||
// (`FUN_0063E0B0`) opens with `if (flags & 4) return;`, so a write then is a
|
||||
// silent no-op. But the bit is NOT set yet on the glue state, where the write
|
||||
// below lands: verified in game by deleting the Config.wtf line, launching,
|
||||
// and finding 0 in the AddOns dialog and back in Config.wtf on exit.
|
||||
//
|
||||
// The bit is not set at registration either: the disassembly at `0x00402E24`
|
||||
// pushes flags 0 and registerConsole 0, so the registrar computes
|
||||
// `0 | 1 | 0x80000000`, and nothing in the binary ORs bit 2 into a cvar's
|
||||
// flags. Something between the glue screen and the world sets it; the
|
||||
// mechanism is still unidentified. Practical consequence: this must keep
|
||||
// running from the GLUE registration. Do not "simplify" it to the in-game
|
||||
// hook alone — the write would silently stop working, and the in-game hook
|
||||
// stays only as a second chance for a boot that somehow skips glue.
|
||||
//
|
||||
// We override the DEFAULT ONLY: if the live value differs from the value the
|
||||
// cvar was registered with, someone chose it and we leave it alone. The cvar
|
||||
// carries the archive flag, so our 0 persists to Config.wtf and the
|
||||
// comparison then fails on every later boot — the write happens once per
|
||||
// install, not once per session. The one false positive is a user who puts
|
||||
// exactly the default in Config.wtf, which reads as untouched; recovering the
|
||||
// watchdog means choosing any other value.
|
||||
//
|
||||
// Runs from both the glue and in-game registration flows. Cvar storage is
|
||||
// process-global and the write is idempotent by construction (afterwards the
|
||||
// value no longer equals the default), so whichever fires first wins and the
|
||||
// other is a no-op. Glue fires earliest, which also means the AddOns dialog
|
||||
// shows 0 rather than 48 on a first launch.
|
||||
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace CVar::ScriptMemory {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kCVarName = "scriptMemory";
|
||||
|
||||
void ApplyDefaultOverride() {
|
||||
Factory::Handle cvar = Factory::Find(kCVarName);
|
||||
if (cvar == nullptr)
|
||||
return; // registered at boot by the engine; absent only if that moved
|
||||
const char *value = Factory::GetString(cvar);
|
||||
const char *fallback = Factory::GetDefaultString(cvar);
|
||||
if (value == nullptr || fallback == nullptr)
|
||||
return;
|
||||
if (std::strcmp(value, fallback) != 0)
|
||||
return; // deliberately set — the user's choice stands
|
||||
Factory::SetInt(cvar, 0); // lands on the glue state; see the timing note
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&ApplyDefaultOverride};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&ApplyDefaultOverride};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CVar::ScriptMemory
|
||||
@@ -64,7 +64,7 @@ uint32_t ColorByte(void *L, int idx, double dflt) {
|
||||
}
|
||||
|
||||
int __fastcall Script_SetCursorPosition(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:SetCursorPosition(position)");
|
||||
return 0;
|
||||
@@ -82,7 +82,7 @@ int __fastcall Script_SetCursorPosition(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCursorPosition(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:GetCursorPosition()");
|
||||
return 0;
|
||||
@@ -96,7 +96,7 @@ int __fastcall Script_GetCursorPosition(void *L) {
|
||||
// vs GetCursorPosition's byte offset. Equal for ASCII; differs once the text
|
||||
// holds multibyte characters.
|
||||
int __fastcall Script_GetUTF8CursorPosition(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:GetUTF8CursorPosition()");
|
||||
return 0;
|
||||
@@ -113,7 +113,7 @@ int __fastcall Script_GetUTF8CursorPosition(void *L) {
|
||||
// helper does; vanilla can only clear a selection via HighlightText(0, 0),
|
||||
// which yanks it to offset 0 instead of leaving the caret in place.
|
||||
int __fastcall Script_ClearHighlightText(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:ClearHighlightText()");
|
||||
return 0;
|
||||
@@ -128,7 +128,7 @@ int __fastcall Script_ClearHighlightText(void *L) {
|
||||
// focused box in one global (written by SetFocus / cleared by ClearFocus), so
|
||||
// focus is just pointer identity against it.
|
||||
int __fastcall Script_HasFocus(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
@@ -140,7 +140,7 @@ int __fastcall Script_HasFocus(void *L) {
|
||||
|
||||
// True when the box holds any text (its byte length is non-zero).
|
||||
int __fastcall Script_HasText(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
@@ -156,7 +156,7 @@ int __fastcall Script_HasText(void *L) {
|
||||
// it on all three via the engine's own region color setter. Colors are 0..1;
|
||||
// alpha defaults to 1. Takes effect on the next selection paint.
|
||||
int __fastcall Script_SetHighlightColor(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:SetHighlightColor(r, g, b [, a])");
|
||||
@@ -181,7 +181,7 @@ int __fastcall Script_SetHighlightColor(void *L) {
|
||||
// GetHighlightColor() -> r, g, b, a (each 0..1). Reads the color off the first
|
||||
// highlight region; a region with no explicit color reads back white (opaque).
|
||||
int __fastcall Script_GetHighlightColor(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:GetHighlightColor()");
|
||||
return 0;
|
||||
@@ -205,7 +205,7 @@ int __fastcall Script_GetHighlightColor(void *L) {
|
||||
// drops every entry and resets the write position. No-op when history was
|
||||
// never enabled (max 0).
|
||||
int __fastcall Script_ClearHistory(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
void *eb = Game::Lua::ResolveEditBox(L);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:ClearHistory()");
|
||||
return 0;
|
||||
|
||||
@@ -143,6 +143,25 @@ int IndexOf(uint32_t setID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<const Set *> SetsContainingItem(uint64_t itemGuid) {
|
||||
EnsureLoaded();
|
||||
std::vector<const Set *> found;
|
||||
// The sentinels are slot markers, not items — a set with an empty or
|
||||
// ignored slot must not match a caller that hands us one of them.
|
||||
if (itemGuid == GUID_EMPTY || itemGuid == GUID_IGNORED)
|
||||
return found;
|
||||
|
||||
for (const Set &s : g_sets) {
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
if (s.items[i] == itemGuid) {
|
||||
found.push_back(&s);
|
||||
break; // one hit per set, however many slots match
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
uint32_t Create(const char *name, const char *icon) {
|
||||
EnsureLoaded();
|
||||
if (g_path.empty() || name == nullptr || name[0] == '\0')
|
||||
|
||||
@@ -31,6 +31,18 @@ const Set *FindByID(uint32_t setID);
|
||||
const Set *FindByName(const char *name);
|
||||
int IndexOf(uint32_t setID); // 0-based; -1 if missing
|
||||
|
||||
// Every set that includes the item instance `itemGuid`, in set order.
|
||||
// Empty when the item belongs to no set, and for the reserved
|
||||
// `GUID_EMPTY` / `GUID_IGNORED` sentinels — those mark a slot as empty
|
||||
// or skipped, so they are never a real item and must not match.
|
||||
//
|
||||
// Matching is by GUID, so it identifies the one physical item that was
|
||||
// saved into the set rather than any copy of the same itemID: a second
|
||||
// Arcanite Reaper in the bag next to the set's own does not report as
|
||||
// part of the set. Pointers reference `All()`'s storage and stay valid
|
||||
// until the next mutation.
|
||||
std::vector<const Set *> SetsContainingItem(uint64_t itemGuid);
|
||||
|
||||
// Mutations — bump-and-save semantics. Each returns the affected
|
||||
// set's ID (or 0 on failure), persists to disk, and fires
|
||||
// `EQUIPMENT_SETS_CHANGED`. Callers don't need to manage the file
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
|
||||
#include "Storage.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "settings/Paths.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -25,22 +24,6 @@ namespace EquipmentSet::Storage {
|
||||
|
||||
namespace {
|
||||
|
||||
const char *ReadAccountName() {
|
||||
return Game::Read<const char *>(Offsets::VAR_ACCOUNT_NAME_PTR);
|
||||
}
|
||||
|
||||
const char *ReadCharacterName() {
|
||||
auto *p = reinterpret_cast<const char *>(Offsets::VAR_CHARACTER_NAME);
|
||||
return (p[0] == '\0') ? nullptr : p;
|
||||
}
|
||||
|
||||
const char *ReadRealmName() {
|
||||
auto *info = Game::Read<uint8_t *>(Offsets::VAR_REALM_INFO_PTR);
|
||||
if (info == nullptr)
|
||||
return nullptr;
|
||||
return Game::Read<const char *>(info, Offsets::OFF_REALM_INFO_NAME);
|
||||
}
|
||||
|
||||
// Removes trailing CR/LF/whitespace. Names from the engine are ASCII
|
||||
// only (the chat input filter rejects high bytes) so we don't have to
|
||||
// think about multi-byte trailing whitespace.
|
||||
@@ -74,23 +57,7 @@ bool ParseField(const std::string &line, const std::string &key, std::string *va
|
||||
} // namespace
|
||||
|
||||
std::string ResolveFilePath() {
|
||||
const char *account = ReadAccountName();
|
||||
if (account == nullptr || account[0] == '\0')
|
||||
return {};
|
||||
const char *realm = ReadRealmName();
|
||||
if (realm == nullptr || realm[0] == '\0')
|
||||
return {};
|
||||
const char *character = ReadCharacterName();
|
||||
if (character == nullptr)
|
||||
return {};
|
||||
std::string out = "WTF\\Account\\";
|
||||
out += account;
|
||||
out += '\\';
|
||||
out += realm;
|
||||
out += '\\';
|
||||
out += character;
|
||||
out += "\\ClassicAPI_EquipmentSets.txt";
|
||||
return out;
|
||||
return Settings::Paths::CharacterFile("ClassicAPI_EquipmentSets.txt");
|
||||
}
|
||||
|
||||
bool Load(const std::string &path, std::vector<Set> *outSets) {
|
||||
|
||||
@@ -160,7 +160,7 @@ int __fastcall Script_GameTooltipSetEquipmentSet(void *L) {
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
|
||||
void *self = Game::Lua::ResolveObject(L, 1);
|
||||
void *self = Game::Lua::ResolveTooltip(L);
|
||||
if (self == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -407,6 +407,30 @@ static int __fastcall Script_C_Reputation_GetWatchedFactionData(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Reputation.GetFactionDataByID(factionID)` — modern table-shaped
|
||||
// accessor keyed by faction ID rather than by displayed-list position.
|
||||
// Returns nil when the ID has no `Faction.dbc` record.
|
||||
//
|
||||
// Unlike the index form, this doesn't need the faction to be in the
|
||||
// player's displayed reputation list: `ReadFactionData` fills an
|
||||
// unencountered faction cleanly (currentStanding 0, not at war), so a
|
||||
// lookup works for any real faction.
|
||||
static int __fastcall Script_C_Reputation_GetFactionDataByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_Reputation.GetFactionDataByID(factionID)");
|
||||
return 0;
|
||||
}
|
||||
const int factionID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
FactionData d;
|
||||
if (!ReadFactionData(factionID, &d))
|
||||
return 0; // nil — no such faction
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
PushFactionDataTable(L, d);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Reputation.GetFactionDataByIndex(factionSortIndex)` — modern
|
||||
// table-shaped accessor over the displayed reputation list. 1-based
|
||||
// index covering the same range as vanilla's `GetFactionInfo(index)`
|
||||
@@ -440,6 +464,182 @@ static int __fastcall Script_C_Reputation_GetFactionDataByIndex(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Reputation.ToggleFactionAtWarByID(factionID)` — ClassicAPI
|
||||
// extension. Flips a faction's at-war state by ID rather than by
|
||||
// displayed-list position.
|
||||
//
|
||||
// Mirrors the stock `FactionToggleAtWar(index)` body exactly, minus the
|
||||
// index resolve: read the current at-war flag, negate it, hand it to the
|
||||
// engine's own setter. Every rule therefore stays with the engine —
|
||||
// refusing to make peace below -3000 standing, honouring the
|
||||
// peace-forced flag, the loot-session bail, and sending
|
||||
// `CMSG_SET_FACTION_ATWAR`. Going through the engine's setter also means
|
||||
// `Faction::UnitFactionPolyfill`'s hook on it fires `UNIT_FACTION`
|
||||
// ("any future C++ caller of the setter inherits the polyfill"), and its
|
||||
// change-detection keeps that silent when the engine refuses the toggle.
|
||||
//
|
||||
// Non-positive IDs are ignored (the stock path likewise skips
|
||||
// factionID 0, which is what its resolver returns for header rows).
|
||||
static int __fastcall Script_C_Reputation_ToggleFactionAtWarByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Reputation.ToggleFactionAtWarByID(factionID)");
|
||||
return 0;
|
||||
}
|
||||
const int factionID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (factionID <= 0)
|
||||
return 0;
|
||||
|
||||
// `newState` is declared `int`, not `char`, to match the detour
|
||||
// `Faction::UnitFactionPolyfill` installs on this setter: a `char`
|
||||
// argument only defines the low byte of EDX, leaving the detour's
|
||||
// `int` parameter with undefined high bits.
|
||||
using GetAtWar_t = unsigned int(__fastcall *)(int factionID);
|
||||
using SetAtWar_t = void(__fastcall *)(int factionID, int newState);
|
||||
auto getAtWar = reinterpret_cast<GetAtWar_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FACTION_GET_AT_WAR));
|
||||
auto setAtWar = reinterpret_cast<SetAtWar_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FACTION_SET_AT_WAR));
|
||||
|
||||
setAtWar(factionID, (getAtWar(factionID) != 0) ? 0 : 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `factionID` → its 0..63 reputation-slot index, or -1 when the faction
|
||||
// has no slot (no record, or a header category). Same `Faction.dbc`
|
||||
// `RepListIndex` field `ReadFactionData` reads.
|
||||
static int RepSlotForFaction(int factionID) {
|
||||
if (factionID <= 0)
|
||||
return -1;
|
||||
const uint8_t *record = FactionRecord(factionID);
|
||||
if (record == nullptr)
|
||||
return -1;
|
||||
const int repListIndex = *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_FACTION_REP_LIST_INDEX);
|
||||
if (repListIndex < 0 || repListIndex >= Offsets::MAX_REP_SLOTS)
|
||||
return -1;
|
||||
return repListIndex;
|
||||
}
|
||||
|
||||
// Whether a reputation slot carries the INACTIVE flag.
|
||||
static bool RepSlotIsInactive(int repSlot) {
|
||||
auto *slot = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_REP_SLOTS) +
|
||||
static_cast<uintptr_t>(repSlot) * Offsets::REP_SLOT_STRIDE);
|
||||
return (*(slot + Offsets::OFF_REP_SLOT_FLAGS) &
|
||||
Offsets::REP_SLOT_FLAG_INACTIVE) != 0;
|
||||
}
|
||||
|
||||
// `C_Reputation.IsFactionActive(factionSortIndex)` — whether the faction
|
||||
// at a 1-based displayed-list position is NOT filed under "Inactive".
|
||||
//
|
||||
// Runs the same chain as the engine's own inactive check (resolve the
|
||||
// index to a faction id, then read the slot's INACTIVE bit) and returns
|
||||
// the inverse as a real boolean. A position that names no faction — out
|
||||
// of range, or a category header — reports `false`.
|
||||
static int __fastcall Script_C_Reputation_IsFactionActive(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Reputation.IsFactionActive(factionSortIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
const int maxIdx = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_FACTION_VISIBLE_MAX_INDEX));
|
||||
|
||||
const int factionID = (idx >= 0 && idx <= maxIdx) ? Resolver()(idx) : 0;
|
||||
const int repSlot = RepSlotForFaction(factionID);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushBool(L, repSlot >= 0 && !RepSlotIsInactive(repSlot));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Reputation.IsFactionActiveByID(factionID)` — ClassicAPI extension.
|
||||
// The same answer keyed by faction id. Reports `false` for a faction
|
||||
// with no reputation slot.
|
||||
static int __fastcall Script_C_Reputation_IsFactionActiveByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Reputation.IsFactionActiveByID(factionID)");
|
||||
return 0;
|
||||
}
|
||||
const int repSlot =
|
||||
RepSlotForFaction(static_cast<int>(Game::Lua::ToNumber(L, 1)));
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushBool(L, repSlot >= 0 && !RepSlotIsInactive(repSlot));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Shared body for the two by-ID inactive setters below. Hands the id to
|
||||
// the engine's own inactive setter, which flips the rep slot's INACTIVE
|
||||
// bit, tells the server, and rebuilds the displayed faction list so the
|
||||
// faction moves into or out of the "Inactive" category. That setter is
|
||||
// also what `Faction::UnitFactionPolyfill` hooks, so `UNIT_FACTION`
|
||||
// fires when the flag actually changes.
|
||||
//
|
||||
// `newState` is `int` rather than `char` to match that detour's
|
||||
// signature — see the note in ToggleFactionAtWarByID.
|
||||
static int SetFactionInactiveByID(void *L, int newState, const char *usage) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
const int factionID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (factionID <= 0)
|
||||
return 0;
|
||||
|
||||
using SetInactive_t = void(__fastcall *)(int factionID, int newState);
|
||||
auto fn = reinterpret_cast<SetInactive_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FACTION_SET_INACTIVE));
|
||||
fn(factionID, newState);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `C_Reputation.SetFactionInactiveByID(factionID)` — ClassicAPI
|
||||
// extension. Moves a faction into the "Inactive" category by ID rather
|
||||
// than by displayed-list position.
|
||||
static int __fastcall Script_C_Reputation_SetFactionInactiveByID(void *L) {
|
||||
return SetFactionInactiveByID(
|
||||
L, 1, "Usage: C_Reputation.SetFactionInactiveByID(factionID)");
|
||||
}
|
||||
|
||||
// `C_Reputation.SetFactionActiveByID(factionID)` — the inverse: takes a
|
||||
// faction back out of the "Inactive" category.
|
||||
static int __fastcall Script_C_Reputation_SetFactionActiveByID(void *L) {
|
||||
return SetFactionInactiveByID(
|
||||
L, 0, "Usage: C_Reputation.SetFactionActiveByID(factionID)");
|
||||
}
|
||||
|
||||
// `C_Reputation.SetSelectedFactionByID(factionID)` — ClassicAPI
|
||||
// extension. Selects a faction in the reputation pane by ID rather
|
||||
// than by displayed-list position, the same convenience
|
||||
// `SetWatchedFactionByID` gives over `SetWatchedFactionIndex`.
|
||||
//
|
||||
// The engine already stores the selection AS a factionID
|
||||
// (`VAR_FACTION_SELECTED_ID`) — the stock index form only resolves its
|
||||
// argument to an id before storing it — so this writes the id straight
|
||||
// through, with no list walk and no dependence on the faction being
|
||||
// currently listed. `GetSelectedFaction()` maps it back to an index,
|
||||
// reporting 0 while the faction isn't in the displayed list.
|
||||
//
|
||||
// Negative IDs are ignored; 0 clears the selection.
|
||||
static int __fastcall Script_C_Reputation_SetSelectedFactionByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Reputation.SetSelectedFactionByID(factionID)");
|
||||
return 0;
|
||||
}
|
||||
const int factionID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (factionID < 0)
|
||||
return 0;
|
||||
|
||||
*reinterpret_cast<int32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_FACTION_SELECTED_ID)) = factionID;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `C_Reputation.SetWatchedFactionByID(factionID)` — sets the faction
|
||||
// shown above the XP bar by ID rather than by displayed-list index.
|
||||
// Modern API; vanilla 1.12 only exposes `SetWatchedFactionIndex(idx)`,
|
||||
@@ -475,10 +675,46 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetWatchedFactionByID",
|
||||
&Script_C_Reputation_SetWatchedFactionByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetSelectedFactionByID",
|
||||
&Script_C_Reputation_SetSelectedFactionByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "ToggleFactionAtWarByID",
|
||||
&Script_C_Reputation_ToggleFactionAtWarByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActive",
|
||||
&Script_C_Reputation_IsFactionActive);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActiveByID",
|
||||
&Script_C_Reputation_IsFactionActiveByID);
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "IsFactionInactive",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)));
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionInactiveByID",
|
||||
&Script_C_Reputation_SetFactionInactiveByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionActiveByID",
|
||||
&Script_C_Reputation_SetFactionActiveByID);
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "SetFactionInactive",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)));
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "SetFactionActive",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)));
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "ToggleFactionAtWar",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)));
|
||||
// The stock global, mirrored into the namespace — the engine's own
|
||||
// handler already has the Lua C ABI, so it registers as-is.
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "SetSelectedFaction",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)));
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetWatchedFactionData",
|
||||
&Script_C_Reputation_GetWatchedFactionData);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionStandings",
|
||||
&Script_C_Reputation_GetFactionStandings);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByID",
|
||||
&Script_C_Reputation_GetFactionDataByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByIndex",
|
||||
&Script_C_Reputation_GetFactionDataByIndex);
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ float WrapBudget(void *fs) {
|
||||
void *ResolveSelf(void *L, const char *usage) {
|
||||
void *fs = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
fs = Game::Lua::ResolveObject(L, 1);
|
||||
fs = Game::Lua::ResolveFontString(L, 1, /*raiseError=*/false);
|
||||
if (fs == nullptr)
|
||||
Game::Lua::Error(L, "%s", usage);
|
||||
return fs;
|
||||
@@ -362,7 +362,7 @@ int __fastcall Script_GetMaxLines(void *L) {
|
||||
int __fastcall Script_SetFormattedText(void *L) {
|
||||
void *fs = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
fs = Game::Lua::ResolveObject(L, 1);
|
||||
fs = Game::Lua::ResolveFontString(L, 1, /*raiseError=*/false);
|
||||
if (fs == nullptr || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: fontstring:SetFormattedText(\"format\"[, ...])");
|
||||
return 0;
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
|
||||
#include "frame/Attributes.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
@@ -143,13 +144,7 @@ void LowerCopy(char *dst, const char *src, size_t n) {
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// Bounded, always-terminated copy (avoids the C4996 strncpy/strcpy warnings).
|
||||
void BoundedCopy(char *dst, const char *src, size_t n) {
|
||||
size_t i = 0;
|
||||
for (; src && src[i] && i + 1 < n; ++i)
|
||||
dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
using Common::BoundedCopy;
|
||||
|
||||
// dst = tolower(a .. b .. c). Truncates at n-1.
|
||||
void Compose3Lower(char *dst, size_t n, const char *a, const char *b, const char *c) {
|
||||
@@ -655,7 +650,7 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
if (!ReadModAttr(L, fi, prefix, "spell", suffix, spell, sizeof spell))
|
||||
return false;
|
||||
if (unit)
|
||||
Spell::AtUnit::CastByName(spell, unit);
|
||||
Spell::AtUnit::CastByName(spell, unit, /*placeGroundSpell*/ true);
|
||||
else
|
||||
Game::Lua::CallGlobalString(L, "CastSpellByName", spell);
|
||||
return true;
|
||||
@@ -995,7 +990,7 @@ int DoSet(void *L, bool fireHandler) { // (self, name, value)
|
||||
// (the key the mouse-focus poll uses) and enable its mouse so it can be
|
||||
// hovered.
|
||||
if (std::strcmp(lname, "unit") == 0) {
|
||||
void *obj = Game::Lua::ResolveObject(L, 1);
|
||||
void *obj = Game::Lua::ResolveFrame(L);
|
||||
if (obj != nullptr) {
|
||||
if (isString) {
|
||||
const char *tok = Game::Lua::ToString(L, 3);
|
||||
@@ -1023,7 +1018,7 @@ int DoSet(void *L, bool fireHandler) { // (self, name, value)
|
||||
}
|
||||
|
||||
if (fireHandler)
|
||||
FireAttributeChanged(L, Game::Lua::ResolveObject(L, 1), lname);
|
||||
FireAttributeChanged(L, Game::Lua::ResolveFrame(L), lname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+19
-22
@@ -29,10 +29,11 @@
|
||||
//
|
||||
// - The engine fires a button's OnClick through the arg'd script runner
|
||||
// FUN_FRAME_RUN_SCRIPT_WITH_CONTEXT(frame, frame+0x4CC, "%s", &buttonName).
|
||||
// We co-hook that runner and, when the fired slot IS the OnClick slot,
|
||||
// re-invoke the runner for our PreClick cell first, then let OnClick run,
|
||||
// then re-invoke for PostClick — reusing the SAME (fmt, varargs) so
|
||||
// PreClick/PostClick receive arg1 = the button name exactly like OnClick.
|
||||
// We intercept that runner (through `Frame::RunnerHook`, the shared single
|
||||
// hook on it) and, when the fired slot IS the OnClick slot, re-invoke the
|
||||
// runner for our PreClick cell first, then let OnClick run, then re-invoke
|
||||
// for PostClick — reusing the SAME (fmt, varargs) so PreClick/PostClick
|
||||
// receive arg1 = the button name exactly like OnClick.
|
||||
//
|
||||
// Why the runner and not the button click vmethod (FUN_00779540): SuperWoW
|
||||
// inline-hooks that vmethod for click-casting, and a second MinHook there
|
||||
@@ -50,6 +51,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/RunnerHook.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
@@ -122,13 +124,12 @@ int __fastcall Resolver_h(void *self, void *edx, const char *name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Runner co-hook (fire PreClick before / PostClick after OnClick) ---
|
||||
using RunScript_t = void(__cdecl *)(void *frame, uint32_t *slotPtr,
|
||||
const char *fmt, void *varargs);
|
||||
RunScript_t g_runOriginal = nullptr;
|
||||
// --- Runner interceptor (fire PreClick before / PostClick after OnClick) ---
|
||||
// Subscribed to `Frame::RunnerHook`, the single shared hook on
|
||||
// FUN_FRAME_RUN_SCRIPT_WITH_CONTEXT (MinHook allows one hook per address).
|
||||
|
||||
// Fire the handler (if any) for `kind` on `frame`, reusing the OnClick fire's
|
||||
// own (fmt, varargs) so arg1 = the button name. We call the original runner
|
||||
// own (fmt, varargs) so arg1 = the button name. We call the engine runner
|
||||
// directly (its exec-context stamp is valid at this dispatch level). The runner
|
||||
// balances the Lua stack itself; we snapshot/restore the top as cheap insurance
|
||||
// against any imbalance leaking to the still-running button click code.
|
||||
@@ -138,26 +139,25 @@ void FireCell(void *frame, int kind, const char *fmt, void *varargs) {
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
g_runOriginal(frame, slot, fmt, varargs);
|
||||
Frame::RunnerHook::Original(frame, slot, fmt, varargs);
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
void __cdecl RunScript_h(void *frame, uint32_t *slotPtr, const char *fmt,
|
||||
void *varargs) {
|
||||
// Falls through (false) for every fire that isn't a top-level OnClick;
|
||||
// otherwise runs the whole bracket itself and reports it handled (true).
|
||||
bool OnRun(void *frame, uint32_t *slotPtr, const char *fmt, void *varargs) {
|
||||
const auto onClickSlot = reinterpret_cast<uint32_t *>(
|
||||
reinterpret_cast<char *>(frame) + Offsets::OFF_BUTTON_ONCLICK_HANDLER);
|
||||
|
||||
if (slotPtr != onClickSlot || g_firing > 0) {
|
||||
g_runOriginal(frame, slotPtr, fmt, varargs);
|
||||
return;
|
||||
}
|
||||
if (slotPtr != onClickSlot || g_firing > 0)
|
||||
return false;
|
||||
|
||||
++g_firing;
|
||||
FireCell(frame, SK_PRECLICK, fmt, varargs);
|
||||
g_runOriginal(frame, slotPtr, fmt, varargs); // the button's OnClick
|
||||
Frame::RunnerHook::Original(frame, slotPtr, fmt, varargs); // the button's OnClick
|
||||
FireCell(frame, SK_POSTCLICK, fmt, varargs);
|
||||
--g_firing;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _resolverHook{
|
||||
@@ -165,10 +165,7 @@ static const Game::HookAutoRegister _resolverHook{
|
||||
reinterpret_cast<void *>(&Resolver_h),
|
||||
reinterpret_cast<void **>(&g_resolverOriginal)};
|
||||
|
||||
static const Game::HookAutoRegister _runnerHook{
|
||||
Offsets::FUN_FRAME_RUN_SCRIPT_WITH_CONTEXT,
|
||||
reinterpret_cast<void *>(&RunScript_h),
|
||||
reinterpret_cast<void **>(&g_runOriginal)};
|
||||
static const Frame::RunnerHook::AutoSubscribe _runnerSub{&OnRun};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+24
-60
@@ -119,6 +119,28 @@ bool CallRegionNumber(void *L, uintptr_t fn, double *out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `IsMouseOver` and `IsDragging` go on the REGION base registry, which every
|
||||
// region-family type inherits from — so a frame, texture or fontstring self
|
||||
// reaches them legitimately, and a Region-only gate would reject the very
|
||||
// callers the base registration exists to serve. Accepting the four ids
|
||||
// states that set directly instead of resting on an assumption about which
|
||||
// classes the engine's `IsA` treats as Regions. Both methods only read
|
||||
// Region-base state, so the wider set is sound. Non-raising: each reports
|
||||
// false for an unusable self, the contract they already had.
|
||||
constexpr uintptr_t kRegionFamilyTypeIds[] = {
|
||||
Offsets::VAR_REGION_LUA_TYPE_ID,
|
||||
Offsets::VAR_FRAME_LUA_TYPE_ID,
|
||||
Offsets::VAR_TEXTURE_LUA_TYPE_ID,
|
||||
Offsets::VAR_FONTSTRING_LUA_TYPE_ID,
|
||||
};
|
||||
|
||||
void *ResolveRegionFamily(void *L) {
|
||||
return Game::Lua::ResolveTypedObjectAny(
|
||||
L, 1, kRegionFamilyTypeIds,
|
||||
static_cast<int>(sizeof(kRegionFamilyTypeIds) / sizeof(kRegionFamilyTypeIds[0])),
|
||||
/*raiseError=*/false);
|
||||
}
|
||||
|
||||
int __fastcall Script_IsMouseOver(void *L) {
|
||||
// Offsets first — the stack gets reshaped by the delegated getters.
|
||||
const double offTop = Game::Lua::IsNumber(L, 2) ? Game::Lua::ToNumber(L, 2) : 0.0;
|
||||
@@ -126,7 +148,7 @@ int __fastcall Script_IsMouseOver(void *L) {
|
||||
const double offLeft = Game::Lua::IsNumber(L, 4) ? Game::Lua::ToNumber(L, 4) : 0.0;
|
||||
const double offRight = Game::Lua::IsNumber(L, 5) ? Game::Lua::ToNumber(L, 5) : 0.0;
|
||||
|
||||
void *obj = Game::Lua::ResolveObject(L, 1);
|
||||
void *obj = ResolveRegionFamily(L);
|
||||
if (obj == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
@@ -191,7 +213,7 @@ int __fastcall Script_GetRect(void *L) {
|
||||
// regions — a texture/fontstring self can never match the frame stored
|
||||
// there, so it reports false, as it should.
|
||||
int __fastcall Script_IsDragging(void *L) {
|
||||
void *self = Game::Lua::ResolveObject(L, 1);
|
||||
void *self = ResolveRegionFamily(L);
|
||||
void *ctx = Game::Read<void *>(static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
void *dragTarget =
|
||||
ctx != nullptr
|
||||
@@ -331,63 +353,6 @@ int __fastcall Script_HookScript(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- IsEventRegistered (Frame) ---------------------------------------------
|
||||
|
||||
// `frame:IsEventRegistered("event")` — vanilla has RegisterEvent /
|
||||
// UnregisterEvent but not the query. There's no engine Script_* to
|
||||
// delegate to, so we replicate the membership check that lives inside
|
||||
// Frame::RegisterEvent (FUN_00702140): find the event by name in the live
|
||||
// event table, then walk that entry's subscriber chain for this frame.
|
||||
// The frame pointer we compare against is the same CFrameScriptObject*
|
||||
// the engine stores (Game::Lua::ResolveObject resolves self exactly as
|
||||
// Script_RegisterEvent does).
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
bool FrameHasEvent(void *frame, const char *eventName) {
|
||||
const uint32_t count = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_COUNT));
|
||||
auto *base = Game::Read<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_BASE_PTR));
|
||||
if (count == 0 || base == nullptr)
|
||||
return false;
|
||||
|
||||
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I));
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const char *name = Game::Read<const char *>(
|
||||
entry, Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
if (sstrcmpi(name, eventName, 0x7FFFFFFF) != 0)
|
||||
continue;
|
||||
// Names are unique, so this is the one entry — walk its chain.
|
||||
uintptr_t node = Game::Read<uintptr_t>(
|
||||
entry, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
while ((node & 1) == 0 && node != 0) {
|
||||
if (Game::Read<void *>(node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
|
||||
return true;
|
||||
node = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsEventRegistered(void *L) {
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: frame:IsEventRegistered(\"event\")");
|
||||
return 0;
|
||||
}
|
||||
void *frame = Game::Lua::ResolveObject(L, 1);
|
||||
const char *eventName = Game::Lua::ToString(L, 2);
|
||||
const bool registered =
|
||||
frame != nullptr && eventName != nullptr && FrameHasEvent(frame, eventName);
|
||||
Game::Lua::PushBool(L, registered);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- SetPoint one-arg form (Script_SetPoint co-hook) -----------------------
|
||||
|
||||
// Vanilla's Script_SetPoint accepts every modern form except the bare
|
||||
@@ -427,7 +392,6 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
Offsets::FUN_SCRIPT_FRAME_HIDE>},
|
||||
{"SetResizeBounds", &Script_SetResizeBounds},
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"IsEventRegistered", &Script_IsEventRegistered},
|
||||
{"GetEffectiveAlpha", &Script_GetEffectiveAlpha},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "frame/RunnerHook.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Frame::RunnerHook {
|
||||
|
||||
namespace {
|
||||
|
||||
// Chained at static-init time by each `AutoSubscribe` constructor, before
|
||||
// `DllMain`. Zero-initialized (namespace static) ahead of any dynamic init, so
|
||||
// the head is valid when subscribers link in.
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
using Runner_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt,
|
||||
void *varargs);
|
||||
Runner_t g_orig = nullptr;
|
||||
|
||||
void __cdecl Hook(void *frame, uint32_t *slotPtr, const char *fmt, void *varargs) {
|
||||
for (AutoSubscribe *s = g_head; s != nullptr; s = s->next)
|
||||
if (s->fn(frame, slotPtr, fmt, varargs))
|
||||
return; // handled — suppressed, or the subscriber ran Original itself
|
||||
g_orig(frame, slotPtr, fmt, varargs);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{
|
||||
Offsets::FUN_FRAME_RUN_SCRIPT_WITH_CONTEXT, reinterpret_cast<void *>(&Hook),
|
||||
reinterpret_cast<void **>(&g_orig)};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Interceptor f) : fn(f), next(g_head) { g_head = this; }
|
||||
|
||||
void Original(void *frame, uint32_t *slotPtr, const char *fmt, void *varargs) {
|
||||
g_orig(frame, slotPtr, fmt, varargs);
|
||||
}
|
||||
|
||||
} // namespace Frame::RunnerHook
|
||||
@@ -0,0 +1,56 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared interceptor for the engine's arg'd frame-script runner
|
||||
// `FUN_FRAME_RUN_SCRIPT_WITH_CONTEXT` (0x00702710) — `__cdecl(frame, slotPtr,
|
||||
// fmt, vaPtr)`: stamps the exec context from `slotPtr[1]`, runs the handler in
|
||||
// `slotPtr[0]` through FUN_FRAME_RUN_SCRIPT_ARGS, restores. Exactly two engine
|
||||
// paths funnel through it: every arg'd input-script fire (OnClick,
|
||||
// OnMouseWheel, … via the variadic forwarder FUN_007026F0) and the event
|
||||
// dispatcher FUN_FIRE_EVENT's per-frame OnEvent fire, which passes
|
||||
// `slotPtr == frame + OFF_FRAME_ONEVENT_SLOT`.
|
||||
//
|
||||
// MinHook allows ONE hook per address, and more than one feature needs this
|
||||
// runner: `Frame::ClickEvents` brackets OnClick with PreClick/PostClick, and
|
||||
// `Frame::UnitEvent` suppresses OnEvent fires that a RegisterUnitEvent filter
|
||||
// rejects. So — exactly like `Event::SignalHook` and `Tick::WorldTick` — this
|
||||
// module owns the single hook and fans out. Declare a file-scope
|
||||
// static const Frame::RunnerHook::AutoSubscribe _sub{&interceptor};
|
||||
//
|
||||
// An interceptor returns `true` when it has fully handled the fire — either
|
||||
// suppressed it, or already ran the engine itself through `Original` (the
|
||||
// bracketing case). `false` falls through to the next subscriber and finally
|
||||
// the original. The FIRST `true` wins, so subscribers must claim disjoint slot
|
||||
// addresses; subscriber order is static-init order and must not matter.
|
||||
|
||||
namespace Frame::RunnerHook {
|
||||
|
||||
using Interceptor = bool (*)(void *frame, uint32_t *slotPtr, const char *fmt,
|
||||
void *varargs);
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Interceptor fn);
|
||||
Interceptor fn;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
// The engine's runner (the MinHook trampoline) — for interceptors that run the
|
||||
// fire themselves, e.g. to bracket it, rather than suppress it. Valid only once
|
||||
// hooks are installed, i.e. from inside an interceptor.
|
||||
void Original(void *frame, uint32_t *slotPtr, const char *fmt, void *varargs);
|
||||
|
||||
} // namespace Frame::RunnerHook
|
||||
@@ -287,14 +287,13 @@ bool RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr)
|
||||
// lua_State: top @ +0x08 (TValue *, next free slot)
|
||||
// TValue: tag @ +0x00 (int); GC/closure pointer @ +0x08 (16-byte TValue)
|
||||
// Closure: isC byte @ +0x06; union member (C func / Proto) @ +0x0C
|
||||
// Proto: numparams (u8) @ +0x45, is_vararg (u8) @ +0x46
|
||||
// Single-use here, so kept local (like the lua_State field offsets elsewhere).
|
||||
// The Proto fields this reads (numparams @ +0x45, is_vararg @ +0x46) are shared
|
||||
// with LuaSyntax::Upvalues and live in Offsets.h as OFF_LUA_PROTO_*.
|
||||
constexpr uintptr_t OFF_LUASTATE_TOP = 0x08;
|
||||
constexpr uintptr_t OFF_TVALUE_GC = 0x08;
|
||||
constexpr uintptr_t OFF_CLOSURE_ISC = 0x06;
|
||||
constexpr uintptr_t OFF_LCLOSURE_PROTO = 0x0C;
|
||||
constexpr uintptr_t OFF_PROTO_NUMPARAMS = 0x45;
|
||||
constexpr uintptr_t OFF_PROTO_IS_VARARG = 0x46;
|
||||
constexpr int kLuaTFunction = 6;
|
||||
constexpr uintptr_t kTValueSize = 0x10;
|
||||
|
||||
@@ -322,8 +321,8 @@ bool HandlerWantsArgs(void *L, int handlerRef) {
|
||||
const void *proto = reinterpret_cast<const void *>(
|
||||
Game::Read<uintptr_t>(cl, OFF_LCLOSURE_PROTO));
|
||||
if (proto != nullptr)
|
||||
wants = Game::Read<uint8_t>(proto, OFF_PROTO_NUMPARAMS) > 0 ||
|
||||
Game::Read<uint8_t>(proto, OFF_PROTO_IS_VARARG) != 0;
|
||||
wants = Game::Read<uint8_t>(proto, Offsets::OFF_LUA_PROTO_NUMPARAMS) > 0 ||
|
||||
Game::Read<uint8_t>(proto, Offsets::OFF_LUA_PROTO_IS_VARARG) != 0;
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTop(L, -2); // pop the handler
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
// 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/>.
|
||||
|
||||
// `frame:RegisterUnitEvent(event, ...units)` — register for an event but only
|
||||
// receive it for the given unit tokens — plus the unit-aware
|
||||
// `frame:IsEventRegistered(event)` (registered [, unit…]).
|
||||
//
|
||||
// Vanilla owns everything except the filter, so the filter is all this adds:
|
||||
//
|
||||
// - Chain membership stays the engine's. RegisterUnitEvent delegates to the
|
||||
// engine's own `Script_RegisterEvent` (stack cut to (self, event)), so the
|
||||
// frame lands on the event's subscriber chain through the same code every
|
||||
// RegisterEvent uses — self typecheck, error text, and DllMain's
|
||||
// RegisterEvent hook included.
|
||||
// - The filter is stored here, keyed by frame, and dropped exactly where the
|
||||
// engine drops the frame's subscriber node: co-hooks on the C++ helpers
|
||||
// `Frame::UnregisterEvent` (FUN_FRAME_UNREGISTER_EVENT) and
|
||||
// `Frame::UnregisterAllEvents` (FUN_FRAME_UNREGISTER_ALL_EVENTS — also the
|
||||
// CScriptObject destructor's path, so frame destruction is covered).
|
||||
// - The check is one gate on the per-frame OnEvent fire. The dispatcher
|
||||
// FUN_FIRE_EVENT sets `_G.event`, walks the chain, and per frame calls the
|
||||
// runner FUN_FRAME_RUN_SCRIPT_WITH_CONTEXT(frame, frame+OFF_FRAME_ONEVENT_SLOT,
|
||||
// fmt, va). We subscribe to that runner through `Frame::RunnerHook` (it is
|
||||
// shared with Frame::ClickEvents — one MinHook per address), and when the
|
||||
// frame has a filter for the event being dispatched and arg1 matches none
|
||||
// of its tokens, we return without invoking the handler. Suppressing at the
|
||||
// runner also suppresses HookScript chains and Frame::ScriptArgs' positional
|
||||
// path — they all live downstream of it, which is what retail does too.
|
||||
//
|
||||
// Semantics mirror Blizzard's 5.4.8 implementation (Ghidra-verified), where the
|
||||
// filter lives ON the subscriber node and the node's kind is fixed when it is
|
||||
// created: RegisterEvent on a filtered registration leaves the filter;
|
||||
// RegisterUnitEvent on a plain registration stays plain; RegisterUnitEvent on a
|
||||
// filtered registration replaces the units; RegisterUnitEvent with no usable
|
||||
// unit is a plain RegisterEvent. Switching kinds takes UnregisterEvent first.
|
||||
//
|
||||
// Two places we deliberately don't mirror 5.4.8, both additive:
|
||||
//
|
||||
// - UNIT COUNT. 5.4.8 reads exactly two units (a hardcoded two-iteration loop
|
||||
// over Lua indices 3 and 4) and folds each token's table index into a byte
|
||||
// of one dword on the node — `filter = filter * 0x100 + idx`. Retail later
|
||||
// filled that dword and documents "up to four units", which its 5.4.8
|
||||
// reader already anticipated by walking all four bytes generically. We
|
||||
// store strings in our own map, so there is no dword to fill and no reason
|
||||
// to pick a number: every argument past the event name that is a string is
|
||||
// taken as a unit. Retail's four are covered by definition.
|
||||
// - TOKEN VALIDATION. 5.4.8 looks each unit up in a static canonical token
|
||||
// table (case-sensitive) and drops unknowns; we keep the string and compare
|
||||
// case-insensitively, because ClassicAPI's own tokens (`focus`,
|
||||
// `nameplateN`, `markN`) are not in the engine's table.
|
||||
//
|
||||
// Filter rule: the filter applies only when the event's first argument is a
|
||||
// string (every UNIT_* event fires as "%s" + token). A first spec that isn't
|
||||
// `%s`, or a no-arg event (those go through FUN_FIRE_EVENT_NO_ARGS → a runner
|
||||
// we never see), fires as with RegisterEvent — one rule that holds on every
|
||||
// dispatch path, whose failure mode is an extra event rather than a lost one.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/RunnerHook.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Frame::UnitEvent {
|
||||
|
||||
namespace {
|
||||
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
// Storm SStrCmpI — `__stdcall`, RET 0xC (see Offsets.h; a __cdecl declaration
|
||||
// drifts ESP by 12 per call).
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
int CallScript(uintptr_t fn, void *L) { return reinterpret_cast<ScriptFn_t>(fn)(L); }
|
||||
|
||||
bool EqualsIgnoreCase(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return false;
|
||||
auto cmp = reinterpret_cast<SStrCmpI_t>(static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I));
|
||||
return cmp(a, b, 0x7FFFFFFF) == 0;
|
||||
}
|
||||
|
||||
// One filtered registration — the unit filter 5.4.8 keeps on the event's
|
||||
// subscriber node for this frame. `event` is the CANONICAL name copied from the
|
||||
// event-table entry, so the fire-time compare against the dispatcher-set
|
||||
// `_G.event` (which is that same entry name) is a plain strcmp.
|
||||
struct Filter {
|
||||
std::string event;
|
||||
std::vector<std::string> units; // never empty; a filter with no unit is no filter
|
||||
};
|
||||
|
||||
// Keyed by frame. Entries die with the frame's subscriber node (the two
|
||||
// unregister co-hooks) and on /reload (PrepareForReload).
|
||||
std::unordered_map<const void *, std::vector<Filter>> g_filters;
|
||||
|
||||
// ---- event table -----------------------------------------------------------
|
||||
|
||||
// The live event-table entry whose name matches `eventName` (case-insensitive,
|
||||
// as Frame::RegisterEvent matches), or nullptr for an unknown event.
|
||||
const uint8_t *FindEventEntry(const char *eventName) {
|
||||
if (eventName == nullptr)
|
||||
return nullptr;
|
||||
const uint32_t count = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_COUNT));
|
||||
auto *base = Game::Read<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_BASE_PTR));
|
||||
if (count == 0 || base == nullptr)
|
||||
return nullptr;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const char *name = Game::Read<const char *>(entry, Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name != nullptr && EqualsIgnoreCase(name, eventName))
|
||||
return entry; // names are unique
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Is `frame` on `entry`'s subscriber chain — the membership walk
|
||||
// Frame::RegisterEvent performs before appending.
|
||||
bool ChainHas(const uint8_t *entry, const void *frame) {
|
||||
uintptr_t node = Game::Read<uintptr_t>(entry, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
while ((node & 1) == 0 && node != 0) {
|
||||
if (Game::Read<const void *>(node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
|
||||
return true;
|
||||
node = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- filter map --------------------------------------------------------------
|
||||
|
||||
Filter *FindFilter(const void *frame, const char *eventName) {
|
||||
auto it = g_filters.find(frame);
|
||||
if (it == g_filters.end())
|
||||
return nullptr;
|
||||
for (Filter &f : it->second)
|
||||
if (EqualsIgnoreCase(f.event.c_str(), eventName))
|
||||
return &f;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void EraseFilter(const void *frame, const char *eventName) {
|
||||
auto it = g_filters.find(frame);
|
||||
if (it == g_filters.end())
|
||||
return;
|
||||
std::vector<Filter> &v = it->second;
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
if (EqualsIgnoreCase(v[i].event.c_str(), eventName)) {
|
||||
v.erase(v.begin() + static_cast<ptrdiff_t>(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (v.empty())
|
||||
g_filters.erase(it);
|
||||
}
|
||||
|
||||
// ---- Lua methods --------------------------------------------------------------
|
||||
|
||||
// `frame:RegisterUnitEvent(event, ...units)` → registered.
|
||||
int __fastcall Script_RegisterUnitEvent(void *L) {
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: frame:RegisterUnitEvent(\"event\" [, \"unit1\", ...])");
|
||||
return 0;
|
||||
}
|
||||
void *frame = Game::Lua::ResolveFrame(L);
|
||||
if (frame == nullptr) {
|
||||
// Not a frame — hand it to the engine so it raises its own error, so
|
||||
// the message stays the engine's rather than a copy of it.
|
||||
Game::Lua::SetTop(L, 2);
|
||||
return CallScript(Offsets::FUN_SCRIPT_FRAME_REGISTEREVENT, L);
|
||||
}
|
||||
|
||||
// Read everything BEFORE reshaping the stack for the delegate — including
|
||||
// the top, which the reshape destroys. Like 5.4.8 we take only real strings
|
||||
// as units (a number is not coerced) and silently skip anything else.
|
||||
const std::string eventName = Game::Lua::ToString(L, 2);
|
||||
const int argTop = Game::Lua::GetTop(L);
|
||||
std::vector<std::string> units;
|
||||
for (int idx = 3; idx <= argTop; ++idx) {
|
||||
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_STRING)
|
||||
continue;
|
||||
const char *s = Game::Lua::ToString(L, idx);
|
||||
if (s != nullptr && *s != '\0')
|
||||
units.push_back(s);
|
||||
}
|
||||
const uint8_t *entry = FindEventEntry(eventName.c_str());
|
||||
const bool wasRegistered = entry != nullptr && ChainHas(entry, frame);
|
||||
|
||||
// Chain membership is the engine's: (self, event) → Script_RegisterEvent.
|
||||
Game::Lua::SetTop(L, 2);
|
||||
CallScript(Offsets::FUN_SCRIPT_FRAME_REGISTEREVENT, L);
|
||||
|
||||
// 5.4.8 node semantics (see the header). No usable unit → this was a plain
|
||||
// RegisterEvent and an existing filter is left alone; an unknown event has
|
||||
// no node to attach a filter to.
|
||||
if (entry != nullptr && !units.empty()) {
|
||||
if (Filter *f = FindFilter(frame, eventName.c_str())) {
|
||||
f->units = units; // filtered → replace the units
|
||||
} else if (!wasRegistered) {
|
||||
Filter fresh;
|
||||
fresh.event = Game::Read<const char *>(entry, Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
fresh.units = units;
|
||||
g_filters[frame].push_back(fresh);
|
||||
}
|
||||
// else: already registered plainly → stays plain
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, entry != nullptr && ChainHas(entry, frame));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `frame:IsEventRegistered(event)` → registered [, unit…]. Vanilla has
|
||||
// RegisterEvent / UnregisterEvent but not the query; the membership check is
|
||||
// the same chain walk Frame::RegisterEvent performs, so the answer is exactly
|
||||
// what the dispatcher sees. Units come back in registration order.
|
||||
int __fastcall Script_IsEventRegistered(void *L) {
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: frame:IsEventRegistered(\"event\")");
|
||||
return 0;
|
||||
}
|
||||
void *frame = Game::Lua::ResolveFrame(L);
|
||||
const char *eventName = Game::Lua::ToString(L, 2);
|
||||
const uint8_t *entry = (frame != nullptr) ? FindEventEntry(eventName) : nullptr;
|
||||
const bool registered = entry != nullptr && ChainHas(entry, frame);
|
||||
Game::Lua::PushBool(L, registered);
|
||||
if (!registered)
|
||||
return 1;
|
||||
const Filter *f = FindFilter(frame, eventName);
|
||||
if (f == nullptr)
|
||||
return 1;
|
||||
if (Game::Lua::CheckStack(L, static_cast<int>(f->units.size()) + 1) == 0)
|
||||
return 1; // can't grow the stack — report registered, drop the units
|
||||
int pushed = 1;
|
||||
for (const std::string &u : f->units) {
|
||||
Game::Lua::PushString(L, u.c_str());
|
||||
++pushed;
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
|
||||
// ---- the gate ------------------------------------------------------------------
|
||||
|
||||
// True (and `*out` = arg1) when the runner's first conversion spec is `%s` —
|
||||
// the unit token of every unit event. Anything else means arg1 isn't a string
|
||||
// and the filter does not apply.
|
||||
bool FirstArgIsString(const char *fmt, const void *varargs, const char **out) {
|
||||
if (fmt == nullptr || varargs == nullptr)
|
||||
return false;
|
||||
const char *p = std::strchr(fmt, '%');
|
||||
if (p == nullptr || p[1] != 's')
|
||||
return false;
|
||||
*out = *static_cast<const char *const *>(varargs);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Frame::RunnerHook interceptor. Returns true ONLY to suppress a fire (the
|
||||
// engine runner is then never called for it); every other path returns false
|
||||
// and the fire proceeds untouched. Touches no Lua code: `_G.event` is read with
|
||||
// RawGet so an `__index` metatable on _G can never run script here, and no
|
||||
// reference into g_filters outlives the call.
|
||||
bool OnRun(void *frame, uint32_t *slotPtr, const char *fmt, void *varargs) {
|
||||
// Only the dispatcher's OnEvent fire passes the OnEvent slot.
|
||||
if (frame == nullptr ||
|
||||
slotPtr != reinterpret_cast<uint32_t *>(static_cast<uint8_t *>(frame) +
|
||||
Offsets::OFF_FRAME_ONEVENT_SLOT))
|
||||
return false;
|
||||
// Frames without filters pay one probe and leave.
|
||||
if (g_filters.empty())
|
||||
return false;
|
||||
auto it = g_filters.find(frame);
|
||||
if (it == g_filters.end())
|
||||
return false;
|
||||
|
||||
// Which event? The dispatcher has just set `_G.event` to the entry's name.
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return false;
|
||||
Game::Lua::PushString(L, "event");
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
const char *ev = Game::Lua::ToString(L, -1);
|
||||
const Filter *match = nullptr;
|
||||
if (ev != nullptr) {
|
||||
for (const Filter &f : it->second) {
|
||||
if (std::strcmp(f.event.c_str(), ev) == 0) {
|
||||
match = &f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (match == nullptr)
|
||||
return false;
|
||||
|
||||
// The filter applies only when arg1 is a string.
|
||||
const char *arg1 = nullptr;
|
||||
if (!FirstArgIsString(fmt, varargs, &arg1))
|
||||
return false;
|
||||
if (arg1 != nullptr) {
|
||||
for (const std::string &u : match->units)
|
||||
if (EqualsIgnoreCase(arg1, u.c_str()))
|
||||
return false;
|
||||
}
|
||||
return true; // no token matched — suppress
|
||||
}
|
||||
|
||||
// ---- unregister co-hooks (the filter dies with the node) ------------------------
|
||||
|
||||
// `__thiscall(frame, eventName)` mimicked as `__fastcall(ecx, edx_unused, …)`.
|
||||
using UnregisterEvent_t = void(__fastcall *)(void *frame, void *edx, const char *eventName);
|
||||
UnregisterEvent_t g_origUnregisterEvent = nullptr;
|
||||
|
||||
void __fastcall UnregisterEvent_h(void *frame, void *edx, const char *eventName) {
|
||||
if (frame != nullptr && !g_filters.empty())
|
||||
EraseFilter(frame, eventName);
|
||||
g_origUnregisterEvent(frame, edx, eventName);
|
||||
}
|
||||
|
||||
// `__fastcall(frame)`; the Lua wrapper has dead `(0)` calls after lua_error, so
|
||||
// tolerate a null frame.
|
||||
using UnregisterAll_t = void(__fastcall *)(void *frame, void *edx);
|
||||
UnregisterAll_t g_origUnregisterAll = nullptr;
|
||||
|
||||
void __fastcall UnregisterAllEvents_h(void *frame, void *edx) {
|
||||
if (frame != nullptr && !g_filters.empty())
|
||||
g_filters.erase(frame);
|
||||
g_origUnregisterAll(frame, edx);
|
||||
}
|
||||
|
||||
// ---- registration -----------------------------------------------------------------
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
{"RegisterUnitEvent", &Script_RegisterUnitEvent},
|
||||
{"IsEventRegistered", &Script_IsEventRegistered},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods,
|
||||
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
|
||||
}
|
||||
|
||||
// Frames survive /reload but the engine rebuilds the event table with empty
|
||||
// chains and every frame re-registers from scratch, so the filters start over
|
||||
// too — and a recycled frame pointer can never inherit a stale one.
|
||||
void PrepareForReload() { g_filters.clear(); }
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
const Frame::RunnerHook::AutoSubscribe _runnerSub{&OnRun};
|
||||
|
||||
const Game::HookAutoRegister _hookUnregister{
|
||||
Offsets::FUN_FRAME_UNREGISTER_EVENT, reinterpret_cast<void *>(&UnregisterEvent_h),
|
||||
reinterpret_cast<void **>(&g_origUnregisterEvent)};
|
||||
const Game::HookAutoRegister _hookUnregisterAll{
|
||||
Offsets::FUN_FRAME_UNREGISTER_ALL_EVENTS,
|
||||
reinterpret_cast<void *>(&UnregisterAllEvents_h),
|
||||
reinterpret_cast<void **>(&g_origUnregisterAll)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Frame::UnitEvent
|
||||
@@ -18,8 +18,7 @@
|
||||
|
||||
#include "Notes.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "settings/Paths.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -41,43 +40,11 @@ std::unordered_map<uint64_t, Entry> g_notes;
|
||||
std::string g_path;
|
||||
bool g_loaded = false;
|
||||
|
||||
const char *ReadAccountName() {
|
||||
return Game::Read<const char *>(Offsets::VAR_ACCOUNT_NAME_PTR);
|
||||
}
|
||||
|
||||
const char *ReadRealmName() {
|
||||
auto *info = Game::Read<uint8_t *>(Offsets::VAR_REALM_INFO_PTR);
|
||||
if (info == nullptr)
|
||||
return nullptr;
|
||||
return Game::Read<const char *>(info, Offsets::OFF_REALM_INFO_NAME);
|
||||
}
|
||||
|
||||
const char *ReadCharacterName() {
|
||||
auto *p = reinterpret_cast<const char *>(Offsets::VAR_CHARACTER_NAME);
|
||||
return (p[0] == '\0') ? nullptr : p;
|
||||
}
|
||||
|
||||
// WTF\Account\<acct>\<realm>\<char>\ClassicAPI_FriendNotes.txt, or empty until
|
||||
// the account + realm + character session globals are populated. Per-character
|
||||
// because the friends list is per-character in vanilla.
|
||||
std::string ResolveFilePath() {
|
||||
const char *account = ReadAccountName();
|
||||
if (account == nullptr || account[0] == '\0')
|
||||
return {};
|
||||
const char *realm = ReadRealmName();
|
||||
if (realm == nullptr || realm[0] == '\0')
|
||||
return {};
|
||||
const char *character = ReadCharacterName();
|
||||
if (character == nullptr)
|
||||
return {};
|
||||
std::string out = "WTF\\Account\\";
|
||||
out += account;
|
||||
out += '\\';
|
||||
out += realm;
|
||||
out += '\\';
|
||||
out += character;
|
||||
out += "\\ClassicAPI_FriendNotes.txt";
|
||||
return out;
|
||||
return Settings::Paths::CharacterFile("ClassicAPI_FriendNotes.txt");
|
||||
}
|
||||
|
||||
// Notes are a single line each; the file is tab-delimited, so a note must
|
||||
|
||||
@@ -58,7 +58,7 @@ static int __fastcall Script_GameTooltipGetGameObject(void *L) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:GetGameObject()");
|
||||
return 0;
|
||||
}
|
||||
void *tooltipObj = Game::Lua::ResolveObject(L, 1);
|
||||
void *tooltipObj = Game::Lua::ResolveTooltip(L);
|
||||
if (tooltipObj == nullptr)
|
||||
return 0;
|
||||
|
||||
@@ -105,7 +105,7 @@ static int __fastcall Script_GameTooltipHasGameObject(void *L) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:HasGameObject()");
|
||||
return 0;
|
||||
}
|
||||
void *tooltipObj = Game::Lua::ResolveObject(L, 1);
|
||||
void *tooltipObj = Game::Lua::ResolveTooltip(L);
|
||||
if (tooltipObj == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "input/Modifier.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
@@ -241,4 +243,8 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t CurrentMask() {
|
||||
return g_modifierMask;
|
||||
}
|
||||
|
||||
} // namespace Input::Modifier
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Input::Modifier {
|
||||
|
||||
// The cached left/right modifier bitmap the `Is*KeyDown` globals read (bit
|
||||
// 0 LSHIFT, 1 RSHIFT, 2 LCTRL, 3 RCTRL, 4 LALT, 5 RALT). Maintained by the
|
||||
// thread message hook in Modifier.cpp, so it changes exactly when
|
||||
// `MODIFIER_STATE_CHANGED` fires. C++ pollers (the `#showtooltip`
|
||||
// evaluator) compare it across ticks to re-evaluate `[mod:...]`
|
||||
// conditionals the frame a modifier changes, instead of waiting for the
|
||||
// periodic pass.
|
||||
uint32_t CurrentMask();
|
||||
|
||||
} // namespace Input::Modifier
|
||||
+5
-1
@@ -29,7 +29,11 @@ Resolved Resolve(void *L, int idx) {
|
||||
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_STRING) {
|
||||
return out;
|
||||
}
|
||||
const char *s = Game::Lua::ToString(L, idx);
|
||||
return ResolveString(Game::Lua::ToString(L, idx));
|
||||
}
|
||||
|
||||
Resolved ResolveString(const char *s) {
|
||||
Resolved out{0, 0, nullptr};
|
||||
if (s == nullptr) {
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,16 @@ struct Resolved {
|
||||
// Returns `{0, nullptr}` for nil, tables, or other unsupported types.
|
||||
Resolved Resolve(void *L, int idx);
|
||||
|
||||
// String half of `Resolve`, for callers with no Lua stack (the
|
||||
// `#showtooltip` evaluator runs on the world tick). Same three string forms
|
||||
// and the same precedence: embedded `item:` link, then plain numeric, then
|
||||
// name. `name` aliases `s`, so keep `s` alive as long as the result.
|
||||
//
|
||||
// NOTE the plain-numeric branch: a caller where a bare number means
|
||||
// something else (a spellID, an equipment slot) must decide that BEFORE
|
||||
// calling, because this reads `"6948"` as an itemID.
|
||||
Resolved ResolveString(const char *s);
|
||||
|
||||
// Convenience: itemID only, dropping any name info. Equivalent to
|
||||
// `Resolve(L, idx).itemID`. Suitable for callers that don't support
|
||||
// name-based matching.
|
||||
|
||||
@@ -43,8 +43,6 @@ namespace Item::Consumable {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kItemClassConsumable = 0;
|
||||
constexpr uint32_t kInvTypeAmmo = 24; // INVTYPE_AMMO
|
||||
constexpr uint32_t kInvTypeThrown = 25; // INVTYPE_THROWN
|
||||
|
||||
int __fastcall Script_C_Item_IsConsumableItem(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
@@ -63,7 +61,8 @@ int __fastcall Script_C_Item_IsConsumableItem(void *L) {
|
||||
record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
|
||||
const bool consumable = classID == kItemClassConsumable ||
|
||||
invType == kInvTypeAmmo || invType == kInvTypeThrown;
|
||||
invType == Offsets::INVTYPE_AMMO ||
|
||||
invType == Offsets::INVTYPE_THROWN;
|
||||
Game::Lua::PushBoolean(L, consumable);
|
||||
return 1;
|
||||
}
|
||||
|
||||
+36
-24
@@ -23,6 +23,7 @@
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Item::Count {
|
||||
|
||||
@@ -73,16 +74,17 @@ int GetItemContribution(const uint8_t *cgItem, bool includeUses) {
|
||||
}
|
||||
|
||||
// Walks `bagID` and accumulates per-slot contributions for items
|
||||
// matching `targetItemID`. Skips empty slots. Each `ResolveBag` call
|
||||
// clobbers Lua stack[1]/[2]; we own the stack inside the callback.
|
||||
int CountInBag(void *L, int bagID, int targetItemID, bool includeUses) {
|
||||
// matching `arg` (itemID, or name via `MatchesArg`). Skips empty slots.
|
||||
// Each `ResolveBag` call clobbers Lua stack[1]/[2]; we own the stack
|
||||
// inside the callback.
|
||||
int CountInBag(void *L, int bagID, const Item::Arg::Resolved &arg, bool includeUses) {
|
||||
int total = 0;
|
||||
const int slotCount = Item::Location::GetBagSlotCount(bagID);
|
||||
for (int slot = 1; slot <= slotCount; slot++) {
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bagID, slot);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
if (Item::ID::FromCGItem(item) != targetItemID)
|
||||
if (!Item::Location::MatchesArg(item, arg))
|
||||
continue;
|
||||
total += GetItemContribution(item, includeUses);
|
||||
}
|
||||
@@ -110,7 +112,7 @@ const uint8_t *ResolveByGuid(int type, uint64_t guid) {
|
||||
// main bank (slots 39..62 in player invMgr) and individual bank-bag
|
||||
// invMgrs (slots 0..N-1).
|
||||
int CountInGuidArray(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
int targetItemID, bool includeUses) {
|
||||
const Item::Arg::Resolved &arg, bool includeUses) {
|
||||
if (invMgr == nullptr)
|
||||
return 0;
|
||||
auto *guidArray = *reinterpret_cast<const uint64_t *const *>(
|
||||
@@ -122,7 +124,7 @@ int CountInGuidArray(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
const uint8_t *item = ResolveByGuid(Offsets::TYPEMASK_ITEM, guidArray[slot]);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
if (Item::ID::FromCGItem(item) != targetItemID)
|
||||
if (!Item::Location::MatchesArg(item, arg))
|
||||
continue;
|
||||
total += GetItemContribution(item, includeUses);
|
||||
}
|
||||
@@ -136,7 +138,7 @@ int CountInGuidArray(const uint8_t *invMgr, int firstSlot, int lastSlot,
|
||||
//
|
||||
// Layout: bag invMgr+0x00 = max slot count (the bag's size); bag
|
||||
// invMgr+0x04 = its GUID array. Same shape as the player invMgr.
|
||||
int CountInBankBags(int targetItemID, bool includeUses) {
|
||||
int CountInBankBags(const Item::Arg::Resolved &arg, bool includeUses) {
|
||||
auto *playerInvMgr = Unit::Identity::PlayerInventoryManager();
|
||||
if (playerInvMgr == nullptr)
|
||||
return 0;
|
||||
@@ -159,8 +161,7 @@ int CountInBankBags(int targetItemID, bool includeUses) {
|
||||
const int bagSlots = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInvMgr));
|
||||
if (bagSlots <= 0)
|
||||
continue;
|
||||
total += CountInGuidArray(bagInvMgr, 0, bagSlots - 1, targetItemID,
|
||||
includeUses);
|
||||
total += CountInGuidArray(bagInvMgr, 0, bagSlots - 1, arg, includeUses);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
@@ -169,13 +170,13 @@ int CountInBankBags(int targetItemID, bool includeUses) {
|
||||
// for items matching `targetItemID`. Equipped items are part of the
|
||||
// player's total inventory in the modern API — `GetItemCount` of a
|
||||
// currently-equipped trinket returns 1, not 0.
|
||||
int CountEquipped(int targetItemID, bool includeUses) {
|
||||
int CountEquipped(const Item::Arg::Resolved &arg, bool includeUses) {
|
||||
int total = 0;
|
||||
for (int slot = 1; slot <= 19; slot++) {
|
||||
const uint8_t *item = Item::Location::ResolveEquipmentSlot(slot);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
if (Item::ID::FromCGItem(item) != targetItemID)
|
||||
if (!Item::Location::MatchesArg(item, arg))
|
||||
continue;
|
||||
total += GetItemContribution(item, includeUses);
|
||||
}
|
||||
@@ -184,9 +185,12 @@ int CountEquipped(int targetItemID, bool includeUses) {
|
||||
|
||||
// `C_Item.GetItemCount(itemInfo [, includeBank [, includeUses]])` —
|
||||
// returns the player's total count of `itemInfo` across equipped
|
||||
// items + bags (and optionally bank). `itemInfo` is a number or
|
||||
// `"item:NNN"` string. Item names are NOT accepted (vanilla itself
|
||||
// has no name → ID resolver).
|
||||
// items + bags (and optionally bank). `itemInfo` is a number, an
|
||||
// `"item:NNN"` string / link, or an item NAME — a plain string is matched
|
||||
// case-insensitively against each carried item's decorated display name
|
||||
// (`Item::Location::MatchesArg`, the same rule `C_Item.UseItemByName`
|
||||
// uses), which is what lets the `/cast` / `/use` handler decide
|
||||
// "is this an item I carry" before falling back to a spell cast.
|
||||
//
|
||||
// What's counted:
|
||||
// - **Equipped items** (slots 1..19) — always. Matches modern
|
||||
@@ -210,10 +214,17 @@ int CountEquipped(int targetItemID, bool includeUses) {
|
||||
// count (via the `or 1` neutralizer in `GetUsesPerItem`), so the flag
|
||||
// is a no-op for them.
|
||||
int __fastcall Script_C_Item_GetItemCount(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0) {
|
||||
Game::Lua::PushNumber(L, 0);
|
||||
return 1;
|
||||
Item::Arg::Resolved arg = Item::Arg::Resolve(L, 1);
|
||||
// A plain string is a name. Copy it off the Lua string heap before the
|
||||
// bag walk — `ResolveBag` clears the stack.
|
||||
char nameBuf[128];
|
||||
if (arg.itemID <= 0) {
|
||||
if (arg.name == nullptr || arg.name[0] == '\0') {
|
||||
Game::Lua::PushNumber(L, 0);
|
||||
return 1;
|
||||
}
|
||||
std::snprintf(nameBuf, sizeof(nameBuf), "%s", arg.name);
|
||||
arg.name = nameBuf;
|
||||
}
|
||||
// Read both flags before the bag walk — `ResolveBag` clobbers
|
||||
// the Lua stack via SetTop(0) and would invalidate any later
|
||||
@@ -221,9 +232,9 @@ int __fastcall Script_C_Item_GetItemCount(void *L) {
|
||||
const bool includeBank = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
const bool includeUses = Game::Lua::ToBoolean(L, 3) != 0;
|
||||
|
||||
int total = CountEquipped(itemID, includeUses);
|
||||
int total = CountEquipped(arg, includeUses);
|
||||
for (int bag = 0; bag <= 4; bag++)
|
||||
total += CountInBag(L, bag, itemID, includeUses);
|
||||
total += CountInBag(L, bag, arg, includeUses);
|
||||
if (includeBank) {
|
||||
// Direct GUID-array reads — bypass the bank gate so this works
|
||||
// without the bank window having ever been opened in the
|
||||
@@ -231,8 +242,8 @@ int __fastcall Script_C_Item_GetItemCount(void *L) {
|
||||
total += CountInGuidArray(Unit::Identity::PlayerInventoryManager(),
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT,
|
||||
itemID, includeUses);
|
||||
total += CountInBankBags(itemID, includeUses);
|
||||
arg, includeUses);
|
||||
total += CountInBankBags(arg, includeUses);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
@@ -245,9 +256,10 @@ int __fastcall Script_C_Item_GetItemCount(void *L) {
|
||||
int InInventory(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
int total = CountEquipped(itemID, /*includeUses*/ false);
|
||||
const Item::Arg::Resolved arg{itemID, 0, nullptr};
|
||||
int total = CountEquipped(arg, /*includeUses*/ false);
|
||||
for (int bag = 0; bag <= 4; bag++)
|
||||
total += CountInBag(L, bag, itemID, /*includeUses*/ false);
|
||||
total += CountInBag(L, bag, arg, /*includeUses*/ false);
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
+36
-23
@@ -135,19 +135,28 @@ int __fastcall Script_C_Item_IsEquippedItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.EquipItemByName(itemInfo [, dstSlot])` — finds the first
|
||||
// item in the player's bags matching `itemInfo` (itemID, link, or
|
||||
// name) and equips it.
|
||||
// `C_Item.EquipItemByName(item [, dstSlot])` — equips `item`.
|
||||
//
|
||||
// `item` is an item reference (itemID, `item:N`, a link, or the name of an
|
||||
// item the player carries — the first bag match is equipped) or an item
|
||||
// location (`{bagID=B, slotIndex=S}`, `{equipmentSlotIndex=N}`, an item
|
||||
// GUID), which names one exact item. See
|
||||
// `Item::Location::FindItemArgOrLocation`, shared with the item-use APIs;
|
||||
// this call wants the source coordinates too, since the swap packet carries
|
||||
// them.
|
||||
//
|
||||
// Two paths after a shared cursor-clear:
|
||||
//
|
||||
// - **Explicit `dstSlot`** (1..19): cursor-free direct swap via
|
||||
// `Item::Swap::FromBag`, which calls the engine's
|
||||
// `FUN_INVENTORY_SWAP` primitive. Atomic server-side, no pickup.
|
||||
// - **Explicit `dstSlot`** (1..19): cursor-free direct swap via the engine's
|
||||
// `FUN_INVENTORY_SWAP` primitive — atomic server-side, no pickup.
|
||||
// `Item::Swap::FromBag` for an item in a bag, `FromPaperdoll` for one
|
||||
// already equipped (an `{equipmentSlotIndex=N}` location, which is how a
|
||||
// ring 11↔12 or weapon 16↔17 move is expressed).
|
||||
//
|
||||
// - **No `dstSlot`** (engine auto-picks slot from INVTYPE): cursor-
|
||||
// pickup + `AutoEquipCursorItem`, because 1.12's auto-pick logic
|
||||
// reads its slot decision off cursor state.
|
||||
// reads its slot decision off cursor state. An already-equipped item has
|
||||
// nothing to auto-pick, so that combination does nothing.
|
||||
//
|
||||
// Both paths start with `ClearCursor()` to clear any preexisting
|
||||
// cursor state. If something was on the cursor, it gets returned
|
||||
@@ -157,37 +166,41 @@ int __fastcall Script_C_Item_IsEquippedItem(void *L) {
|
||||
// item-flag clear (`FUN_00495190(0, 1)`); a held item would stay
|
||||
// visually locked until a relog refreshed inventory state.
|
||||
//
|
||||
// Returns nothing. Silently no-ops on bad/missing input, item not
|
||||
// found in bags, or engine refusing the swap (combat lockdown,
|
||||
// The item is resolved AFTER that clear, so a location naming the slot a
|
||||
// held item came from resolves to it (its slot reads empty while held) and a
|
||||
// name search sees the returned item too. `Script_ClearCursor` is
|
||||
// `FUN_00495190(1, 1)` and nothing else — it takes no arguments and reads no
|
||||
// Lua stack — so our own arguments survive it.
|
||||
//
|
||||
// Returns nothing. Silently no-ops on bad/missing input, an item the player
|
||||
// doesn't have, or the engine refusing the swap (combat lockdown,
|
||||
// item-locked flag, type mismatch).
|
||||
int __fastcall Script_C_Item_EquipItemByName(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const bool hasDstSlot = Game::Lua::IsNumber(L, 2);
|
||||
const int dstSlot = hasDstSlot ? static_cast<int>(Game::Lua::ToNumber(L, 2)) : 0;
|
||||
|
||||
// Return any held cursor item to its slot BEFORE searching for
|
||||
// the target. FUN_INVENTORY_SWAP's end-of-call cleanup at
|
||||
// FUN_00495190(0, 1) clears LOCAL cursor globals but skips the
|
||||
// visual-lock-flag clear (`item+0x314 & ~1`) — so a held item
|
||||
// would stay visually locked until a relog refreshed state.
|
||||
// ClearCursor's FUN_00495190(1, 1) does include that clear.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
// Return any held cursor item to its slot BEFORE resolving the target.
|
||||
// FUN_INVENTORY_SWAP's end-of-call cleanup at FUN_00495190(0, 1) clears
|
||||
// LOCAL cursor globals but skips the visual-lock-flag clear
|
||||
// (`item+0x314 & ~1`) — so a held item would stay visually locked until
|
||||
// a relog refreshed state. ClearCursor's FUN_00495190(1, 1) includes it.
|
||||
reinterpret_cast<int(__fastcall *)(void *)>(
|
||||
Offsets::FUN_SCRIPT_CLEAR_CURSOR)(L);
|
||||
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
if (!Item::Location::FindItemArgOrLocation(L, 1, &found)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (hasDstSlot) {
|
||||
Item::Swap::FromBag(found.item, found.bagID, found.slotIndex, dstSlot);
|
||||
if (found.equipmentSlotIndex != 0)
|
||||
Item::Swap::FromPaperdoll(found.item, found.equipmentSlotIndex, dstSlot);
|
||||
else
|
||||
Item::Swap::FromBag(found.item, found.bagID, found.slotIndex, dstSlot);
|
||||
return 0;
|
||||
}
|
||||
if (found.equipmentSlotIndex != 0)
|
||||
return 0; // already equipped — no slot for the engine to pick
|
||||
|
||||
// Auto-slot path: pick the item up onto the cursor, then call the
|
||||
// engine's `CGPlayer::AutoEquipCursorItem` helper directly
|
||||
|
||||
+2
-15
@@ -26,12 +26,12 @@
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Icon.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Spell.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Item::GetData {
|
||||
|
||||
@@ -85,19 +85,6 @@ const char *LookupInvType(uint32_t invType) {
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr || iconName[0] == '\0')
|
||||
return false;
|
||||
std::snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wrath flipped the bagFamily encoding from raw 1-based IDs to a
|
||||
// bitmask. We surface the modern shape so backported addons compile
|
||||
// cleanly. See `Item::Info::BagFamilyIdToBitmask` for the full story.
|
||||
@@ -247,7 +234,7 @@ void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
Game::Lua::SetFieldNumber(L, "displayInfoID", static_cast<double>(displayInfoID));
|
||||
char iconPath[260];
|
||||
if (BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
if (Item::Icon::PathForDisplayInfoID(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
Game::Lua::SetFieldString(L, "icon", iconPath);
|
||||
|
||||
// Quality / classification
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "item/Icon.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace Item::Icon {
|
||||
|
||||
bool PathForDisplayInfoID(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr || iconName[0] == '\0')
|
||||
return false;
|
||||
std::snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PathForItemID(uint32_t itemID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
return PathForDisplayInfoID(
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID), out, outSize);
|
||||
}
|
||||
|
||||
} // namespace Item::Icon
|
||||
@@ -0,0 +1,33 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::Icon {
|
||||
|
||||
// Writes `Interface\Icons\<name>` for an `ItemDisplayInfo.dbc` row into
|
||||
// `out`. The one place this path is built — `GetItemInfo`,
|
||||
// `GetItemInfoInstant`, `C_Item.GetItemData` and the container getters all
|
||||
// come through here, so a change to the chain has a single site. Returns
|
||||
// false (out empty) when the row is missing or carries no icon.
|
||||
bool PathForDisplayInfoID(uint32_t displayInfoID, char *out, size_t outSize);
|
||||
|
||||
// Same, starting from an itemID: the cached item's ItemStats
|
||||
// `displayInfoID`, then the row above. Returns false when the item isn't
|
||||
// cached. Passive: never warms the cache.
|
||||
bool PathForItemID(uint32_t itemID, char *out, size_t outSize);
|
||||
|
||||
} // namespace Item::Icon
|
||||
+7
-18
@@ -18,12 +18,12 @@
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Icon.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Item::Info {
|
||||
@@ -72,19 +72,6 @@ static const char *LookupInvType(uint32_t invType) {
|
||||
return PushedOrEmpty(table[invType]);
|
||||
}
|
||||
|
||||
static bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = 0;
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr)
|
||||
return false;
|
||||
snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// `GetItemInfoInstant` is the modern "synchronous, never blocks" item
|
||||
// info call. In modern WoW the trailing 6 fields come from client-side
|
||||
// `Item-sparse.db2` and so the call always returns the full 7-tuple.
|
||||
@@ -121,7 +108,7 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
if (!Item::Icon::PathForDisplayInfoID(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
|
||||
Game::Lua::PushString(L, LookupItemClassName(classID));
|
||||
@@ -141,12 +128,14 @@ static int PushIconForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
if (record == nullptr) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
}
|
||||
const uint32_t displayInfoID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
if (!Item::Icon::PathForDisplayInfoID(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
return 0;
|
||||
Game::Lua::PushString(L, iconPath);
|
||||
return 1;
|
||||
@@ -290,7 +279,7 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
: name;
|
||||
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
if (!Item::Icon::PathForDisplayInfoID(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
char link[256];
|
||||
const bool haveLink = Item::Link::BasicFromIDSuffix(static_cast<uint32_t>(itemID),
|
||||
|
||||
+118
-12
@@ -234,6 +234,60 @@ bool FindByArg(void *L, const Item::Arg::Resolved &arg, ByGUIDResult *out) {
|
||||
return FindByArgInBags(L, arg, out);
|
||||
}
|
||||
|
||||
const uint8_t *ResolveBagSlotNoLua(int bagID, int slotIndex) {
|
||||
if (slotIndex < 1)
|
||||
return nullptr;
|
||||
auto GetItemBySlot = reinterpret_cast<GetItemBySlot_t>(
|
||||
Offsets::FUN_ITEMMGR_GET_ITEM_BY_SLOT);
|
||||
if (bagID == 0) {
|
||||
if (slotIndex > Offsets::BACKPACK_NUM_SLOTS)
|
||||
return nullptr;
|
||||
void *invMgr = const_cast<uint8_t *>(Unit::Identity::PlayerInventoryManager());
|
||||
if (invMgr == nullptr)
|
||||
return nullptr;
|
||||
return static_cast<const uint8_t *>(
|
||||
GetItemBySlot(invMgr, Offsets::BACKPACK_LINEAR_BASE + slotIndex - 1));
|
||||
}
|
||||
void *bagInv = EquippedBagInventory(bagID);
|
||||
if (bagInv == nullptr)
|
||||
return nullptr;
|
||||
const int slotCount = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInv));
|
||||
if (slotIndex > slotCount)
|
||||
return nullptr;
|
||||
return static_cast<const uint8_t *>(GetItemBySlot(bagInv, slotIndex - 1));
|
||||
}
|
||||
|
||||
bool FindByArgNoLua(const Item::Arg::Resolved &arg, ByGUIDResult *out) {
|
||||
if (arg.itemID <= 0 && arg.name == nullptr)
|
||||
return false;
|
||||
|
||||
for (int slot = Offsets::EQUIPMENT_SLOT_FIRST;
|
||||
slot <= Offsets::EQUIPMENT_SLOT_LAST; ++slot) {
|
||||
auto *item = ResolveEquipmentSlot(slot);
|
||||
if (item != nullptr && MatchesArg(item, arg)) {
|
||||
out->equipmentSlotIndex = slot;
|
||||
out->bagID = 0;
|
||||
out->slotIndex = 0;
|
||||
out->item = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int bagID = 0; bagID <= 4; ++bagID) {
|
||||
const int slotCount = GetBagSlotCount(bagID);
|
||||
for (int slotIndex = 1; slotIndex <= slotCount; ++slotIndex) {
|
||||
auto *item = ResolveBagSlotNoLua(bagID, slotIndex);
|
||||
if (item == nullptr || !MatchesArg(item, arg))
|
||||
continue;
|
||||
out->equipmentSlotIndex = 0;
|
||||
out->bagID = bagID;
|
||||
out->slotIndex = slotIndex;
|
||||
out->item = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FindByGUID(void *L, uint64_t guid, ByGUIDResult *out) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
@@ -280,32 +334,84 @@ bool IsLocationArg(void *L, int idx) {
|
||||
return t == Game::Lua::TYPE_TABLE || t == Game::Lua::TYPE_STRING;
|
||||
}
|
||||
|
||||
const uint8_t *Resolve(void *L, int locIdx) {
|
||||
// The location forms, reporting WHERE the item is as well as which one.
|
||||
// `Resolve` is this minus the coordinates; the argument resolvers below add
|
||||
// the item-reference forms on top.
|
||||
static bool ResolveLocationDetail(void *L, int locIdx, ByGUIDResult *out) {
|
||||
*out = ByGUIDResult{};
|
||||
const int t = Game::Lua::Type(L, locIdx);
|
||||
|
||||
if (t == Game::Lua::TYPE_STRING) {
|
||||
uint64_t guid = 0;
|
||||
if (!ParseGUIDString(Game::Lua::ToString(L, locIdx), &guid))
|
||||
return nullptr;
|
||||
ByGUIDResult found;
|
||||
if (!FindByGUID(L, guid, &found))
|
||||
return nullptr;
|
||||
return found.item;
|
||||
return false;
|
||||
if (!FindByGUID(L, guid, out)) {
|
||||
*out = ByGUIDResult{}; // a miss may have written into it
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (t != Game::Lua::TYPE_TABLE)
|
||||
return nullptr;
|
||||
return false;
|
||||
|
||||
int eqSlot = 0;
|
||||
if (TryReadIntField(L, locIdx, "equipmentSlotIndex", &eqSlot))
|
||||
return ResolveEquipmentSlot(eqSlot);
|
||||
if (TryReadIntField(L, locIdx, "equipmentSlotIndex", &eqSlot)) {
|
||||
out->equipmentSlotIndex = eqSlot;
|
||||
out->item = ResolveEquipmentSlot(eqSlot);
|
||||
return out->item != nullptr;
|
||||
}
|
||||
|
||||
int bagID = 0, slotIndex = 0;
|
||||
if (TryReadIntField(L, locIdx, "bagID", &bagID) &&
|
||||
TryReadIntField(L, locIdx, "slotIndex", &slotIndex))
|
||||
return ResolveBagSlot(L, bagID, slotIndex);
|
||||
TryReadIntField(L, locIdx, "slotIndex", &slotIndex)) {
|
||||
out->bagID = bagID;
|
||||
out->slotIndex = slotIndex;
|
||||
out->item = ResolveBagSlot(L, bagID, slotIndex);
|
||||
return out->item != nullptr;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *Resolve(void *L, int locIdx) {
|
||||
ByGUIDResult found;
|
||||
return ResolveLocationDetail(L, locIdx, &found) ? found.item : nullptr;
|
||||
}
|
||||
|
||||
bool FindItemArgOrLocation(void *L, int idx, ByGUIDResult *out) {
|
||||
*out = ByGUIDResult{};
|
||||
const int t = Game::Lua::Type(L, idx);
|
||||
|
||||
// A table is only ever a location, and so is a string that parses as an
|
||||
// item GUID — a strict `0x` + 16 hex digits, which no item link or item
|
||||
// name can look like. Decide from the argument alone, and commit: a
|
||||
// location resolve STOMPS the Lua stack (`PackBagSlot` overwrites the
|
||||
// first stack slots with its own arguments), so falling back to the
|
||||
// reference forms after one has run would re-read the argument as
|
||||
// whatever it left behind — a bag index read as an itemID, which names
|
||||
// an unrelated item.
|
||||
if (t == Game::Lua::TYPE_TABLE)
|
||||
return ResolveLocationDetail(L, idx, out);
|
||||
if (t == Game::Lua::TYPE_STRING) {
|
||||
uint64_t guid = 0;
|
||||
if (ParseGUIDString(Game::Lua::ToString(L, idx), &guid)) {
|
||||
if (FindByGUID(L, guid, out))
|
||||
return true;
|
||||
*out = ByGUIDResult{}; // a miss may have written into it
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const Item::Arg::Resolved arg = Item::Arg::Resolve(L, idx);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr)
|
||||
return false;
|
||||
return FindByArgInBags(L, arg, out);
|
||||
}
|
||||
|
||||
const uint8_t *ResolveItemArgOrLocation(void *L, int idx) {
|
||||
ByGUIDResult found;
|
||||
return FindItemArgOrLocation(L, idx, &found) ? found.item : nullptr;
|
||||
}
|
||||
|
||||
} // namespace Item::Location
|
||||
|
||||
@@ -171,6 +171,46 @@ bool FindByArgInBags(void *L, const Item::Arg::Resolved &arg, ByGUIDResult *out)
|
||||
// Lua stack.
|
||||
bool FindByArg(void *L, const Item::Arg::Resolved &arg, ByGUIDResult *out);
|
||||
|
||||
// Resolves the item argument of the "use this item" APIs, which take
|
||||
// EITHER an item location or an item reference:
|
||||
// - a location — `{equipmentSlotIndex=N}` / `{bagID=B, slotIndex=S}`, or
|
||||
// an item GUID string — names ONE exact item, so it is tried first and
|
||||
// resolved through `Resolve`;
|
||||
// - a reference — itemID, `item:N`, an item link, or the name of an item
|
||||
// the player carries — is searched for with `FindByArgInBags`, which
|
||||
// takes the first match.
|
||||
// The distinction matters wherever two stacks of the same item are held: a
|
||||
// location says which one, a reference does not.
|
||||
//
|
||||
// Returns nullptr when the argument is neither form, or names nothing the
|
||||
// player has. Stomps the Lua stack (both paths) — read every other argument
|
||||
// off the stack before calling.
|
||||
const uint8_t *ResolveItemArgOrLocation(void *L, int idx);
|
||||
|
||||
// Same resolution, for callers that need WHERE the item is and not only
|
||||
// which one — the equip paths encode the source slot in their packet. Fills
|
||||
// `*out` the way `FindByArg` does: `equipmentSlotIndex` non-zero for an
|
||||
// equipped item, else `bagID` / `slotIndex`. A reference is searched for in
|
||||
// the bags only, so it never resolves to an equipped item; a location can
|
||||
// name either.
|
||||
bool FindItemArgOrLocation(void *L, int idx, ByGUIDResult *out);
|
||||
|
||||
// Resolves `(bagID, slotIndex)` to its `CGItem *` WITHOUT the Lua stack:
|
||||
// the backpack (bag 0) is indexed straight off the player inventory manager
|
||||
// at `BACKPACK_LINEAR_BASE + slot - 1`, bags 1..4 through
|
||||
// `EquippedBagInventory` — both via the engine's `GetItemBySlot`. Returns
|
||||
// nullptr for a bad bag, an out-of-range slot, or an empty slot. Safe from
|
||||
// any context (world tick, packet hooks) — the `PackBagSlot`-based
|
||||
// `ResolveBag` is not, because it reads its inputs off the Lua stack.
|
||||
const uint8_t *ResolveBagSlotNoLua(int bagID, int slotIndex);
|
||||
|
||||
// `FindByArg` for callers with no Lua callback context: same equipment-first,
|
||||
// bags-second walk and the same `MatchesArg` predicate, built on
|
||||
// `ResolveEquipmentSlot` + `ResolveBagSlotNoLua`. Pure C — used by the
|
||||
// `#showtooltip` evaluator on the world tick and by the action-bar display
|
||||
// overrides at hover time.
|
||||
bool FindByArgNoLua(const Item::Arg::Resolved &arg, ByGUIDResult *out);
|
||||
|
||||
// Parses the `"0xHHHHHHHHLLLLLLLL"` GUID string format
|
||||
// `C_Item.GetItemGUID` returns. Strict: requires exactly `0x` prefix
|
||||
// + 16 case-insensitive hex digits. Returns false (and leaves `*out`
|
||||
|
||||
+213
-47
@@ -55,35 +55,53 @@ bool ReadGuid(const void *cgObject, uint32_t *lo, uint32_t *hi) {
|
||||
// the magic +19 in FromBag is explained at the call site.
|
||||
constexpr int FIRST_BAG_CONTAINER_LINEAR = 19;
|
||||
|
||||
// Backpack contents (bag 0) live at linear slots 23..38 in the
|
||||
// player invMgr — i.e. 1-based bag-0 slot S maps to linear 22+S.
|
||||
constexpr int BACKPACK_LINEAR_OFFSET = 22;
|
||||
// Player-container linear-slot bases, DERIVED from the named
|
||||
// first-slot constants in Offsets.h rather than restated. The whole map
|
||||
// (paperdoll / backpack / bank / bank bags / keyring) is documented at
|
||||
// `Offsets::OFF_INVMGR_GUID_ARRAY`; a 1-based slot S in one of these
|
||||
// ranges maps to `base + S`, so each base is that range's first linear
|
||||
// slot minus one.
|
||||
constexpr int BACKPACK_LINEAR_OFFSET = Offsets::INVMGR_BACKPACK_FIRST_SLOT - 1;
|
||||
constexpr int BANK_MAIN_LINEAR_OFFSET = Offsets::INVMGR_BANK_MAIN_FIRST_SLOT - 1;
|
||||
constexpr int KEYRING_LINEAR_OFFSET = Offsets::INVMGR_KEYRING_FIRST_SLOT - 1;
|
||||
|
||||
// Main bank contents (bagID -1) live at linear slots 39..62 in the
|
||||
// player invMgr — i.e. 1-based bank slot S maps to linear 38+S.
|
||||
// Same encoding shape as the backpack (container is the player
|
||||
// itself), different base offset. Range valid only when the bank
|
||||
// window has been opened in the current session.
|
||||
constexpr int BANK_MAIN_LINEAR_OFFSET = 38;
|
||||
constexpr int BANK_MAIN_NUM_SLOTS = 24;
|
||||
|
||||
// Bank bag containers occupy 1-based equipment slots 64..69 (linear
|
||||
// 63..68). Addressed by bagID 5..10 — bagID 5 = first bank bag.
|
||||
// Bank bag containers are addressed by bagID 5..10 — bagID 5 = first
|
||||
// bank bag. `ResolveEquipmentSlot` takes a 1-based equipment slot, so
|
||||
// the base is the bank-bag range's first linear slot plus one.
|
||||
// Requires bank window open; otherwise the engine's CGContainer for
|
||||
// the bag is unsynced and `ResolveEquipmentSlot` returns null.
|
||||
constexpr int FIRST_BANK_BAG_EQ_SLOT = 64;
|
||||
constexpr int FIRST_BANK_BAG_EQ_SLOT = Offsets::INVMGR_BANK_BAG_FIRST_SLOT + 1;
|
||||
constexpr int FIRST_BANK_BAG_ID = 5;
|
||||
constexpr int LAST_BANK_BAG_ID = 10;
|
||||
|
||||
// Bounds a player-container linear slot the way `FUN_PACK_BAG_SLOT`
|
||||
// does — against the invMgr's own live slot count, not a hardcoded
|
||||
// size. That is what makes the keyring addressable without knowing its
|
||||
// capacity, which grows as the character unlocks slots.
|
||||
bool LinearSlotInRange(const void *player, uint32_t linearSlot) {
|
||||
const uint32_t count = Game::Read<uint32_t>(
|
||||
player,
|
||||
Offsets::OFF_PLAYER_INVENTORY_MANAGER + Offsets::OFF_INVMGR_SLOT_COUNT);
|
||||
return linearSlot < count;
|
||||
}
|
||||
|
||||
// (bagID, 1-based slot) → (containerGUID, engine linear slot). Returns
|
||||
// false for unequipped bags or out-of-range slots; does NOT validate
|
||||
// that the slot is occupied (caller checks separately).
|
||||
//
|
||||
// bagID 0 → container = player, linear = 22 + slot (backpack)
|
||||
// bagID 1..4 → container = equipped bag's CGContainer, linear = slot - 1
|
||||
// bagID -1 → container = player, linear = 38 + slot (bank main)
|
||||
// bagID 0 → container = player, linear = 22 + slot (backpack)
|
||||
// bagID 1..4 → container = equipped bag's CGContainer, linear = slot - 1
|
||||
// bagID -1 → container = player, linear = 38 + slot (bank main)
|
||||
// bagID -2 → container = player, linear = 80 + slot (keyring)
|
||||
// bagID 5..10 → container = bank bag's CGContainer, linear = slot - 1
|
||||
//
|
||||
// This mirrors the engine's own bagID→(container, linear slot) map in
|
||||
// `FUN_PACK_BAG_SLOT`, minus that function's Lua coupling: it reads its
|
||||
// bagID and slot off the Lua stack and stomps it, which is why this is
|
||||
// a separate encoder rather than a call into it. Keep the two in step —
|
||||
// the keyring row above exists because this encoder was missing it
|
||||
// while the engine's map had it all along.
|
||||
//
|
||||
// Bank-side encodings only resolve once the bank window has been
|
||||
// opened in the current session (the engine doesn't sync the bank
|
||||
// CGContainers or bank-slot inventory entries before then).
|
||||
@@ -92,15 +110,41 @@ bool EncodeBagSlot(int bagID, int slotInBag,
|
||||
uint32_t *linearSlot) {
|
||||
if (slotInBag < 1)
|
||||
return false;
|
||||
|
||||
// Backpack, main bank and keyring all live directly in the player's
|
||||
// invMgr and differ only in their base — and in whether the range
|
||||
// has a fixed end. `lastLinear == 0` means "no fixed end", which is
|
||||
// the keyring: its capacity grows, so the invMgr count is the only
|
||||
// correct bound.
|
||||
int base = 0;
|
||||
int lastLinear = 0;
|
||||
bool inPlayer = true;
|
||||
if (bagID == 0) {
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
base = BACKPACK_LINEAR_OFFSET;
|
||||
lastLinear = Offsets::INVMGR_BACKPACK_LAST_SLOT;
|
||||
} else if (bagID == -1) {
|
||||
base = BANK_MAIN_LINEAR_OFFSET;
|
||||
lastLinear = Offsets::INVMGR_BANK_MAIN_LAST_SLOT;
|
||||
} else if (bagID == -2) {
|
||||
base = KEYRING_LINEAR_OFFSET;
|
||||
} else {
|
||||
inPlayer = false;
|
||||
}
|
||||
if (inPlayer) {
|
||||
const void *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
const uint32_t linear = static_cast<uint32_t>(base + slotInBag);
|
||||
if (lastLinear != 0 && linear > static_cast<uint32_t>(lastLinear))
|
||||
return false; // would alias into the next range
|
||||
if (!LinearSlotInRange(player, linear))
|
||||
return false;
|
||||
if (!ReadGuid(player, containerLo, containerHi))
|
||||
return false;
|
||||
*linearSlot = static_cast<uint32_t>(BACKPACK_LINEAR_OFFSET + slotInBag);
|
||||
*linearSlot = linear;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bagID >= 1 && bagID <= 4) {
|
||||
const uint8_t *bag = Item::Location::ResolveEquipmentSlot(
|
||||
FIRST_BAG_CONTAINER_LINEAR + bagID);
|
||||
@@ -111,17 +155,6 @@ bool EncodeBagSlot(int bagID, int slotInBag,
|
||||
*linearSlot = static_cast<uint32_t>(slotInBag - 1);
|
||||
return true;
|
||||
}
|
||||
if (bagID == -1) {
|
||||
if (slotInBag > BANK_MAIN_NUM_SLOTS)
|
||||
return false;
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
if (!ReadGuid(player, containerLo, containerHi))
|
||||
return false;
|
||||
*linearSlot = static_cast<uint32_t>(BANK_MAIN_LINEAR_OFFSET + slotInBag);
|
||||
return true;
|
||||
}
|
||||
if (bagID >= FIRST_BANK_BAG_ID && bagID <= LAST_BANK_BAG_ID) {
|
||||
const uint8_t *bag = Item::Location::ResolveEquipmentSlot(
|
||||
FIRST_BANK_BAG_EQ_SLOT + (bagID - FIRST_BANK_BAG_ID));
|
||||
@@ -135,6 +168,35 @@ bool EncodeBagSlot(int bagID, int slotInBag,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Container GUID for a bagID, with no slot involved — what the
|
||||
// autostore opcode needs, since it names a destination container and
|
||||
// lets the server choose the slot inside it. Bags 0 and -1 resolve to
|
||||
// the player, which the engine's converter reports as `0xFF`
|
||||
// (INVENTORY_SLOT_BAG_0); the two are indistinguishable on the wire.
|
||||
bool EncodeContainerGuid(int bagID, uint32_t *containerLo, uint32_t *containerHi) {
|
||||
if (bagID == 0 || bagID == -1) {
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
return ReadGuid(player, containerLo, containerHi);
|
||||
}
|
||||
if (bagID >= 1 && bagID <= 4) {
|
||||
const uint8_t *bag = Item::Location::ResolveEquipmentSlot(
|
||||
FIRST_BAG_CONTAINER_LINEAR + bagID);
|
||||
if (bag == nullptr)
|
||||
return false;
|
||||
return ReadGuid(bag, containerLo, containerHi);
|
||||
}
|
||||
if (bagID >= FIRST_BANK_BAG_ID && bagID <= LAST_BANK_BAG_ID) {
|
||||
const uint8_t *bag = Item::Location::ResolveEquipmentSlot(
|
||||
FIRST_BANK_BAG_EQ_SLOT + (bagID - FIRST_BANK_BAG_ID));
|
||||
if (bag == nullptr)
|
||||
return false;
|
||||
return ReadGuid(bag, containerLo, containerHi);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Common send path. All paperdoll-side state (player ptr, player
|
||||
// GUID) is resolved here; caller supplies the source side
|
||||
// (already-encoded linear slot + the container GUID it lives in).
|
||||
@@ -227,6 +289,15 @@ bool ToBag(const void *cgItem, int srcPaperdollSlot, int dstBagID, int dstSlotIn
|
||||
}
|
||||
|
||||
bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int count) {
|
||||
return MoveCountFrom(Item::Location::ResolveBag(L, srcBag, srcSlot),
|
||||
srcBag, srcSlot, dstBag, dstSlot, count);
|
||||
}
|
||||
|
||||
bool MoveCountFrom(const void *srcItem, int srcBag, int srcSlot, int dstBag,
|
||||
int dstSlot, int count) {
|
||||
if (srcItem == nullptr)
|
||||
return false; // empty source slot
|
||||
|
||||
// Vanilla protocol writes count as a single byte. Clamp at 255;
|
||||
// anything larger would either truncate silently (255 sent) or
|
||||
// wrap (0 = no-op). Refuse zero outright too.
|
||||
@@ -242,13 +313,6 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
&dstContainerLo, &dstContainerHi, &dstLinear))
|
||||
return false;
|
||||
|
||||
// Confirm source is occupied — surfacing "empty source" as a clean
|
||||
// local `false` rather than relying on the server reject + log
|
||||
// SMSG_INVENTORY_CHANGE_FAILURE noise.
|
||||
const uint8_t *srcItem = Item::Location::ResolveBag(L, srcBag, srcSlot);
|
||||
if (srcItem == nullptr)
|
||||
return false;
|
||||
|
||||
// Vanilla server rejects `CMSG_SPLIT_ITEM` when count == srcStack
|
||||
// (`EQUIP_ERR_COULDNT_SPLIT_ITEMS`) — conceptually "split N off,
|
||||
// leave residual", and splitting the entire stack would leave
|
||||
@@ -257,14 +321,15 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
// server treats as a merge for same-item destinations (up to
|
||||
// maxStack). Route there for count == srcStack so callers don't
|
||||
// have to special-case "move everything".
|
||||
auto *srcDescriptor = Item::ObjectFields(srcItem);
|
||||
auto *srcDescriptor =
|
||||
Item::ObjectFields(static_cast<const uint8_t *>(srcItem));
|
||||
const int srcStack = srcDescriptor == nullptr ? 0 :
|
||||
static_cast<int>(Game::Read<uint32_t>(
|
||||
srcDescriptor, Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
if (count > srcStack)
|
||||
return false; // server would reject anyway; fail fast and locally
|
||||
if (count == srcStack)
|
||||
return Containers(L, srcBag, srcSlot, dstBag, dstSlot);
|
||||
return ContainersFrom(srcItem, srcBag, srcSlot, dstBag, dstSlot);
|
||||
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
@@ -285,7 +350,99 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AutoStoreFrom(const void *srcItem, int srcBag, int srcSlot, int dstBag) {
|
||||
// The opcode puts no item GUID on the wire — the server locates the
|
||||
// item from (bag, slot) — so the pointer is here purely to prove the
|
||||
// source is occupied, which keeps an empty slot a clean local false
|
||||
// instead of a server reject.
|
||||
if (srcItem == nullptr)
|
||||
return false;
|
||||
|
||||
const bool srcIsBank =
|
||||
srcBag == -1 || (srcBag >= FIRST_BANK_BAG_ID && srcBag <= LAST_BANK_BAG_ID);
|
||||
|
||||
// Which of the two autostore opcodes serves this pair — and whether
|
||||
// either does. The bank opcode takes its direction from the source,
|
||||
// so it can only cross the boundary; see the header note.
|
||||
bool crossToBank = false; // use FUN_INVENTORY_AUTOSTORE_BANK
|
||||
if (srcIsBank) {
|
||||
if (dstBag != 0)
|
||||
return false; // bank→bank, or a bank source aimed at one bag
|
||||
crossToBank = true; // 0x282 sends a bank source to the inventory
|
||||
} else if (dstBag == -1) {
|
||||
crossToBank = true; // 0x282 sends an inventory source to the bank
|
||||
} else if (dstBag < 0 || dstBag > 4) {
|
||||
return false; // individual bank bags are not addressable
|
||||
}
|
||||
|
||||
uint32_t srcContainerLo = 0, srcContainerHi = 0, srcLinear = 0;
|
||||
if (!EncodeBagSlot(srcBag, srcSlot,
|
||||
&srcContainerLo, &srcContainerHi, &srcLinear))
|
||||
return false;
|
||||
|
||||
uint32_t dstContainerLo = 0, dstContainerHi = 0;
|
||||
if (!crossToBank &&
|
||||
!EncodeContainerGuid(dstBag, &dstContainerLo, &dstContainerHi))
|
||||
return false;
|
||||
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
if (crossToBank) {
|
||||
// Three stack args, `RET 0xC`. No destination on the wire — the
|
||||
// server reads the direction off the source position.
|
||||
using AutoStoreBankFn_t = void(__thiscall *)(
|
||||
void *thisPlayer,
|
||||
uint32_t srcContainerLo, uint32_t srcContainerHi,
|
||||
uint32_t srcSlot);
|
||||
auto fnBank = reinterpret_cast<AutoStoreBankFn_t>(
|
||||
Offsets::FUN_INVENTORY_AUTOSTORE_BANK);
|
||||
fnBank(player, srcContainerLo, srcContainerHi, srcLinear);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No item GUID on the wire for this opcode — the server locates the
|
||||
// item from (srcBag, srcLinearSlot). The first two args are the
|
||||
// family's ABI padding, as with the split builder.
|
||||
//
|
||||
// EIGHT stack args, matching the callee's `RET 0x20`. The trailing
|
||||
// one is never read by the body, but declaring seven makes it pop
|
||||
// four bytes we never pushed, and the caller's stack shifts under
|
||||
// it — which shows up much later as a garbage pointer in whoever
|
||||
// runs next, not as a fault here.
|
||||
using AutoStoreFn_t = void(__thiscall *)(
|
||||
void *thisPlayer,
|
||||
uint32_t unused1, uint32_t unused2,
|
||||
uint32_t srcContainerLo, uint32_t srcContainerHi, uint32_t srcSlot,
|
||||
uint32_t dstContainerLo, uint32_t dstContainerHi,
|
||||
uint32_t unused3);
|
||||
auto fn = reinterpret_cast<AutoStoreFn_t>(Offsets::FUN_INVENTORY_AUTOSTORE);
|
||||
fn(player,
|
||||
0, 0,
|
||||
srcContainerLo, srcContainerHi, srcLinear,
|
||||
dstContainerLo, dstContainerHi,
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AutoStore(void *L, int srcBag, int srcSlot, int dstBag) {
|
||||
// Resolving stomps the Lua stack, so callers must have read every
|
||||
// argument they need before getting here.
|
||||
return AutoStoreFrom(Item::Location::ResolveBag(L, srcBag, srcSlot),
|
||||
srcBag, srcSlot, dstBag);
|
||||
}
|
||||
|
||||
bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot) {
|
||||
return ContainersFrom(Item::Location::ResolveBag(L, srcBag, srcSlot),
|
||||
srcBag, srcSlot, dstBag, dstSlot);
|
||||
}
|
||||
|
||||
bool ContainersFrom(const void *srcItem, int srcBag, int srcSlot,
|
||||
int dstBag, int dstSlot) {
|
||||
if (srcItem == nullptr)
|
||||
return false; // empty source slot
|
||||
|
||||
uint32_t srcContainerLo = 0, srcContainerHi = 0, srcLinear = 0;
|
||||
uint32_t dstContainerLo = 0, dstContainerHi = 0, dstLinear = 0;
|
||||
if (!EncodeBagSlot(srcBag, srcSlot,
|
||||
@@ -295,13 +452,6 @@ bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot) {
|
||||
&dstContainerLo, &dstContainerHi, &dstLinear))
|
||||
return false;
|
||||
|
||||
// Look up the source CGItem so we can put its GUID on the wire.
|
||||
// This stomps the Lua stack — caller must have already read every
|
||||
// arg it cares about.
|
||||
const uint8_t *srcItem = Item::Location::ResolveBag(L, srcBag, srcSlot);
|
||||
if (srcItem == nullptr)
|
||||
return false; // empty source slot
|
||||
|
||||
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
@@ -310,12 +460,28 @@ bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot) {
|
||||
if (!ReadGuid(srcItem, &itemLo, &itemHi))
|
||||
return false;
|
||||
|
||||
// flag = 1 SUPPRESSES the engine's pre-send confirmation gate, and
|
||||
// that is load-bearing rather than an optimization. With flag = 0 the
|
||||
// builder inspects the item being moved and, if it is Bind-on-Equip
|
||||
// and this character could equip it, stashes the parameters and
|
||||
// raises the bind-confirmation dialog INSTEAD OF SENDING ANYTHING —
|
||||
// silently, since the builder returns void. It does the same when the
|
||||
// item is not in the item cache yet. Either way the move just does
|
||||
// not happen.
|
||||
//
|
||||
// Suppressing it is correct here because this function addresses bag
|
||||
// CONTENT slots on both sides and never a paperdoll slot, so nothing
|
||||
// it sends can bind an item; the gate exists for the paths that move
|
||||
// an item into equipment, which are `FromBag` / `ToBag` /
|
||||
// `FromPaperdoll` and which pass 0. The engine sets this same flag
|
||||
// itself when it re-issues a swap after the player accepts the
|
||||
// dialog.
|
||||
auto fn = reinterpret_cast<SwapFn_t>(Offsets::FUN_INVENTORY_SWAP);
|
||||
fn(player,
|
||||
itemLo, itemHi,
|
||||
srcContainerLo, srcContainerHi, srcLinear,
|
||||
dstContainerLo, dstContainerHi, dstLinear,
|
||||
0);
|
||||
1);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -97,4 +97,80 @@ bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot);
|
||||
// Lua stack.
|
||||
bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int count);
|
||||
|
||||
// "Put this item away" — the server picks the destination slot
|
||||
// (`CMSG_AUTOSTORE_BAG_ITEM`, opcode 0x10B). Unlike `Containers` and
|
||||
// `MoveCount` there is no destination slot to supply, and that is the
|
||||
// whole value of it: the server first merges the stack into existing
|
||||
// non-full stacks of the same item (splitting it across several if
|
||||
// needed), then places any remainder in a free slot.
|
||||
//
|
||||
// So this is the right primitive for consolidating partial stacks,
|
||||
// because it needs no stack-size lookup on this side. Computing the
|
||||
// merge here would mean sizing every stack first, and the max-stack
|
||||
// getter is nil until that item's data has arrived from the server —
|
||||
// so a cold item cache silently skips exactly the stacks it could not
|
||||
// size. The server never has to look one up.
|
||||
//
|
||||
// `dstBag` says where you want it, and two different engine builders
|
||||
// serve it because the wire has two autostore opcodes:
|
||||
// dstBag 0 → anywhere in the main inventory that it fits
|
||||
// dstBag 1..4 → that equipped bag only
|
||||
// dstBag -1 → anywhere in the bank that it fits
|
||||
//
|
||||
// The backpack is not separately addressable, because on the wire the
|
||||
// backpack IS the player container — so 0 means the whole inventory,
|
||||
// not "the backpack". Individual bank bags (5..10) are likewise not
|
||||
// addressable: the bank opcode carries no destination at all.
|
||||
//
|
||||
// WHAT IS NOT EXPRESSIBLE: moving an item to a fresh slot on the side
|
||||
// it is already on, when that side is the bank. The bank opcode derives
|
||||
// its direction from the source, so a bank-sourced item always travels
|
||||
// to the inventory. Consolidating stacks WITHIN the bank therefore
|
||||
// cannot use this call — use `MoveCount` per pair instead. The
|
||||
// combinations this accepts are exactly:
|
||||
// inventory source, dstBag 0..4 → stays in the inventory
|
||||
// inventory source, dstBag -1 → goes to the bank
|
||||
// bank source, dstBag 0 → comes back to the inventory
|
||||
// Anything else returns false rather than sending a packet that would
|
||||
// do something other than what was asked.
|
||||
//
|
||||
// Returns false on bad args, an unequipped bag, a rejected
|
||||
// source/destination pair, or an empty source slot. Everything else is
|
||||
// server-side; failures come back as `SMSG_INVENTORY_CHANGE_FAILURE`
|
||||
// exactly as for the other two. Stomps the Lua stack for the same
|
||||
// reason `Containers` does.
|
||||
bool AutoStore(void *L, int srcBag, int srcSlot, int dstBag);
|
||||
|
||||
// Lua-free forms of `Containers` and `AutoStore`, for callers that
|
||||
// already hold the source `CGItem *` and therefore need no lookup.
|
||||
//
|
||||
// The Lua-taking forms exist only to resolve that pointer, and they do
|
||||
// it through `Item::Location::ResolveBag` → the engine's `PackBagSlot`,
|
||||
// which reads its arguments off the Lua stack and stomps it. That is
|
||||
// fine inside a Lua call, where the stack is ours. It is NOT fine from
|
||||
// a tick or bag-update callback, where the engine may have its own
|
||||
// values there — so anything running outside a Lua call must use these.
|
||||
//
|
||||
// The caller owns the pointer's validity. It stays valid across a batch
|
||||
// of swaps: a server-side swap moves item GUIDs between slots and does
|
||||
// not destroy the objects, so pointers snapshotted before the batch are
|
||||
// still good during it.
|
||||
bool ContainersFrom(const void *srcItem, int srcBag, int srcSlot,
|
||||
int dstBag, int dstSlot);
|
||||
|
||||
bool AutoStoreFrom(const void *srcItem, int srcBag, int srcSlot, int dstBag);
|
||||
|
||||
// CAUTION when batching. This reads the source's LIVE stack count to
|
||||
// decide whether `count` is the whole stack (which must go as a swap,
|
||||
// since the server rejects a split that would empty its source) or a
|
||||
// partial one. A caller firing several moves in one frame gets no server
|
||||
// replies in between, so those counts are still the pre-batch values
|
||||
// while the caller's own model has moved on — and a move the caller
|
||||
// knows is "the last 3" is sent as a split of 3 from a stack the server
|
||||
// says holds 3, which it refuses with "Couldn't split those items".
|
||||
// Such a caller should decide from its own model and call
|
||||
// `ContainersFrom` directly for whole-stack moves.
|
||||
bool MoveCountFrom(const void *srcItem, int srcBag, int srcSlot, int dstBag,
|
||||
int dstSlot, int count);
|
||||
|
||||
} // namespace Item::Swap
|
||||
|
||||
@@ -188,7 +188,7 @@ static int __fastcall Script_GameTooltipGetItem(void *L) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:GetItem()");
|
||||
return 0;
|
||||
}
|
||||
void *tooltipObj = Game::Lua::ResolveObject(L, 1);
|
||||
void *tooltipObj = Game::Lua::ResolveTooltip(L);
|
||||
if (tooltipObj == nullptr)
|
||||
return 0;
|
||||
auto *base = static_cast<const uint8_t *>(tooltipObj);
|
||||
@@ -274,7 +274,7 @@ static int __fastcall Script_GameTooltipHasItem(void *L) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:HasItem()");
|
||||
return 0;
|
||||
}
|
||||
void *tooltipObj = Game::Lua::ResolveObject(L, 1);
|
||||
void *tooltipObj = Game::Lua::ResolveTooltip(L);
|
||||
if (tooltipObj == nullptr) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
|
||||
+24
-17
@@ -11,12 +11,16 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.UseItemByName(itemInfo [, unit])` — finds the first item in
|
||||
// the player's bags matching `itemInfo` (itemID, link, or name) and
|
||||
// uses it via the engine's `CGItem::UseItem` primitive. Optional
|
||||
// `unit` is a unit token (`"player"`, `"target"`, `"focus"`, …)
|
||||
// passed to spell-cast items (scrolls, traps, on-use targeted
|
||||
// effects) as the cast target.
|
||||
// `C_Item.UseItemByName(item [, unit])` — uses `item` via the engine's
|
||||
// `CGItem::UseItem` primitive. Optional `unit` is a unit token (`"player"`,
|
||||
// `"target"`, `"focus"`, …) passed to spell-cast items (scrolls, traps,
|
||||
// on-use targeted effects) as the cast target.
|
||||
//
|
||||
// `item` is an item reference (itemID, `item:N`, a link, or the name of an
|
||||
// item the player carries — the first bag match is used) or an item location
|
||||
// (`{bagID=B, slotIndex=S}`, `{equipmentSlotIndex=N}`, an item GUID), which
|
||||
// names one exact item. See `Item::Location::ResolveItemArgOrLocation`, which
|
||||
// `C_Item.UseAtCursor` and `C_Item.UseAtUnit` share.
|
||||
//
|
||||
// Same structure 3.3.5's `Script_UseItemByName` uses: locate the item
|
||||
// directly, then hand it to the engine's by-item-pointer use call.
|
||||
@@ -37,15 +41,15 @@
|
||||
// stray target is harmless. We default to a zero GUID when the caller
|
||||
// omits `unit` — matches existing call sites.
|
||||
//
|
||||
// Returns nothing. Silently no-ops on bad input, item not found in
|
||||
// bags, item locked, on cooldown, etc. An unrecognized `unit` string
|
||||
// Returns nothing. Silently no-ops on bad input, an item the player doesn't
|
||||
// have, item locked, on cooldown, etc. An unrecognized `unit` string
|
||||
// is treated as "no target" rather than raising, matching arg-1's
|
||||
// "silently no-ops on bad input" contract.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "unit/TokenResolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -62,25 +66,28 @@ uint64_t ResolveUnitGuid(void *L, int idx) {
|
||||
if (!Game::Lua::IsString(L, idx))
|
||||
return 0;
|
||||
const char *token = Game::Lua::ToString(L, idx);
|
||||
// The engine's resolver RAISES for a string that names no token, which
|
||||
// would turn a bad `unit` into an error out of a function whose contract
|
||||
// is to no-op on bad input — and this is reached from secure-button
|
||||
// attributes and key bindings, where the string is whatever an addon
|
||||
// configured. Probe first so an unusable one is simply no target.
|
||||
if (!Unit::TokenResolve::IsUnitToken(token))
|
||||
return 0;
|
||||
auto fn = reinterpret_cast<TokenToGUID_t>(Offsets::FUN_TOKEN_TO_GUID);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Item_UseItemByName(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read the unit first: the item resolve below stomps the Lua stack.
|
||||
const uint64_t targetGuid = ResolveUnitGuid(L, 2);
|
||||
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
const uint8_t *item = Item::Location::ResolveItemArgOrLocation(L, 1);
|
||||
if (item == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto useItem = reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE);
|
||||
useItem(found.item, &targetGuid, 0);
|
||||
useItem(item, &targetGuid, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -11,13 +11,19 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.UseAtCursor(itemInfo)` — use the named item at the player's
|
||||
// `C_Item.UseAtCursor(item)` — use the given item at the player's
|
||||
// current cursor world position. For items whose on-use spell is
|
||||
// ground-target (Iron Grenade, demolition charges, etc.), bypasses
|
||||
// the manual click on the AoE reticle that the engine would
|
||||
// otherwise require.
|
||||
//
|
||||
// Mirrors `C_Item.UseItemByName`'s bag-walk + `FUN_ITEM_USE` dispatch
|
||||
// `item` is an item location (`{bagID=B, slotIndex=S}`,
|
||||
// `{equipmentSlotIndex=N}`, an item GUID) or an item reference (itemID,
|
||||
// `item:N`, a link, the name of an item you carry) — see
|
||||
// `Item::Location::ResolveItemArgOrLocation`. A location names one exact
|
||||
// item, which is what a caller holding two stacks of the same thing needs.
|
||||
//
|
||||
// Mirrors `C_Item.UseItemByName`'s resolve + `FUN_ITEM_USE` dispatch
|
||||
// — the engine's item-use path eventually hands the on-use spell to
|
||||
// the same cast dispatcher that ground-target spells go through, so
|
||||
// placement-mode activation works identically. After firing the
|
||||
@@ -25,11 +31,11 @@
|
||||
//
|
||||
// Returns `true` when the cursor-placement leg landed; `false` for
|
||||
// items that aren't ground-target (the item still fires, just with
|
||||
// no implicit target), unparseable input, item-not-in-bags, etc.
|
||||
// no implicit target), unparseable input, an item the player doesn't
|
||||
// have, etc.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "spell/AtCursor.h"
|
||||
|
||||
@@ -43,21 +49,15 @@ using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targe
|
||||
int flag);
|
||||
|
||||
int __fastcall Script_C_Item_UseAtCursor(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
const uint8_t *item = Item::Location::ResolveItemArgOrLocation(L, 1);
|
||||
if (item == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint64_t zeroTarget = 0;
|
||||
auto useItem = reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE);
|
||||
useItem(found.item, &zeroTarget, 0);
|
||||
useItem(item, &zeroTarget, 0);
|
||||
|
||||
const bool placed = Spell::AtCursor::Resolve();
|
||||
Game::Lua::PushBool(L, placed);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user