Compare commits

...
2 Commits
Author SHA1 Message Date
Brues ba4520493a Stop clobbering native FrameXML time globals; sort enUS locale
Stock 1.12.1 already defines DAYS_ABBR/HOURS_ABBR/MINUTES_ABBR/
SECONDS_ABBR (bare labels: "Day"/"Hr"/"Min"/"Sec") and the four
*_ONELETTER_ABBR. Our enUS.lua loads after FrameXML, so redefining
them with the modern "%d |4Day:Days;" format silently overwrote the
native values and broke any native/other-addon code reading the bare
labels.

Remove those redefinitions from enUS.lua (keep only the strings stock
genuinely lacks), and drop SecondsToTime / SecondsToTimeAbbrev /
FormatUnit from TimeUtil — they were the only consumers of the
modern-format _ABBR strings. SecondsToClock stays (uses the
stock-missing HOURS_MINUTES_SECONDS / MINUTES_SECONDS). Remaining
enUS entries sorted, all still have live consumers.
2026-06-22 12:19:26 -05:00
Brues dc80ade21b Add native GetPhysicalScreenSize + PixelUtil backport
`GetPhysicalScreenSize()` → (widthPixels, heightPixels). Modern WoW
added this native in 7.0; FrameXML's PixelUtil builds its entire
pixel↔UI-unit conversion on it (768/physicalHeight). 1.12 lacks it and
has no pre-parsed pixel-dimension global — GetScreenWidth/Height are
UI-space (UIParent aspect at [UIParent+0x7c]), and the engine only
holds the current mode as the gxResolution CVar string, sscanf'd on
demand by SetScreenResolution / display-init. So the native reads the
gxResolution CVar directly (FUN_FIND_CVAR, value at +0x20) and parses
"WIDTHxHEIGHT" — the same source pfUI's GetPerfectPixel uses. Verified
in-game: 2560x1080.

7.0 was the only binary exposing the function, but retail .text is
encrypted on disk so static decompilation yields garbage; derived the
implementation from the 1.12 binary instead.

Also backports FrameXML/PixelUtil.lua (SetPoint/SetSize/SetWidth/
SetHeight/SetStatusBarValue/Get*). Its math deps (Round/Lerp/
ClampedPercentageBetween) already live in MathUtil; it consumes the
native GetPhysicalScreenSize (no Lua polyfill).
2026-06-22 00:04:17 -05:00
7 changed files with 248 additions and 103 deletions
+1
View File
@@ -14,6 +14,7 @@ Util\Constants.lua
Util\String.lua
Util\TableUtil.lua
Util\MathUtil.lua
Util\PixelUtil.lua
Util\FrameUtil.lua
Util\FormattingUtil.lua
Util\TimeUtil.lua
+86
View File
@@ -0,0 +1,86 @@
-- Backport of FrameXML/PixelUtil.lua to vanilla 1.12 / Lua 5.0.
--
-- PixelUtil snaps frame sizes/points to whole physical pixels so
-- borders and textures stay crisp at fractional UI scales. The table
-- ports verbatim — its only math deps (Round, Lerp,
-- ClampedPercentageBetween) all live in MathUtil, which loads first.
--
-- The one piece vanilla lacks is the native `GetPhysicalScreenSize()`
-- (added in 7.0). The ClassicAPI DLL provides it (see
-- src/screen/Info.cpp) by reading the engine's `gxResolution` CVar, so
-- it's a real global registered at engine boot — no Lua polyfill
-- needed here.
PixelUtil = {};
function PixelUtil.GetPixelToUIUnitFactor()
local physicalWidth, physicalHeight = GetPhysicalScreenSize();
return 768.0 / physicalHeight;
end
function PixelUtil.GetNearestPixelSize(uiUnitSize, layoutScale, minPixels)
if uiUnitSize == 0 and (not minPixels or minPixels == 0) then
return 0;
end
local uiUnitFactor = PixelUtil.GetPixelToUIUnitFactor();
local numPixels = Round((uiUnitSize * layoutScale) / uiUnitFactor);
if minPixels then
if uiUnitSize < 0.0 then
if numPixels > -minPixels then
numPixels = -minPixels;
end
else
if numPixels < minPixels then
numPixels = minPixels;
end
end
end
return numPixels * uiUnitFactor / layoutScale;
end
function PixelUtil.ConvertPixelsToUI(desiredPixels, layoutScale)
return PixelUtil.GetNearestPixelSize(desiredPixels, layoutScale);
end
function PixelUtil.ConvertPixelsToUIForRegion(desiredPixels, region)
return PixelUtil.GetNearestPixelSize(desiredPixels, region:GetEffectiveScale());
end
function PixelUtil.SetWidth(region, width, minPixels)
region:SetWidth(PixelUtil.GetNearestPixelSize(width, region:GetEffectiveScale(), minPixels));
end
function PixelUtil.SetHeight(region, height, minPixels)
region:SetHeight(PixelUtil.GetNearestPixelSize(height, region:GetEffectiveScale(), minPixels));
end
function PixelUtil.SetSize(region, width, height, minWidthPixels, minHeightPixels)
PixelUtil.SetWidth(region, width, minWidthPixels);
PixelUtil.SetHeight(region, height, minHeightPixels);
end
function PixelUtil.SetPoint(region, point, relativeTo, relativePoint, offsetX, offsetY, minOffsetXPixels, minOffsetYPixels)
region:SetPoint(point, relativeTo, relativePoint,
PixelUtil.GetNearestPixelSize(offsetX, region:GetEffectiveScale(), minOffsetXPixels),
PixelUtil.GetNearestPixelSize(offsetY, region:GetEffectiveScale(), minOffsetYPixels)
);
end
function PixelUtil.SetStatusBarValue(statusBar, value)
local width = statusBar:GetWidth();
if width and width > 0.0 then
local min, max = statusBar:GetMinMaxValues();
local percent = ClampedPercentageBetween(value, min, max);
if percent == 0.0 or percent == 1.0 then
statusBar:SetValue(value);
else
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetWidth() * percent, statusBar:GetEffectiveScale());
local roundedValue = Lerp(min, max, numPixels / width);
statusBar:SetValue(roundedValue);
end
else
statusBar:SetValue(value);
end
end
-77
View File
@@ -1,16 +1,3 @@
-- Backport of FrameXML/TimeUtil.lua (feasible subset) to vanilla 1.12 /
-- Lua 5.0.
--
-- Ports the deprecated-but-widely-used duration formatters (SecondsToTime,
-- SecondsToClock, SecondsToTimeAbbrev) plus the unit constants/helpers. The
-- SecondsFormatterMixin (the modern replacement) is omitted — it's a heavy
-- mixin class little addon code targets on classic.
--
-- 5.0 notes: `%` -> math.mod, `s:format()` -> string.format. The FrameXML
-- format-string globals these functions consume (DAYS_ABBR, D_HOURS,
-- HOURS_MINUTES_SECONDS, the *_ONELETTER_ABBR set, …) are defined in the
-- locale layer (locales/enUS.lua), which loads first.
SECONDS_PER_MIN = 60;
SECONDS_PER_HOUR = 60 * SECONDS_PER_MIN;
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
@@ -52,67 +39,3 @@ function SecondsToClock(seconds, displayZeroHours)
return string.format(MINUTES_SECONDS, units.minutes, units.seconds);
end
end
-- 1.12's native unit strings are bare labels ("Hr", "Min") with no `%d`,
-- unlike modern's "%d Hr"; insert the count correctly for either shape.
local function FormatUnit(fmt, count)
if string.find(fmt, "%%d") then
return string.format(fmt, count);
end
return count .. " " .. fmt;
end
function SecondsToTime(seconds, noSeconds, notAbbreviated, maxCount, roundUp)
local time = "";
local count = 0;
local tempTime;
seconds = roundUp and math.ceil(seconds) or math.floor(seconds);
maxCount = maxCount or 2;
-- With a single term, use a higher 1.5x threshold; with 2+, 1.0x.
local threshold = (maxCount > 1) and 1.0 or 1.5;
if seconds >= SECONDS_PER_DAY * threshold then
count = count + 1;
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_DAY)
or math.floor(seconds / SECONDS_PER_DAY);
time = FormatUnit(notAbbreviated and D_DAYS or DAYS_ABBR, tempTime);
seconds = math.mod(seconds, SECONDS_PER_DAY);
end
if count < maxCount and seconds >= SECONDS_PER_HOUR * threshold then
count = count + 1;
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_HOUR)
or math.floor(seconds / SECONDS_PER_HOUR);
time = time .. FormatUnit(notAbbreviated and D_HOURS or HOURS_ABBR, tempTime);
seconds = math.mod(seconds, SECONDS_PER_HOUR);
end
if count < maxCount and seconds >= SECONDS_PER_MIN * threshold then
count = count + 1;
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_MIN)
or math.floor(seconds / SECONDS_PER_MIN);
time = time .. FormatUnit(notAbbreviated and D_MINUTES or MINUTES_ABBR, tempTime);
seconds = math.mod(seconds, SECONDS_PER_MIN);
end
if count < maxCount and seconds > 0 and not noSeconds then
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
time = time .. FormatUnit(notAbbreviated and D_SECONDS or SECONDS_ABBR, seconds);
end
return time;
end
-- Returns (formatString, value) for the single largest applicable unit.
function SecondsToTimeAbbrev(seconds, thresholdOverride)
local threshold = thresholdOverride or 1.5;
if seconds >= SECONDS_PER_DAY * threshold then
return DAY_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_DAY);
end
if seconds >= SECONDS_PER_HOUR * threshold then
return HOUR_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_HOUR);
end
if seconds >= SECONDS_PER_MIN * threshold then
return MINUTE_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_MIN);
end
return SECOND_ONELETTER_ABBR, seconds;
end
+12 -26
View File
@@ -1,13 +1,21 @@
BINDING_NAME_AOELOOT = "AoE Loot"
BINDING_NAME_AOELOOT = "AoE Loot";
BINDING_NAME_FOCUSTARGET = "Focus Target";
BINDING_NAME_TARGETFOCUS = "Target Focus";
COPPER_AMOUNT_SYMBOL = "c";
FOCUSTARGET = "Focus Target";
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
GENERIC_FRACTION_STRING = "%d/%d";
GOLD_AMOUNT_SYMBOL = "g";
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
ITEMS_EQUIPPED = "%d |4item:items; equipped";
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
ITEM_MISSING = "%s missing";
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
ITEM_MISSING = "%s missing";
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
LARGE_NUMBER_SEPERATOR = ",";
MINUTES_SECONDS = "%.2d:%.2d";
PERCENTAGE_STRING = "%d%%";
SILVER_AMOUNT_SYMBOL = "s";
SLASH_CLEARFOCUS1 = "/clearfocus";
SLASH_CLEARFOCUS2 = "/clearfocus";
SLASH_EQUIP_SET1 = "/equipset";
@@ -15,25 +23,3 @@ SLASH_EQUIP_SET2 = "/equipset";
SLASH_FOCUS1 = "/focus";
SLASH_FOCUS2 = "/focus";
TARGETFOCUS = "Target Focus";
LARGE_NUMBER_SEPERATOR = ",";
PERCENTAGE_STRING = "%d%%";
GENERIC_FRACTION_STRING = "%d/%d";
GOLD_AMOUNT_SYMBOL = "g";
SILVER_AMOUNT_SYMBOL = "s";
COPPER_AMOUNT_SYMBOL = "c";
TIME_UNIT_DELIMITER = " ";
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
MINUTES_SECONDS = "%.2d:%.2d";
DAYS_ABBR = "%d |4Day:Days;";
HOURS_ABBR = "%d |4Hr:Hr;";
MINUTES_ABBR = "%d |4Min:Min;";
SECONDS_ABBR = "%d |4Sec:Sec;";
D_DAYS = "%d |4Day:Days;";
D_HOURS = "%d |4Hour:Hours;";
D_MINUTES = "%d |4Minute:Minutes;";
D_SECONDS = "%d |4Second:Seconds;";
DAY_ONELETTER_ABBR = "%d d";
HOUR_ONELETTER_ABBR = "%d h";
MINUTE_ONELETTER_ABBR = "%d m";
SECOND_ONELETTER_ABBR = "%d s";
+1
View File
@@ -47,6 +47,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
+37
View File
@@ -270,6 +270,9 @@ build instructions.
- [`C_QuestLog.IsQuestDataCachedByID(questID)`](#c_questlogisquestdatacachedbyidquestid)
- [`GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])`](#getquestlogleaderboardidobjectiveindex--questindex)
- [Screen](#screen)
- [`GetPhysicalScreenSize()`](#getphysicalscreensize)
- [Spell](#spell)
- [`C_Spell.DoesSpellExist(spellID)`](#c_spelldoesspellexistspellid)
- [`C_Spell.GetSchoolString(schoolMask)`](#c_spellgetschoolstringschoolmask)
@@ -6609,6 +6612,40 @@ A separate function keeps the existing call wire-compatible.
> first, skip zero slots, then the item array. 1-based `objectiveIndex`
> counts only non-empty slots.
## Screen
### `GetPhysicalScreenSize()`
Returns `(widthPixels, heightPixels)` — the display's physical
resolution in pixels. Modern WoW added this as a native in 7.0
(Legion); FrameXML's `PixelUtil` builds its entire pixel↔UI-unit
conversion on it (`768.0 / physicalHeight`), which is what lets addons
snap frames to whole pixels for crisp borders at fractional UI scales.
```lua
local w, h = GetPhysicalScreenSize() -- e.g. 1920, 1080
```
Vanilla 1.12 has no such native — and no pre-parsed pixel-dimension
global. `GetScreenWidth()`/`GetScreenHeight()` return UI-space units
(derived from the UIParent aspect ratio at `[UIParent + 0x7c]`), not
pixels. The engine only ever holds the current mode as the
`gxResolution` CVar string (`"WIDTHxHEIGHT"`); both `SetScreenResolution`
(`0x0048bfd0`) and the display-init path sscanf it on demand rather
than caching dimensions anywhere. So we read the `gxResolution` CVar
directly through the engine's by-name lookup (`FUN_FIND_CVAR`, value at
`+0x20`) and parse it — the same source of truth the engine uses.
Returns `1024, 768` (→ factor 1.0, making PixelUtil a 1:1 no-op) when
the CVar is missing or unparseable. Caveat: in windowed mode
`gxResolution` reflects the configured render resolution, which may
differ from the OS window's client size — vanilla exposes no separate
window-pixel query.
The companion `PixelUtil` table (`PixelUtil.SetPoint`/`SetSize`/
`SetStatusBarValue`/…) is provided Lua-side by the ClassicAPI addon and
consumes this function.
## Spell
> **All `C_Spell.*` functions in this section accept any spell
+111
View File
@@ -0,0 +1,111 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetPhysicalScreenSize()` → (widthPixels, heightPixels) — the
// display's physical resolution in pixels. Modern WoW added this as a
// native in 7.0 (Legion); FrameXML's `PixelUtil` hangs its entire
// pixel↔UI-unit conversion off it (`768.0 / physicalHeight`), which is
// what lets addons snap frames to whole pixels.
//
// Vanilla 1.12 has no such native and no pre-parsed pixel-dimension
// global — the engine only ever holds the current mode as the
// `gxResolution` CVar string ("WIDTHxHEIGHT"). `Script_GetScreenWidth`
// / `GetScreenHeight` return UI-space units (derived from the UIParent
// aspect ratio at `[UIParent + 0x7c]`), not pixels, so they can't
// serve. `SetScreenResolution` (`FUN_0048bfd0`) and the display-init
// path both sscanf the `gxResolution` value on demand rather than
// caching width/height anywhere — so the CVar string is the
// authoritative current-resolution source, and we parse it the same
// way the engine does.
//
// Returns 1024x768 (→ factor 1.0, i.e. 1 pixel == 1 UI unit, making
// PixelUtil a no-op pass-through) when the CVar is missing or
// unparseable. Caveat: in windowed mode `gxResolution` reflects the
// configured render resolution, which may differ from the OS window's
// client size — vanilla exposes no separate window-pixel query.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Screen::Info {
namespace {
using FindCVar_t = const uint8_t *(__fastcall *)(const char *name);
// Parses "WIDTHxHEIGHT" (e.g. "1920x1080"). Tolerant of the separator
// (any run of non-digits between the two numbers) so an uppercase 'X'
// or stray spaces don't break it. Returns false unless both numbers
// are present and positive.
bool ParseResolution(const char *s, int *outW, int *outH) {
if (s == nullptr)
return false;
const char *p = s;
int w = 0;
bool gotW = false;
while (*p >= '0' && *p <= '9') {
w = w * 10 + (*p - '0');
++p;
gotW = true;
}
if (!gotW)
return false;
while (*p != '\0' && (*p < '0' || *p > '9')) // skip the 'x' separator
++p;
int h = 0;
bool gotH = false;
while (*p >= '0' && *p <= '9') {
h = h * 10 + (*p - '0');
++p;
gotH = true;
}
if (!gotH || w <= 0 || h <= 0)
return false;
*outW = w;
*outH = h;
return true;
}
int __fastcall Script_GetPhysicalScreenSize(void *L) {
int width = 1024;
int height = 768; // fallback → 768/768 = factor 1.0 (no-op snapping)
auto findCVar = reinterpret_cast<FindCVar_t>(Offsets::FUN_FIND_CVAR);
const uint8_t *cvar = findCVar("gxResolution");
if (cvar != nullptr) {
const char *value = *reinterpret_cast<const char *const *>(
cvar + Offsets::OFF_CVAR_VALUE_STR);
ParseResolution(value, &width, &height);
}
Game::Lua::PushNumber(L, width);
Game::Lua::PushNumber(L, height);
return 2;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetPhysicalScreenSize",
&Script_GetPhysicalScreenSize);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Screen::Info