mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb9579c398 | ||
|
|
5642644833 | ||
|
|
58e7e1a2c6 | ||
|
|
5339c97b1e | ||
|
|
7406ac5db1 | ||
|
|
1c263152e3 | ||
|
|
ab211962e5 | ||
|
|
33a23c563c |
@@ -11,12 +11,51 @@
|
||||
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
|
||||
-- the reader can craft themselves.
|
||||
|
||||
-- Frame geometry mirrors the real TradeSkillFrame (Blizzard_TradeSkillUI):
|
||||
-- 768x512, TW-TradeSkill quadrant art, 19 rows of 16px, list at (37,-98).
|
||||
local FRAME_W, FRAME_H = 768, 512;
|
||||
local NUM_ROWS = 19;
|
||||
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
|
||||
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
|
||||
-- vanilla 384x512 single-column art, so on that client we fall back to the
|
||||
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
|
||||
-- than stretch the wide art. LO holds every layout value that differs.
|
||||
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
|
||||
local FRAME_W = TURTLE and 768 or 384;
|
||||
local FRAME_H = 512;
|
||||
local NUM_ROWS = TURTLE and 19 or 8;
|
||||
local ROW_HEIGHT = 16;
|
||||
|
||||
local LO = TURTLE and {
|
||||
hit = { 2, 82, 2, 73 },
|
||||
quads = {
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -77, -8 },
|
||||
bar = { 596, 15, 76, -44 },
|
||||
barBdr = { 604, 22 },
|
||||
summary = { "TOPRIGHT", -110, -70 },
|
||||
drop = { 40, -60 },
|
||||
scroll = { 296, 330, 37, -98 },
|
||||
detail = { 379, -113 },
|
||||
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
|
||||
} or {
|
||||
hit = { 0, 34, 0, 75 },
|
||||
quads = {
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -29, -8 },
|
||||
bar = { 268, 15, 73, -37 },
|
||||
barBdr = { 278, 22 },
|
||||
summary = { "TOPRIGHT", -40, -58 },
|
||||
drop = { 12, -62 },
|
||||
scroll = { 296, 130, 21, -96 },
|
||||
detail = { 28, -237 },
|
||||
exit = { "CENTER", "TOPLEFT", 305, -422 },
|
||||
};
|
||||
|
||||
local PROFESSION_ICONS = {
|
||||
[171] = "Trade_Alchemy",
|
||||
[164] = "Trade_BlackSmithing",
|
||||
@@ -32,7 +71,7 @@ local PROFESSION_ICONS = {
|
||||
[356] = "Trade_Fishing",
|
||||
};
|
||||
|
||||
if TURTLE_WOW_VERSION then
|
||||
if TURTLE then
|
||||
PROFESSION_ICONS[142] = "Trade_Survival"
|
||||
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
|
||||
end
|
||||
@@ -188,45 +227,43 @@ local function CreateFrame_TradeSkillLink()
|
||||
f:SetPoint("CENTER", 0, 0);
|
||||
f:SetMovable(true);
|
||||
f:EnableMouse(true);
|
||||
f:SetHitRectInsets(2, 82, 2, 73);
|
||||
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
|
||||
f:RegisterForDrag("LeftButton");
|
||||
f:SetScript("OnDragStart", function() this:StartMoving(); end);
|
||||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
|
||||
f:SetFrameStrata("DIALOG");
|
||||
f:Hide();
|
||||
tinsert(UISpecialFrames, f:GetName())
|
||||
|
||||
local portrait = f:CreateTexture(nil, "BACKGROUND");
|
||||
portrait:SetSize(60, 60);
|
||||
portrait:SetPoint("TOPLEFT", 7, -6);
|
||||
f.portrait = portrait;
|
||||
|
||||
local function Quad(file, w, h, point)
|
||||
for i = 1, table.getn(LO.quads) do
|
||||
local q = LO.quads[i];
|
||||
local tx = f:CreateTexture(nil, "BORDER");
|
||||
tx:SetTexture("Interface\\TradeSkillFrame\\" .. file);
|
||||
tx:SetSize(w, h);
|
||||
tx:SetPoint(point);
|
||||
tx:SetTexture(q[1]);
|
||||
tx:SetSize(q[2], q[3]);
|
||||
tx:SetPoint(q[4]);
|
||||
end
|
||||
Quad("TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT");
|
||||
Quad("TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT");
|
||||
Quad("TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT");
|
||||
Quad("TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT");
|
||||
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
|
||||
title:SetPoint("TOP", 0, -18);
|
||||
f.title = title;
|
||||
|
||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
|
||||
close:SetPoint("TOPRIGHT", -77, -8);
|
||||
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
|
||||
|
||||
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
|
||||
exit:SetSize(80, 22);
|
||||
exit:SetPoint("CENTER", f, "BOTTOMRIGHT", -128, 92);
|
||||
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
|
||||
exit:SetText(EXIT or "Exit");
|
||||
exit:SetScript("OnClick", function() f:Hide(); end);
|
||||
|
||||
local bar = CreateFrame("StatusBar", nil, f);
|
||||
bar:SetSize(596, 15);
|
||||
bar:SetPoint("TOPLEFT", 76, -44);
|
||||
bar:SetSize(LO.bar[1], LO.bar[2]);
|
||||
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
|
||||
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
|
||||
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
|
||||
bar:SetMinMaxValues(0, 1);
|
||||
@@ -236,7 +273,7 @@ local function CreateFrame_TradeSkillLink()
|
||||
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
|
||||
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
|
||||
local border = CreateFrame("Frame", nil, bar);
|
||||
border:SetSize(604, 22);
|
||||
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
|
||||
border:SetPoint("LEFT", -5, 1);
|
||||
border:SetBackdrop({
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
|
||||
@@ -253,19 +290,19 @@ local function CreateFrame_TradeSkillLink()
|
||||
f.barRank = barRank;
|
||||
|
||||
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
summary:SetPoint("TOPRIGHT", -110, -70);
|
||||
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
|
||||
f.summary = summary;
|
||||
|
||||
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
|
||||
"UIDropDownMenuTemplate");
|
||||
dropdown:SetPoint("TOPLEFT", 40, -60);
|
||||
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
|
||||
f.dropdown = dropdown;
|
||||
f.subclassFilter = nil; -- nil = all subclasses
|
||||
|
||||
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
|
||||
f, "FauxScrollFrameTemplate");
|
||||
scroll:SetSize(296, 330);
|
||||
scroll:SetPoint("TOPLEFT", 37, -98);
|
||||
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
|
||||
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
|
||||
scroll:SetScript("OnVerticalScroll", function()
|
||||
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
|
||||
end)
|
||||
@@ -313,7 +350,7 @@ local function CreateFrame_TradeSkillLink()
|
||||
|
||||
local prodIcon = CreateFrame("Button", nil, f);
|
||||
prodIcon:SetSize(37, 37); -- TradeSkillSkillIcon is 37x37
|
||||
prodIcon:SetPoint("TOPLEFT", 379, -113);
|
||||
prodIcon:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
|
||||
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local hdrLeft = prodIcon:CreateTexture(nil, "BACKGROUND");
|
||||
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
|
||||
@@ -620,3 +657,43 @@ function SetItemRef(link, text, button, chatFrame)
|
||||
end
|
||||
return originalSetItemRef(link, text, button, chatFrame);
|
||||
end
|
||||
|
||||
local function CreateProfessionLinkButton(parentName, getLink)
|
||||
local parent = _G[parentName];
|
||||
local btnName = parentName .. "LinkButton";
|
||||
if _G[btnName] or not parent then
|
||||
return;
|
||||
end
|
||||
local btn = CreateFrame("Button", btnName, parent);
|
||||
btn:SetSize(16, 16);
|
||||
btn:SetPoint("LEFT", _G[parentName .. "TitleText"], "RIGHT", 4, 0);
|
||||
|
||||
local icon = "Interface\\GossipFrame\\PetitionGossipIcon";
|
||||
btn:SetNormalTexture(icon);
|
||||
btn:SetHighlightTexture(icon, "ADD");
|
||||
|
||||
btn:SetScript("OnClick", function()
|
||||
local link = getLink();
|
||||
if not link then
|
||||
return;
|
||||
end
|
||||
if not ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Show();
|
||||
end
|
||||
ChatFrameEditBox:Insert(link);
|
||||
end);
|
||||
btn:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT");
|
||||
GameTooltip:SetText(LINK_TRADESKILL_TOOLTIP, nil, nil, nil, nil, 1);
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
end
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
|
||||
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
|
||||
end);
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
|
||||
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink);
|
||||
end);
|
||||
@@ -24,3 +24,4 @@ SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
@@ -33,7 +33,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
|
||||
+17
@@ -137,6 +137,7 @@ build instructions.
|
||||
- [`frame:SetShown(shown)`](#framesetshownshown)
|
||||
- [`frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`](#framesetresizeboundsminwidth-minheight--maxwidth-maxheight)
|
||||
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
|
||||
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
|
||||
|
||||
- [FriendList](#friendlist)
|
||||
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
|
||||
@@ -3143,6 +3144,22 @@ button:HookScript("OnEnter", function(self)
|
||||
end)
|
||||
```
|
||||
|
||||
### `frame:IsEventRegistered(event)`
|
||||
|
||||
Vanilla ships `RegisterEvent` / `UnregisterEvent` but not the query.
|
||||
Returns `true` if the frame is currently registered for `event`,
|
||||
`false` otherwise (including for an unknown event name). Reuses the
|
||||
engine's own subscriber-chain membership check — the same walk
|
||||
`RegisterEvent` performs before appending — so the answer is exactly
|
||||
what the event dispatcher sees. Works on any frame.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_LOGIN")
|
||||
f:IsEventRegistered("PLAYER_LOGIN") -- true
|
||||
f:IsEventRegistered("PLAYER_LOGOUT") -- false
|
||||
```
|
||||
|
||||
## FriendList
|
||||
|
||||
### `C_FriendList.SendWhoQueryByName(name)`
|
||||
|
||||
+30
-17
@@ -2279,7 +2279,18 @@ enum Offsets {
|
||||
// Verified via the `IsAssistingRitual` development session by
|
||||
// observing `0xEE00 → 0xEE05` on `/sit` over a chair (the medium-
|
||||
// chair sit value 5).
|
||||
//
|
||||
// Byte 3 (the AnimTier / misc-flags byte, mask 0x02000000 on the
|
||||
// u32) is the **stealth / sneak flag**: set while the unit is in
|
||||
// the sneaking anim tier (Rogue Stealth, Druid Prowl, Shadowmeld),
|
||||
// which is what drives the client's semi-transparent render of a
|
||||
// stealthed unit. Verified via `_classicapi_DiffAll`: a Prowl
|
||||
// toggle from a Cat-Form baseline flips byte 3 `0x00 → 0x02` while
|
||||
// every other descriptor change is aura-array slot metadata — so
|
||||
// this is the only standalone broadcast bit that means "stealthed".
|
||||
// Read by `IsStealthed()`.
|
||||
OFF_UNIT_FIELD_BYTES_1 = 0x210,
|
||||
UNIT_BYTES_1_STEALTH_FLAG = 0x02000000,
|
||||
|
||||
// `__fastcall(void)` engine helper that refreshes the bonus
|
||||
// action bar from the local player's current shapeshift form
|
||||
@@ -3452,6 +3463,15 @@ enum Offsets {
|
||||
OFF_EVENT_ENTRY_NAME = 0x00,
|
||||
OFF_EVENT_ENTRY_HEAD = 0x0C,
|
||||
|
||||
// Subscriber-chain node layout — the linked list hanging off each
|
||||
// entry's +0x0C head, derived from Frame::RegisterEvent
|
||||
// (FUN_00702140): each node is `{ ?, next@+0x04, frame@+0x08 }`, where
|
||||
// `frame` is the CFrameScriptObject* subscriber. A node value with its
|
||||
// low bit set (or null) is the end/empty sentinel. Walked by
|
||||
// Frame:IsEventRegistered to test membership.
|
||||
OFF_EVENT_NODE_NEXT = 0x04,
|
||||
OFF_EVENT_NODE_FRAME = 0x08,
|
||||
|
||||
// `Frame::RegisterEvent` — the C++ helper called by the Lua
|
||||
// `frame:RegisterEvent(eventName)` method (`Script_RegisterEvent` at
|
||||
// `0x00774A40`). `__thiscall` with `(this=frame, eventName)`. Walks
|
||||
@@ -3880,23 +3900,15 @@ enum Offsets {
|
||||
VAR_ADDON_ARRAY = 0x00BE1B94,
|
||||
VAR_ADDON_COUNT = 0x00BE1B90,
|
||||
|
||||
// Player visibility/state byte field, broadcast in UpdateFields.
|
||||
// Found empirically via _classicapi_DescDump (see src/debug/).
|
||||
// Treated as a u32 but the meaningful bits all live in byte 0:
|
||||
// 0x02 — set when the player has any visibility/form-modifying
|
||||
// state active (stealth, mount, possibly shapeshift —
|
||||
// set by stealth and mount in our test data).
|
||||
// 0x08, 0x10 — set together when mounted (alongside 0x02).
|
||||
//
|
||||
// Ambiguity: bit 0x02 alone doesn't distinguish stealth from
|
||||
// other visibility-modifying states. Stealth detection AND-gates
|
||||
// with `mountDisplayID == 0` to exclude mount; if shapeshift
|
||||
// also sets bit 0x02 we'd false-positive on druids in form
|
||||
// (untested). The robust path is to walk the player's aura
|
||||
// list looking for the actual stealth/prowl spell — left as
|
||||
// future work if false-positives turn up.
|
||||
OFF_PLAYER_FIELD_VIS_BYTES = 0x17C,
|
||||
PLAYER_VIS_BIT_STEALTH = 0x02,
|
||||
// NOTE: an earlier revision defined OFF_PLAYER_FIELD_VIS_BYTES /
|
||||
// PLAYER_VIS_BIT_STEALTH here at 0x17C, believing bit 0x02 was a
|
||||
// "stealth bit". That was wrong: 0x17C is UNIT_FIELD_AURALEVELS
|
||||
// (per-slot caster-level bytes; see OFF_UNIT_FIELD_AURALEVELS above),
|
||||
// and the old read only appeared to work by coincidence (a caster-
|
||||
// level byte that happened to have bit 0x02 set on the test char;
|
||||
// at level 60 = 0x3C it doesn't, so it read false). The real stealth
|
||||
// flag is UNIT_FIELD_BYTES_1 byte 3 (UNIT_BYTES_1_STEALTH_FLAG); see
|
||||
// that field above. IsStealthed reads it.
|
||||
|
||||
// Mount display ID — the creature display ID the engine renders
|
||||
// under the mounted player. Zero when not mounted, non-zero
|
||||
@@ -4650,6 +4662,7 @@ enum Offsets {
|
||||
// reads `spellRec[+0x14]` as the spell's mechanic when the per-
|
||||
// effect EffectMiscValue is 0. Read by C_Spell.GetSpellMechanicByID.
|
||||
OFF_SPELL_RECORD_MECHANIC = 0x14,
|
||||
SPELL_AURA_MOD_STEALTH = 16,
|
||||
SPELL_AURA_MOD_SHAPESHIFT = 36,
|
||||
SPELL_AURA_MOUNTED = 78,
|
||||
|
||||
|
||||
@@ -150,10 +150,40 @@ void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wipe the whole cache. Used on a map transition (see OnWorldTick).
|
||||
void FlushAll() {
|
||||
for (auto &e : g_cache)
|
||||
e.used = false;
|
||||
}
|
||||
|
||||
// -1 = no map seen yet (never a valid Map.dbc id), so the first tick just
|
||||
// records the current map without flushing.
|
||||
int g_lastMapId = -1;
|
||||
|
||||
// Drop entries whose timed aura has elapsed so the table doesn't fill with
|
||||
// stale combat debuffs. Infinite-duration entries (expirationMs == 0) stay
|
||||
// until overwritten.
|
||||
//
|
||||
// Also flush the entire cache on a map-ID change. A battleground/instance/
|
||||
// continent transition (including the teleport out of a BG via /afk) bulk-
|
||||
// clears the player's aura descriptor WITHOUT firing per-aura OnAuraRemoved,
|
||||
// so entries for auras that are now gone would linger and the
|
||||
// GetAuraDataByIndex fallback would keep surfacing them — the "buffs/debuffs
|
||||
// stuck after leaving a battleground" report. Every other unit from the old
|
||||
// map has despawned too, and the local player's real auras re-sync into the
|
||||
// descriptor / PLAYER_BUFF tables on entering the new world, so a full flush
|
||||
// loses nothing legitimate. Crucially, the descriptor drops the fallback
|
||||
// must survive — rogue stealth and party-range fluctuation — do NOT change
|
||||
// the map ID, so they're unaffected.
|
||||
void OnWorldTick() {
|
||||
const int mapId = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
|
||||
if (mapId != g_lastMapId) {
|
||||
if (g_lastMapId != -1)
|
||||
FlushAll();
|
||||
g_lastMapId = mapId;
|
||||
}
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.expirationMs != 0 && now >= e.expirationMs)
|
||||
|
||||
@@ -186,6 +186,65 @@ 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 = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_COUNT));
|
||||
auto *base = *reinterpret_cast<const uint8_t *const *>(
|
||||
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 = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
if (sstrcmpi(name, eventName, 0x7FFFFFFF) != 0)
|
||||
continue;
|
||||
// Names are unique, so this is the one entry — walk its chain.
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
while ((node & 1) == 0 && node != 0) {
|
||||
if (*reinterpret_cast<void *const *>(
|
||||
node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
|
||||
return true;
|
||||
node = *reinterpret_cast<const 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
|
||||
@@ -222,6 +281,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
Offsets::FUN_SCRIPT_FRAME_HIDE>},
|
||||
{"SetResizeBounds", &Script_SetResizeBounds},
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"IsEventRegistered", &Script_IsEventRegistered},
|
||||
};
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_textureMethods[] = {
|
||||
|
||||
+76
-17
@@ -62,6 +62,19 @@
|
||||
// BLP1 and BLP2 keep width/height at header offsets +0x0C/+0x10.
|
||||
// Resolution runs once per overlay per session (cached).
|
||||
//
|
||||
// ## HD map patches (uniform tile upscale)
|
||||
//
|
||||
// A custom `patch-*.mpq` can replace the overlay art with higher-res tiles
|
||||
// (every 256px tile re-exported at 256*scale). Because we read the *file*
|
||||
// dimensions, that upscale must not be assumed to be 256: the resolver
|
||||
// detects the full-cell size `S` (the largest tile dimension = 256*scale)
|
||||
// and expresses both the partial-tile threshold and the texcoords relative
|
||||
// to it. Draw sizes/positions remain in logical map pixels (the patch changes
|
||||
// texture resolution, not the map canvas), so an interior tile reads texcoord
|
||||
// 1.0 at any scale and stock data (S == 256) is byte-identical to before.
|
||||
// The upscale factor need not be integral; only a uniform per-overlay scale
|
||||
// is assumed.
|
||||
//
|
||||
// ## Return shape
|
||||
//
|
||||
// Array of overlay tables:
|
||||
@@ -102,6 +115,17 @@ using SMemFree_t = void(__stdcall *)(void *ptr, const char *file, int line,
|
||||
|
||||
constexpr int kMaxTiles = 64;
|
||||
|
||||
// Smallest power of two >= v (min 16), matching the engine's own
|
||||
// WorldMapFrame tile-file-size loop. A tile's content fills content/NextPot
|
||||
// of its (power-of-two) file — a fraction that's identical whether the file
|
||||
// is stock or HD-upscaled, so texcoords derived from it are scale-invariant.
|
||||
int NextPot(int v) {
|
||||
int p = 16;
|
||||
while (p < v)
|
||||
p <<= 1;
|
||||
return p;
|
||||
}
|
||||
|
||||
// Resolved tile grid for one overlay, cached for the session (DBC and
|
||||
// MPQ contents are immutable per process).
|
||||
struct ResolvedOverlay {
|
||||
@@ -109,6 +133,7 @@ struct ResolvedOverlay {
|
||||
uint8_t rows;
|
||||
uint8_t count; // tiles to draw (foreign tail already dropped)
|
||||
bool upscaled;
|
||||
uint16_t cell; // full-cell file size (256 on stock, 256*scale on an HD patch)
|
||||
uint16_t fileW[kMaxTiles]; // BLP file dimensions per tile
|
||||
uint16_t fileH[kMaxTiles];
|
||||
};
|
||||
@@ -161,13 +186,29 @@ void ResolveTiles(ResolvedOverlay *out, const char *basePath, int dbcW,
|
||||
if (dbcRows < 1)
|
||||
dbcRows = 1;
|
||||
|
||||
// Full-cell file size. On stock data a full (interior) tile is 256px; an
|
||||
// HD map patch (patch-*.mpq) upscales every tile uniformly, so a full cell
|
||||
// is 256*scale. Full tiles are the largest; edges/slivers are smaller — so
|
||||
// the max tile dimension is the full-cell size. Partial-tile detection
|
||||
// (and the texcoords in PushTiles) must compare against this S, not a
|
||||
// hardcoded 256, or an HD edge tile (e.g. a 200px edge upscaled to 400px)
|
||||
// reads as "full" and shears the row-major grid into diagonal strips.
|
||||
int S = 256;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (out->fileW[i] > S)
|
||||
S = out->fileW[i];
|
||||
if (out->fileH[i] > S)
|
||||
S = out->fileH[i];
|
||||
}
|
||||
out->cell = static_cast<uint16_t>(S);
|
||||
|
||||
// Real column count: the first partial-width tile is a row's right
|
||||
// edge. Search one past dbcCols so a sliver column the DBC width
|
||||
// rounds off is still found.
|
||||
int cols = dbcCols;
|
||||
bool widthEdge = false;
|
||||
for (int i = 0; i < n && i <= dbcCols; ++i) {
|
||||
if (out->fileW[i] < 256) {
|
||||
if (out->fileW[i] < S) {
|
||||
cols = i + 1;
|
||||
widthEdge = true;
|
||||
break;
|
||||
@@ -179,7 +220,7 @@ void ResolveTiles(ResolvedOverlay *out, const char *basePath, int dbcW,
|
||||
int rows = dbcRows;
|
||||
bool heightEdge = false;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (out->fileH[i] < 256) {
|
||||
if (out->fileH[i] < S) {
|
||||
rows = i / cols + 1;
|
||||
heightEdge = true;
|
||||
break;
|
||||
@@ -304,27 +345,45 @@ void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
|
||||
posX = offX + col * drawW;
|
||||
posY = offY + row * drawH;
|
||||
} else {
|
||||
// Canonical 256px grid. Edge tiles draw their content pixels:
|
||||
// from the DBC remainder when it's honest, else (sliver
|
||||
// column the DBC rounded away) the whole file.
|
||||
// Canonical 256px logical grid. Draw sizes/positions stay in
|
||||
// logical map pixels; the texcoord is the content fraction of the
|
||||
// (possibly HD-upscaled) file. `scale` = file px per logical px
|
||||
// (1 on stock, 2/4 on an HD patch), so a full interior tile reads
|
||||
// texCoord 1.0 regardless of file size, and an edge's content
|
||||
// (remainder * scale) is measured against its actual file width.
|
||||
const int fileW = r->fileW[i];
|
||||
const int fileH = r->fileH[i];
|
||||
int pixW = 256;
|
||||
int pixH = 256;
|
||||
const double scale = (r->cell > 0) ? (r->cell / 256.0) : 1.0;
|
||||
|
||||
double logicalW = 256.0, logicalH = 256.0;
|
||||
tcX = 1.0;
|
||||
tcY = 1.0;
|
||||
if (col == r->cols - 1) {
|
||||
pixW = dbcW - 256 * (r->cols - 1);
|
||||
if (pixW <= 0 || pixW > fileW)
|
||||
pixW = fileW;
|
||||
const int rem = dbcW - 256 * (r->cols - 1);
|
||||
if (rem <= 0) {
|
||||
// Sliver the DBC width rounded away: whole file is content
|
||||
// (its logical width is the file width de-scaled).
|
||||
logicalW = fileW / scale;
|
||||
} else {
|
||||
// Content fills rem/NextPot(rem) of the POT tile file.
|
||||
// Scale-invariant, so an HD-upscaled file (POT*scale)
|
||||
// reads the same fraction — using the raw file width here
|
||||
// would halve the texcoord on any tile the patch doubled.
|
||||
logicalW = rem;
|
||||
tcX = rem / static_cast<double>(NextPot(rem));
|
||||
}
|
||||
}
|
||||
if (row == r->rows - 1) {
|
||||
pixH = dbcH - 256 * (r->rows - 1);
|
||||
if (pixH <= 0 || pixH > fileH)
|
||||
pixH = fileH;
|
||||
const int rem = dbcH - 256 * (r->rows - 1);
|
||||
if (rem <= 0) {
|
||||
logicalH = fileH / scale;
|
||||
} else {
|
||||
logicalH = rem;
|
||||
tcY = rem / static_cast<double>(NextPot(rem));
|
||||
}
|
||||
}
|
||||
drawW = pixW;
|
||||
drawH = pixH;
|
||||
tcX = static_cast<double>(pixW) / fileW;
|
||||
tcY = static_cast<double>(pixH) / fileH;
|
||||
drawW = logicalW;
|
||||
drawH = logicalH;
|
||||
posX = offX + col * 256;
|
||||
posY = offY + row * 256;
|
||||
}
|
||||
|
||||
+24
-4
@@ -133,6 +133,26 @@ void StampChannel(int spellID, int startMs, int endMs) {
|
||||
|
||||
int NowMs() { return static_cast<int>(reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)()); }
|
||||
|
||||
// Pushes an engine-ms timestamp to Lua as an UNSIGNED 32-bit value.
|
||||
//
|
||||
// `FUN_OS_TICKCOUNT_MS` returns a 64-bit ms tick from one of two backends
|
||||
// (GetTickCount, or a scaled rdtsc — the latter survives warm reboots, so
|
||||
// the value can be well past 2^31 even right after a "restart"). We keep
|
||||
// ticks in a signed int internally, which is fine for the small deltas and
|
||||
// window compares (they cancel), but a direct int->double push surfaces a
|
||||
// NEGATIVE startTimeMs/endTimeMs once the tick's high bit is set.
|
||||
//
|
||||
// The contract is that these match `GetTime()*1000`. Script_GetTime
|
||||
// (0x00515EA0) does `(double)(tick & 0xFFFFFFFF) * 0.001` — i.e. it masks
|
||||
// the tick to the low 32 bits UNSIGNED, then scales. So reinterpreting our
|
||||
// stored tick as uint32 reproduces GetTime()'s exact value for the whole
|
||||
// range, including across the ~49.7-day 2^32 wrap (both wrap together).
|
||||
// Carrying 64-bit precision here would be WRONG: it would diverge from
|
||||
// GetTime() past 2^32 and break addons' (now-start)/(end-start) math.
|
||||
void PushMs(void *L, int ms) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(static_cast<uint32_t>(ms)));
|
||||
}
|
||||
|
||||
void *Resolve(const char *token) {
|
||||
return reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN)(token);
|
||||
}
|
||||
@@ -200,8 +220,8 @@ int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
Game::Lua::PushString(L, name); // 1 name
|
||||
Game::Lua::PushString(L, name); // 2 displayName
|
||||
Game::Lua::PushString(L, SpellIconPath(rec)); // 3 textureID (path)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.startMs)); // 4 startTimeMs
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.endMs)); // 5 endTimeMs
|
||||
PushMs(L, c.startMs); // 4 startTimeMs
|
||||
PushMs(L, c.endMs); // 5 endTimeMs
|
||||
Game::Lua::PushBool(L, IsTradeskill(rec)); // 6 isTradeskill
|
||||
Game::Lua::PushNil(L); // 7 castID
|
||||
Game::Lua::PushBool(L, false); // 8 notInterruptible
|
||||
@@ -230,8 +250,8 @@ int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes
|
||||
Game::Lua::PushString(L, name); // 2 text / displayName
|
||||
Game::Lua::PushString(L, SpellIconPath(rec)); // 3 textureID (path)
|
||||
if (haveTimes) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(startMs)); // 4 startTimeMs
|
||||
Game::Lua::PushNumber(L, static_cast<double>(endMs)); // 5 endTimeMs
|
||||
PushMs(L, startMs); // 4 startTimeMs
|
||||
PushMs(L, endMs); // 5 endTimeMs
|
||||
} else {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
|
||||
+48
-31
@@ -46,6 +46,34 @@ uint32_t MountDisplayID(const uint8_t *desc) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID);
|
||||
}
|
||||
|
||||
// Scans the player's helpful-aura (buff) slots for the first spell whose
|
||||
// Spell.dbc record lists `auraEffect` (a `SPELL_AURA_*` code) in any of
|
||||
// its three effect slots, and returns that spellID (0 if none). Used by
|
||||
// `Dismount` to find the active mount aura to cancel — the engine's
|
||||
// `CMSG_CANCEL_AURA` needs the spellID, not a slot index.
|
||||
uint32_t FindPlayerBuffWithEffect(int auraEffect) {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_BUFF_COUNT; ++slot) {
|
||||
if (!Aura::Data::IsSlotPopulated(player, slot))
|
||||
continue;
|
||||
const uint32_t spellID = Aura::Data::ReadSpellID(player, slot);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
|
||||
auto *auraTypes = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int eff = 0; eff < Offsets::SPELL_RECORD_EFFECT_COUNT; ++eff) {
|
||||
if (auraTypes[eff] == auraEffect)
|
||||
return spellID;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `IsMounted()` — true iff the player is currently mounted. Reads
|
||||
// `UNIT_FIELD_MOUNTDISPLAYID` from the descriptor; non-zero means
|
||||
// the engine is rendering a mount under the player.
|
||||
@@ -71,46 +99,35 @@ int __fastcall Script_Dismount(void *L) {
|
||||
if (MountDisplayID(desc) == 0)
|
||||
return 0;
|
||||
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_BUFF_COUNT; ++slot) {
|
||||
if (!Aura::Data::IsSlotPopulated(player, slot))
|
||||
continue;
|
||||
const uint32_t spellID = Aura::Data::ReadSpellID(player, slot);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
|
||||
auto *auraTypes = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int eff = 0; eff < Offsets::SPELL_RECORD_EFFECT_COUNT; ++eff) {
|
||||
if (auraTypes[eff] != Offsets::SPELL_AURA_MOUNTED)
|
||||
continue;
|
||||
auto fn = reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND));
|
||||
fn(static_cast<int>(spellID));
|
||||
return 0;
|
||||
}
|
||||
const uint32_t spellID = FindPlayerBuffWithEffect(Offsets::SPELL_AURA_MOUNTED);
|
||||
if (spellID != 0) {
|
||||
auto fn = reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND));
|
||||
fn(static_cast<int>(spellID));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `IsStealthed()` — true iff the player is currently stealthed
|
||||
// (Rogue Stealth / Druid Prowl). Checks `PLAYER_FIELD_VIS_BYTES`
|
||||
// bit 0x02. Mount also sets that bit, so we AND-gate with
|
||||
// `mountDisplayID == 0` to disambiguate. See the comment on
|
||||
// `OFF_PLAYER_FIELD_VIS_BYTES` in Offsets.h for the full bit map
|
||||
// and known limitations (untested for shapeshift forms).
|
||||
// (Rogue Stealth / Druid Prowl / Night Elf Shadowmeld / etc.). Reads
|
||||
// the sneak flag in `UNIT_FIELD_BYTES_1` byte 3 (mask
|
||||
// `UNIT_BYTES_1_STEALTH_FLAG`) — the same broadcast bit the client's
|
||||
// render path uses to draw a stealthed unit semi-transparent.
|
||||
//
|
||||
// Derived via `_classicapi_DiffAll`: a Prowl toggle from a Cat-Form
|
||||
// baseline flips byte 3 `0x00 -> 0x02` and touches nothing else but
|
||||
// aura-array slot metadata, so this is the one standalone descriptor
|
||||
// bit that means "stealthed" (the earlier `0x17C & 0x02` read was
|
||||
// actually AURALEVELS and only worked by coincidence). Being a unit
|
||||
// anim-tier flag rather than a class field, it covers every stealth
|
||||
// source and rank without a spellID list or an aura scan.
|
||||
int __fastcall Script_IsStealthed(void *L) {
|
||||
auto *desc = Unit::Identity::PlayerDescriptor();
|
||||
bool stealthed = false;
|
||||
if (desc != nullptr) {
|
||||
const uint32_t visBytes = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_PLAYER_FIELD_VIS_BYTES);
|
||||
stealthed = (visBytes & Offsets::PLAYER_VIS_BIT_STEALTH) != 0
|
||||
&& MountDisplayID(desc) == 0;
|
||||
const uint32_t bytes1 = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_BYTES_1);
|
||||
stealthed = (bytes1 & Offsets::UNIT_BYTES_1_STEALTH_FLAG) != 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, stealthed);
|
||||
return 1;
|
||||
|
||||
Reference in New Issue
Block a user