mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 23:20:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae9b83ad7 | ||
|
|
26e5fdc052 | ||
|
|
4281cd8253 | ||
|
|
9239756153 | ||
|
|
f13aa1943d | ||
|
|
438ad97f8f | ||
|
|
7f4b20c6ec | ||
|
|
3f11f942bd | ||
|
|
9622ea7742 | ||
|
|
659dd7e159 | ||
|
|
1ae102b554 | ||
|
|
a5b6cf367d | ||
|
|
b496568670 | ||
|
|
a37c590a72 | ||
|
|
60479677c1 | ||
|
|
b5038dc3fd | ||
|
|
0bbf802316 | ||
|
|
0fcaa2e064 | ||
|
|
6724ddd2bd | ||
|
|
50d001a71f | ||
|
|
3aad341a91 | ||
|
|
3141acf547 | ||
|
|
05e7bda5da | ||
|
|
c4b48f7f42 | ||
|
|
4d97b1c449 | ||
|
|
e447c70ca3 | ||
|
|
72fa36956e |
@@ -38,3 +38,8 @@ Util\LinkUtil.lua
|
||||
Util\TradeSkillLink.lua
|
||||
Util\IconDataProvider.lua
|
||||
Util\GameTooltip.lua
|
||||
Util\Rectangle.lua
|
||||
Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
|
||||
@@ -24,8 +24,14 @@ function ColorMixin:GetRGB()
|
||||
return self.r, self.g, self.b
|
||||
end
|
||||
|
||||
function ColorMixin:GetHSL()
|
||||
local r, g, b, a = self.r, self.g, self.b, self.a
|
||||
local h, s, l = C_ColorUtil.ConvertHSVToHSL(C_ColorUtil.ConvertRGBToHSV(r, g, b))
|
||||
return h, s, l, a or 1
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGBAsBytes()
|
||||
return self.r * 255, self.g * 255, self.b * 255
|
||||
return Round(self.r * 255), Round(self.g * 255), Round(self.b * 255)
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGBA()
|
||||
@@ -33,14 +39,14 @@ function ColorMixin:GetRGBA()
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGBAAsBytes()
|
||||
return self.r * 255, self.g * 255, self.b * 255, (self.a or 1) * 255
|
||||
return Round(self.r * 255), Round(self.g * 255), Round(self.b * 255), Round((self.a or 1) * 255)
|
||||
end
|
||||
|
||||
function ColorMixin:SetRGBA(r, g, b, a)
|
||||
self.r = r
|
||||
self.g = g
|
||||
self.b = b
|
||||
self.a = a
|
||||
self.a = a or 1
|
||||
end
|
||||
|
||||
function ColorMixin:SetRGB(r, g, b)
|
||||
@@ -48,56 +54,27 @@ function ColorMixin:SetRGB(r, g, b)
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColor()
|
||||
return string.format("ff%.2x%.2x%.2x", self:GetRGBAsBytes())
|
||||
return C_ColorUtil.GenerateTextColorCode(self)
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColorNoAlpha()
|
||||
return string.format("%.2X%.2X%.2X", self:GetRGBAsBytes())
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColorMarkup()
|
||||
return "|c"..self:GenerateHexColor()
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColorNoAlpha()
|
||||
return string.format("%.2x%.2x%.2x", self:GetRGBAsBytes())
|
||||
end
|
||||
|
||||
function ColorMixin:GetHSL()
|
||||
local r, g, b, a = self.r, self.g, self.b, self.a
|
||||
local max, min = math.max(r, g, b), math.min(r, g, b)
|
||||
local h, s, l
|
||||
l = (max + min) / 2
|
||||
if max == min then
|
||||
h, s = 0, 0 -- achromatic
|
||||
else
|
||||
local d = max - min
|
||||
if l > 0.5 then
|
||||
s = d / (2 - max - min)
|
||||
else
|
||||
s = d / (max + min)
|
||||
end
|
||||
if max == r then
|
||||
h = (g - b) / d
|
||||
if g < b then
|
||||
h = h + 6
|
||||
end
|
||||
elseif max == g then
|
||||
h = (b - r) / d + 2
|
||||
elseif max == b then
|
||||
h = (r - g) / d + 4
|
||||
end
|
||||
h = h / 6
|
||||
end
|
||||
return h, s, l, a or 1
|
||||
end
|
||||
|
||||
function ColorMixin:WrapTextInColorCode(text)
|
||||
return WrapTextInColorCode(text, self:GenerateHexColor())
|
||||
return C_ColorUtil.WrapTextInColor(text, self)
|
||||
end
|
||||
|
||||
function WrapTextInColorCode(text, colorHexString)
|
||||
return string.format("|c%s%s|r", colorHexString, text)
|
||||
return C_ColorUtil.WrapTextInColorCode(text, colorHexString)
|
||||
end
|
||||
|
||||
function WrapTextInColor(text, color)
|
||||
return WrapTextInColorCode(text, color:GenerateHexColor())
|
||||
return C_ColorUtil.WrapTextInColor(text, color)
|
||||
end
|
||||
|
||||
function ColorMixin:WrapTextInColorTableCode(text)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
COLOR_FORMAT_RGBA = "RRGGBBAA"
|
||||
COLOR_FORMAT_RGB = "RRGGBB"
|
||||
COLOR_FORMAT_ARGB = "AARRGGBB"
|
||||
FONT_COLOR_CODE_CLOSE = "|r"
|
||||
|
||||
do
|
||||
local ColorMixin = ColorMixin
|
||||
for _, classColor in pairs(RAID_CLASS_COLORS) do
|
||||
@@ -12,30 +17,40 @@ function ExtractColorValueFromHex(str, index)
|
||||
end
|
||||
|
||||
function CreateColorFromHexString(hexColor)
|
||||
if string.len(hexColor) == 8 then
|
||||
if string.len(hexColor) == string.len(COLOR_FORMAT_ARGB) then
|
||||
local a, r, g, b = ExtractColorValueFromHex(hexColor, 1), ExtractColorValueFromHex(hexColor, 3), ExtractColorValueFromHex(hexColor, 5), ExtractColorValueFromHex(hexColor, 7);
|
||||
return CreateColor(r, g, b, a);
|
||||
else
|
||||
error("CreateColorFromHexString input must be hexadecimal digits in this format: AARRGGBB.", 2);
|
||||
end
|
||||
error("CreateColorFromHexString input must be hexadecimal digits in this format: "..COLOR_FORMAT_ARGB..".", 2);
|
||||
return nil;
|
||||
end
|
||||
|
||||
function CreateColorFromRGBAHexString(hexColor)
|
||||
if string.len(hexColor) == 8 then
|
||||
if string.len(hexColor) == string.len(COLOR_FORMAT_RGBA) then
|
||||
local r, g, b, a = ExtractColorValueFromHex(hexColor, 1), ExtractColorValueFromHex(hexColor, 3), ExtractColorValueFromHex(hexColor, 5), ExtractColorValueFromHex(hexColor, 7);
|
||||
return CreateColor(r, g, b, a);
|
||||
else
|
||||
error("CreateColorFromRGBAHexString input must be hexadecimal digits in this format: RRGGBBAA.", 2);
|
||||
end
|
||||
error("CreateColorFromRGBAHexString input must be hexadecimal digits in this format: "..COLOR_FORMAT_RGBA..".", 2);
|
||||
return nil;
|
||||
end
|
||||
|
||||
function CreateColorFromRGBHexString(hexColor)
|
||||
if string.len(hexColor) == 6 then
|
||||
if string.len(hexColor) == string.len(COLOR_FORMAT_RGB) then
|
||||
local r, g, b = ExtractColorValueFromHex(hexColor, 1), ExtractColorValueFromHex(hexColor, 3), ExtractColorValueFromHex(hexColor, 5);
|
||||
return CreateColor(r, g, b, 1);
|
||||
else
|
||||
error("CreateColorFromRGBHexString input must be hexadecimal digits in this format: RRGGBB.", 2);
|
||||
end
|
||||
error("CreateColorFromRGBHexString input must be hexadecimal digits in this format: "..COLOR_FORMAT_RGB..".", 2);
|
||||
return nil;
|
||||
end
|
||||
|
||||
function CreateColorFromBestRGBHexString(hexColor)
|
||||
if string.len(hexColor) == string.len(COLOR_FORMAT_RGBA) then
|
||||
return CreateColorFromRGBAHexString(hexColor);
|
||||
elseif string.len(hexColor) == string.len(COLOR_FORMAT_RGB) then
|
||||
return CreateColorFromRGBHexString(hexColor);
|
||||
end
|
||||
error("CreateColorFromBestRGBHexString input must be hexadecimal digits in either of these formats: "..COLOR_FORMAT_RGBA.." / "..COLOR_FORMAT_RGB..".", 2);
|
||||
return nil;
|
||||
end
|
||||
|
||||
function CreateColorFromBytes(r, g, b, a)
|
||||
@@ -68,9 +83,7 @@ end
|
||||
function GetClassColoredTextForUnit(unit, text)
|
||||
local _, classFilename = UnitClass(unit);
|
||||
local color = GetClassColorObj(classFilename);
|
||||
if (color) then
|
||||
return color:WrapTextInColorCode(text);
|
||||
end
|
||||
return color and color:WrapTextInColorCode(text) or text;
|
||||
end
|
||||
|
||||
function GetFactionColor(factionGroupTag)
|
||||
@@ -82,7 +95,7 @@ function GetFactionColor(factionGroupTag)
|
||||
end
|
||||
|
||||
function RGBToColorCode(r, g, b)
|
||||
return string.format("|cff%.2x%.2x%.2x", r * 255, g * 255, b * 255);
|
||||
return string.format("|cff%02x%02x%02x", r * 255, g * 255, b * 255);
|
||||
end
|
||||
|
||||
function RGBTableToColorCode(rgbTable)
|
||||
|
||||
@@ -11,12 +11,19 @@ local random = math.random
|
||||
local atan2 = math.atan2
|
||||
local sqrt = math.sqrt
|
||||
|
||||
math.huge = 1 / 0
|
||||
MathUtil =
|
||||
{
|
||||
Epsilon = .000001,
|
||||
};
|
||||
|
||||
function CreateCounter(initialCount)
|
||||
MathUtil.ApproxZero = MathUtil.Epsilon;
|
||||
MathUtil.ApproxOne = 1.0 - MathUtil.Epsilon;
|
||||
|
||||
function CreateCounter(initialCount, step)
|
||||
local count = initialCount or 0
|
||||
step = step or 1
|
||||
return function()
|
||||
count = count + 1
|
||||
count = count + step
|
||||
return count
|
||||
end
|
||||
end
|
||||
@@ -130,3 +137,31 @@ end
|
||||
function CalculateAngleBetween(x1, y1, x2, y2)
|
||||
return atan2(y2 - y1, x2 - x1)
|
||||
end
|
||||
|
||||
AccumulatorMixin = {};
|
||||
|
||||
function AccumulatorMixin:Init(initialCount)
|
||||
self.count = initialCount or 0;
|
||||
end
|
||||
|
||||
function AccumulatorMixin:Add(count)
|
||||
self.count = self.count + count;
|
||||
return self.count;
|
||||
end
|
||||
|
||||
function AccumulatorMixin:Subtract(count)
|
||||
self.count = self.count - count;
|
||||
return self.count;
|
||||
end
|
||||
|
||||
function AccumulatorMixin:Count()
|
||||
return self.count;
|
||||
end
|
||||
|
||||
function AccumulatorMixin:Reset(resetCount)
|
||||
self.count = resetCount or 0;
|
||||
end
|
||||
|
||||
function CreateAccumulator(initialCount)
|
||||
return CreateAndInitFromMixin(AccumulatorMixin, initialCount);
|
||||
end
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
-- Backport of 3.3.5 FrameXML/Mixin.lua to vanilla 1.12 / Lua 5.0.
|
||||
-- `Mixin` and `CreateFromMixins` are engine (DLL) natives — see
|
||||
-- src/baselib/Mixin.cpp — so they exist whenever ClassicAPI is injected,
|
||||
-- even with this addon disabled, mirroring retail where they live in
|
||||
-- TableUtil as C functions. Only the composite helper (which calls a Lua
|
||||
-- `:Init` method) stays here, like retail's Mixin.lua.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - Lua 5.0 has no select(); inside a vararg function the args are
|
||||
-- gathered into an implicit `arg` table (`arg.n` for the count).
|
||||
|
||||
function Mixin(object, ...)
|
||||
for i = 1, arg.n do
|
||||
local mixin = arg[i]
|
||||
if mixin then
|
||||
for k, v in pairs(mixin) do
|
||||
object[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return object
|
||||
end
|
||||
|
||||
function CreateFromMixins(...)
|
||||
return Mixin({}, unpack(arg))
|
||||
end
|
||||
-- Lua 5.0 note: varargs arrive in the implicit `arg` table (`arg.n` count),
|
||||
-- so we forward them with unpack(arg) rather than 5.1's `...`.
|
||||
|
||||
function CreateAndInitFromMixin(mixin, ...)
|
||||
local object = CreateFromMixins(mixin)
|
||||
|
||||
@@ -28,7 +28,7 @@ function PlayerUtil.GetClassColor()
|
||||
end
|
||||
|
||||
function PlayerUtil.GetCurrentSpeed()
|
||||
return GetUnitSpeed("player") / 7 * 100
|
||||
return GetUnitSpeed("player") / BASE_MOVEMENT_SPEED * 100
|
||||
end
|
||||
|
||||
function GetPlayerGuid()
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
RectangleMixin = {};
|
||||
|
||||
function CreateRectangle(left, right, top, bottom)
|
||||
local rectangle = CreateFromMixins(RectangleMixin);
|
||||
rectangle:OnLoad(left, right, top, bottom);
|
||||
return rectangle;
|
||||
end
|
||||
|
||||
function RectangleMixin:OnLoad(left, right, top, bottom)
|
||||
self:SetSides(left or 0.0, right or 0.0, top or 0.0, bottom or 0.0);
|
||||
end
|
||||
|
||||
function RectangleMixin:SetSides(left, right, top, bottom)
|
||||
self.left = left;
|
||||
self.right = right;
|
||||
self.top = top;
|
||||
self.bottom = bottom;
|
||||
end
|
||||
|
||||
function RectangleMixin:Reset()
|
||||
self.left = 0.0;
|
||||
self.right = 0.0;
|
||||
self.top = 0.0;
|
||||
self.bottom = 0.0;
|
||||
end
|
||||
|
||||
function RectangleMixin:Stretch(x, y)
|
||||
self:Adjust(-x, x, -y, y);
|
||||
end
|
||||
|
||||
function RectangleMixin:Move(x, y)
|
||||
self:Adjust(x, x, y, y);
|
||||
end
|
||||
|
||||
function RectangleMixin:Adjust(left, right, top, bottom)
|
||||
self.left = self.left + left;
|
||||
self.right = self.right + right;
|
||||
self.top = self.top + top;
|
||||
self.bottom = self.bottom + bottom;
|
||||
end
|
||||
|
||||
function RectangleMixin:IsEmpty()
|
||||
return self.left == self.right or self.top == self.bottom;
|
||||
end
|
||||
|
||||
function RectangleMixin:IsInsideOut()
|
||||
return self.left > self.right or self.top > self.bottom;
|
||||
end
|
||||
|
||||
function RectangleMixin:EnclosesPoint(x, y)
|
||||
return x >= self.left and x <= self.right and y >= self.top and y <= self.bottom;
|
||||
end
|
||||
|
||||
function RectangleMixin:EnclosesRect(otherRect)
|
||||
return self:EnclosesPoint(otherRect:GetLeft(), otherRect:GetTop()) and self:EnclosesPoint(otherRect:GetRight(), otherRect:GetBottom());
|
||||
end
|
||||
|
||||
function RectangleMixin:IntersectsRect(otherRect)
|
||||
return not (
|
||||
self.left > otherRect.right or
|
||||
self.right < otherRect.left or
|
||||
self.top > otherRect.bottom or
|
||||
self.bottom < otherRect.top
|
||||
);
|
||||
end
|
||||
|
||||
function RectangleMixin:GetTop()
|
||||
return self.top;
|
||||
end
|
||||
|
||||
function RectangleMixin:GetBottom()
|
||||
return self.bottom;
|
||||
end
|
||||
|
||||
function RectangleMixin:GetLeft()
|
||||
return self.left;
|
||||
end
|
||||
|
||||
function RectangleMixin:GetRight()
|
||||
return self.right;
|
||||
end
|
||||
|
||||
function RectangleMixin:GetWidth()
|
||||
return self.right - self.left;
|
||||
end
|
||||
|
||||
function RectangleMixin:GetHeight()
|
||||
return self.bottom - self.top;
|
||||
end
|
||||
|
||||
function RectangleMixin:GetCenter()
|
||||
return Lerp(self.left, self.right, .5), Lerp(self.top, self.bottom, .5);
|
||||
end
|
||||
|
||||
function RectangleMixin:GetCenterH()
|
||||
return Lerp(self.left, self.right, .5);
|
||||
end
|
||||
|
||||
function RectangleMixin:GetCenterV()
|
||||
return Lerp(self.top, self.bottom, .5);
|
||||
end
|
||||
|
||||
function RectangleMixin:SetTop(top)
|
||||
self.top = top;
|
||||
end
|
||||
|
||||
function RectangleMixin:SetBottom(bottom)
|
||||
self.bottom = bottom;
|
||||
end
|
||||
|
||||
function RectangleMixin:SetLeft(left)
|
||||
self.left = left;
|
||||
end
|
||||
|
||||
function RectangleMixin:SetRight(right)
|
||||
self.right = right;
|
||||
end
|
||||
|
||||
function RectangleMixin:SetWidth(width)
|
||||
self.right = self.left + width;
|
||||
end
|
||||
|
||||
function RectangleMixin:SetHeight(height)
|
||||
self.bottom = self.top + height;
|
||||
end
|
||||
|
||||
function RectangleMixin:SetSize(width, height)
|
||||
self:SetWidth(width);
|
||||
self:SetHeight(height);
|
||||
end
|
||||
|
||||
function RectangleMixin:SetCenter(x, y)
|
||||
local width = self:GetWidth();
|
||||
local height = self:GetHeight();
|
||||
|
||||
self.left = x - width * .5;
|
||||
self.right = x + width * .5;
|
||||
|
||||
self.top = y - height * .5;
|
||||
self.bottom = y + height * .5;
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
function MouseIsOver(region, topOffset, bottomOffset, leftOffset, rightOffset)
|
||||
return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset);
|
||||
end
|
||||
@@ -0,0 +1,142 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Vector2D.lua. Pure vector
|
||||
-- math on CreateFromMixins (a ClassicAPI C native) + Lua 5.0 math.*; no 1.12
|
||||
-- adaptation needed. (Retail's "used in secure code" note doesn't apply here —
|
||||
-- vanilla has no taint system.)
|
||||
local cos = math.cos;
|
||||
local sin = math.sin;
|
||||
local atan2 = math.atan2;
|
||||
local sqrt = math.sqrt;
|
||||
|
||||
function Vector2D_ScaleBy(scalar, x, y)
|
||||
return x * scalar, y * scalar;
|
||||
end
|
||||
|
||||
function Vector2D_DivideBy(divisor, x, y)
|
||||
return x / divisor, y / divisor;
|
||||
end
|
||||
|
||||
function Vector2D_Add(leftX, leftY, rightX, rightY)
|
||||
return leftX + rightX, leftY + rightY;
|
||||
end
|
||||
|
||||
function Vector2D_Subtract(leftX, leftY, rightX, rightY)
|
||||
return leftX - rightX, leftY - rightY;
|
||||
end
|
||||
|
||||
function Vector2D_Cross(leftX, leftY, rightX, rightY)
|
||||
return leftX * rightY - leftY * rightX;
|
||||
end
|
||||
|
||||
function Vector2D_Dot(leftX, leftY, rightX, rightY)
|
||||
return leftX * rightX + leftY * rightY;
|
||||
end
|
||||
|
||||
function Vector2D_GetLengthSquared(x, y)
|
||||
return Vector2D_Dot(x, y, x, y);
|
||||
end
|
||||
|
||||
function Vector2D_GetLength(x, y)
|
||||
return sqrt(Vector2D_GetLengthSquared(x, y));
|
||||
end
|
||||
|
||||
function Vector2D_Normalize(x, y)
|
||||
return Vector2D_DivideBy(Vector2D_GetLength(x, y), x, y);
|
||||
end
|
||||
|
||||
function Vector2D_CalculateAngleBetween(leftX, leftY, rightX, rightY)
|
||||
return atan2(Vector2D_Cross(leftX, leftY, rightX, rightY), Vector2D_Dot(leftX, leftY, rightX, rightY));
|
||||
end
|
||||
|
||||
function Vector2D_RotateDirection(rotationRadians, x, y)
|
||||
local cosValue = cos(rotationRadians);
|
||||
local sinValue = sin(rotationRadians);
|
||||
return x * cosValue - y * sinValue, x * sinValue + y * cosValue;
|
||||
end
|
||||
|
||||
Vector2DMixin = {};
|
||||
|
||||
function CreateVector2D(x, y)
|
||||
local vector = CreateFromMixins(Vector2DMixin);
|
||||
vector:OnLoad(x, y);
|
||||
return vector;
|
||||
end
|
||||
|
||||
function AreVector2DEqual(left, right)
|
||||
if left and right then
|
||||
return left:IsEqualTo(right);
|
||||
end
|
||||
return left == right;
|
||||
end
|
||||
|
||||
function Vector2DMixin:OnLoad(x, y)
|
||||
self:SetXY(x, y);
|
||||
end
|
||||
|
||||
function Vector2DMixin:IsEqualTo(otherVector)
|
||||
return self.x == otherVector.x
|
||||
and self.y == otherVector.y;
|
||||
end
|
||||
|
||||
function Vector2DMixin:GetXY()
|
||||
return self.x, self.y;
|
||||
end
|
||||
|
||||
function Vector2DMixin:SetXY(x, y)
|
||||
self.x = x;
|
||||
self.y = y;
|
||||
end
|
||||
|
||||
function Vector2DMixin:ScaleBy(scalar)
|
||||
self:SetXY(Vector2D_ScaleBy(scalar, self:GetXY()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector2DMixin:DivideBy(scalar)
|
||||
self:SetXY(Vector2D_DivideBy(scalar, self:GetXY()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector2DMixin:Add(other)
|
||||
self:SetXY(Vector2D_Add(self.x, self.y, other:GetXY()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector2DMixin:Subtract(other)
|
||||
self:SetXY(Vector2D_Subtract(self.x, self.y, other:GetXY()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector2DMixin:Cross(other)
|
||||
self:SetXY(Vector2D_Cross(self.x, self.y, other:GetXY()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector2DMixin:Dot(other)
|
||||
return Vector2D_Dot(self.x, self.y, other:GetXY());
|
||||
end
|
||||
|
||||
function Vector2DMixin:IsZero()
|
||||
return self.x == 0 and self.y == 0;
|
||||
end
|
||||
|
||||
function Vector2DMixin:GetLengthSquared()
|
||||
return Vector2D_GetLengthSquared(self:GetXY());
|
||||
end
|
||||
|
||||
function Vector2DMixin:GetLength()
|
||||
return Vector2D_GetLength(self:GetXY());
|
||||
end
|
||||
|
||||
function Vector2DMixin:Normalize()
|
||||
self:SetXY(Vector2D_Normalize(self:GetXY()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector2DMixin:RotateDirection(rotationRadians)
|
||||
self:SetXY(Vector2D_RotateDirection(rotationRadians, self:GetXY()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector2DMixin:Clone()
|
||||
return CreateVector2D(self:GetXY());
|
||||
end
|
||||
@@ -0,0 +1,173 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Vector3D.lua. Pure vector
|
||||
-- math on CreateFromMixins (a ClassicAPI C native) + Lua 5.0 math.*; no 1.12
|
||||
-- adaptation needed. (Retail's "used in secure code" note doesn't apply here —
|
||||
-- vanilla has no taint system.)
|
||||
local cos = math.cos;
|
||||
local sin = math.sin;
|
||||
local atan2 = math.atan2;
|
||||
local asin = math.asin;
|
||||
local sqrt = math.sqrt;
|
||||
|
||||
function Vector3D_ScaleBy(scalar, x, y, z)
|
||||
return x * scalar, y * scalar, z * scalar;
|
||||
end
|
||||
|
||||
function Vector3D_DivideBy(divisor, x, y, z)
|
||||
return x / divisor, y / divisor, z / divisor;
|
||||
end
|
||||
|
||||
function Vector3D_Add(leftX, leftY, leftZ, rightX, rightY, rightZ)
|
||||
return leftX + rightX, leftY + rightY, leftZ + rightZ;
|
||||
end
|
||||
|
||||
function Vector3D_Subtract(leftX, leftY, leftZ, rightX, rightY, rightZ)
|
||||
return leftX - rightX, leftY - rightY, leftZ - rightZ;
|
||||
end
|
||||
|
||||
function Vector3D_Cross(leftX, leftY, leftZ, rightX, rightY, rightZ)
|
||||
return leftY * rightZ - leftZ * rightY, leftZ * rightX - leftX * rightZ, leftX * rightY - leftY * rightX;
|
||||
end
|
||||
|
||||
function Vector3D_Dot(leftX, leftY, leftZ, rightX, rightY, rightZ)
|
||||
return leftX * rightX + leftY * rightY + leftZ * rightZ;
|
||||
end
|
||||
|
||||
function Vector3D_GetLengthSquared(x, y, z)
|
||||
return Vector3D_Dot(x, y, z, x, y, z);
|
||||
end
|
||||
|
||||
function Vector3D_GetLength(x, y, z)
|
||||
return sqrt(Vector3D_GetLengthSquared(x, y, z));
|
||||
end
|
||||
|
||||
function Vector3D_Normalize(x, y, z)
|
||||
return Vector3D_DivideBy(Vector3D_GetLength(x, y, z), x, y, z);
|
||||
end
|
||||
|
||||
function Vector3D_AddVector(left, right)
|
||||
local clone = left:Clone();
|
||||
clone:Add(right);
|
||||
return clone;
|
||||
end
|
||||
|
||||
function Vector3D_SubtractVector(left, right)
|
||||
local clone = left:Clone();
|
||||
clone:Subtract(right);
|
||||
return clone;
|
||||
end
|
||||
|
||||
function Vector3D_NormalizeVector(vector)
|
||||
local clone = vector:Clone();
|
||||
clone:Normalize();
|
||||
return clone;
|
||||
end
|
||||
|
||||
function Vector3D_ScaleVector(scalar, vector)
|
||||
local clone = vector:Clone();
|
||||
clone:ScaleBy(scalar);
|
||||
return clone;
|
||||
end
|
||||
|
||||
function Vector3D_CalculateNormalFromYawPitch(yaw, pitch)
|
||||
return cos(-pitch) * cos(yaw),
|
||||
cos(-pitch) * sin(yaw),
|
||||
sin(-pitch);
|
||||
end
|
||||
|
||||
function Vector3D_CalculateYawPitchFromNormal(x, y, z)
|
||||
if x ~= 0 or y ~= 0 then
|
||||
return atan2(y, x), asin(-z);
|
||||
end
|
||||
|
||||
return 0, asin(-z);
|
||||
end
|
||||
|
||||
function Vector3D_CalculateYawPitchFromNormalVector(vector)
|
||||
return Vector3D_CalculateYawPitchFromNormal(vector:GetXYZ());
|
||||
end
|
||||
|
||||
|
||||
function Vector3D_CreateNormalVectorFromYawPitch(yawRadians, pitchRadians)
|
||||
return CreateVector3D(Vector3D_CalculateNormalFromYawPitch(yawRadians, pitchRadians));
|
||||
end
|
||||
|
||||
Vector3DMixin = {};
|
||||
|
||||
function CreateVector3D(x, y, z)
|
||||
local vector = CreateFromMixins(Vector3DMixin);
|
||||
vector:OnLoad(x, y, z);
|
||||
return vector;
|
||||
end
|
||||
|
||||
function AreVector3DEqual(left, right)
|
||||
if left and right then
|
||||
return left:IsEqualTo(right);
|
||||
end
|
||||
return left == right;
|
||||
end
|
||||
|
||||
function Vector3DMixin:OnLoad(x, y, z)
|
||||
self:SetXYZ(x, y, z);
|
||||
end
|
||||
|
||||
function Vector3DMixin:IsEqualTo(otherVector)
|
||||
return self.x == otherVector.x
|
||||
and self.y == otherVector.y
|
||||
and self.z == otherVector.z;
|
||||
end
|
||||
|
||||
function Vector3DMixin:GetXYZ()
|
||||
return self.x, self.y, self.z;
|
||||
end
|
||||
|
||||
function Vector3DMixin:SetXYZ(x, y, z)
|
||||
self.x = x;
|
||||
self.y = y;
|
||||
self.z = z;
|
||||
end
|
||||
|
||||
function Vector3DMixin:ScaleBy(scalar)
|
||||
self:SetXYZ(Vector3D_ScaleBy(scalar, self:GetXYZ()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector3DMixin:DivideBy(scalar)
|
||||
self:SetXYZ(Vector3D_DivideBy(scalar, self:GetXYZ()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector3DMixin:Add(other)
|
||||
self:SetXYZ(Vector3D_Add(self.x, self.y, self.z, other:GetXYZ()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector3DMixin:Subtract(other)
|
||||
self:SetXYZ(Vector3D_Subtract(self.x, self.y, self.z, other:GetXYZ()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector3DMixin:Cross(other)
|
||||
self:SetXYZ(Vector3D_Cross(self.x, self.y, self.z, other:GetXYZ()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector3DMixin:Dot(other)
|
||||
return Vector3D_Dot(self.x, self.y, self.z, other:GetXYZ());
|
||||
end
|
||||
|
||||
function Vector3DMixin:GetLengthSquared()
|
||||
return Vector3D_GetLengthSquared(self:GetXYZ());
|
||||
end
|
||||
|
||||
function Vector3DMixin:GetLength()
|
||||
return Vector3D_GetLength(self:GetXYZ());
|
||||
end
|
||||
|
||||
function Vector3DMixin:Normalize()
|
||||
self:SetXYZ(Vector3D_Normalize(self:GetXYZ()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector3DMixin:Clone()
|
||||
return CreateVector3D(self:GetXYZ());
|
||||
end
|
||||
@@ -0,0 +1,143 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Vector4D.lua. Pure vector
|
||||
-- math on CreateFromMixins (a ClassicAPI C native) + Lua 5.0 math.*; no 1.12
|
||||
-- adaptation needed. (Retail's "used in secure code" note doesn't apply here —
|
||||
-- vanilla has no taint system.)
|
||||
local cos = math.cos;
|
||||
local sin = math.sin;
|
||||
local atan2 = math.atan2;
|
||||
local asin = math.asin;
|
||||
local sqrt = math.sqrt;
|
||||
|
||||
function Vector4D_ScaleBy(scalar, x, y, z, w)
|
||||
return x * scalar, y * scalar, z * scalar, w * scalar;
|
||||
end
|
||||
|
||||
function Vector4D_DivideBy(divisor, x, y, z, w)
|
||||
return x / divisor, y / divisor, z / divisor, w / divisor;
|
||||
end
|
||||
|
||||
function Vector4D_Add(leftX, leftY, leftZ, leftW, rightX, rightY, rightZ, rightW)
|
||||
return leftX + rightX, leftY + rightY, leftZ + rightZ, leftW + rightW;
|
||||
end
|
||||
|
||||
function Vector4D_Subtract(leftX, leftY, leftZ, leftW, rightX, rightY, rightZ, rightW)
|
||||
return leftX - rightX, leftY - rightY, leftZ - rightZ, leftW - rightW;
|
||||
end
|
||||
|
||||
function Vector4D_Dot(leftX, leftY, leftZ, leftW, rightX, rightY, rightZ, rightW)
|
||||
return leftX * rightX + leftY * rightY + leftZ * rightZ + leftW * rightW;
|
||||
end
|
||||
|
||||
function Vector4D_GetLengthSquared(x, y, z, w)
|
||||
return Vector4D_Dot(x, y, z, w, x, y, z, w);
|
||||
end
|
||||
|
||||
function Vector4D_GetLength(x, y, z, w)
|
||||
return sqrt(Vector4D_GetLengthSquared(x, y, z, w));
|
||||
end
|
||||
|
||||
function Vector4D_Normalize(x, y, z, w)
|
||||
return Vector4D_DivideBy(Vector4D_GetLength(x, y, z, w), x, y, z, w);
|
||||
end
|
||||
|
||||
function Vector4D_AddVector(left, right)
|
||||
local clone = left:Clone();
|
||||
clone:Add(right);
|
||||
return clone;
|
||||
end
|
||||
|
||||
function Vector4D_SubtractVector(left, right)
|
||||
local clone = left:Clone();
|
||||
clone:Subtract(right);
|
||||
return clone;
|
||||
end
|
||||
|
||||
function Vector4D_NormalizeVector(vector)
|
||||
local clone = vector:Clone();
|
||||
clone:Normalize();
|
||||
return clone;
|
||||
end
|
||||
|
||||
function Vector4D_ScaleVector(scalar, vector)
|
||||
local clone = vector:Clone();
|
||||
clone:ScaleBy(scalar);
|
||||
return clone;
|
||||
end
|
||||
|
||||
Vector4DMixin = {};
|
||||
|
||||
function CreateVector4D(x, y, z, w)
|
||||
local vector = CreateFromMixins(Vector4DMixin);
|
||||
vector:OnLoad(x, y, z, w);
|
||||
return vector;
|
||||
end
|
||||
|
||||
function AreVector4DEqual(left, right)
|
||||
if left and right then
|
||||
return left:IsEqualTo(right);
|
||||
end
|
||||
return left == right;
|
||||
end
|
||||
|
||||
function Vector4DMixin:OnLoad(x, y, z, w)
|
||||
self:SetXYZW(x, y, z, w);
|
||||
end
|
||||
|
||||
function Vector4DMixin:IsEqualTo(otherVector)
|
||||
return self.x == otherVector.x
|
||||
and self.y == otherVector.y
|
||||
and self.z == otherVector.z
|
||||
and self.w == otherVector.w;
|
||||
end
|
||||
|
||||
function Vector4DMixin:GetXYZW()
|
||||
return self.x, self.y, self.z, self.w;
|
||||
end
|
||||
|
||||
function Vector4DMixin:SetXYZW(x, y, z, w)
|
||||
self.x = x;
|
||||
self.y = y;
|
||||
self.z = z;
|
||||
self.w = w;
|
||||
end
|
||||
|
||||
function Vector4DMixin:ScaleBy(scalar)
|
||||
self:SetXYZW(Vector4D_ScaleBy(scalar, self:GetXYZW()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector4DMixin:DivideBy(scalar)
|
||||
self:SetXYZW(Vector4D_DivideBy(scalar, self:GetXYZW()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector4DMixin:Add(other)
|
||||
self:SetXYZW(Vector4D_Add(self.x, self.y, self.z, self.w, other:GetXYZW()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector4DMixin:Subtract(other)
|
||||
self:SetXYZW(Vector4D_Subtract(self.x, self.y, self.z, self.w, other:GetXYZW()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector4DMixin:Dot(other)
|
||||
return Vector4D_Dot(self.x, self.y, self.z, self.w, other:GetXYZW());
|
||||
end
|
||||
|
||||
function Vector4DMixin:GetLengthSquared()
|
||||
return Vector4D_GetLengthSquared(self:GetXYZW());
|
||||
end
|
||||
|
||||
function Vector4DMixin:GetLength()
|
||||
return Vector4D_GetLength(self:GetXYZW());
|
||||
end
|
||||
|
||||
function Vector4DMixin:Normalize()
|
||||
self:SetXYZW(Vector4D_Normalize(self:GetXYZW()));
|
||||
return self;
|
||||
end
|
||||
|
||||
function Vector4DMixin:Clone()
|
||||
return CreateVector4D(self:GetXYZW());
|
||||
end
|
||||
@@ -155,25 +155,6 @@ _initFrame:SetScript("OnEvent", function()
|
||||
end
|
||||
end);
|
||||
|
||||
-- Vanilla has no Frame:IsMouseOver(); do it ourselves from cursor + bounds.
|
||||
local function DebugTools_IsMouseOver(frame)
|
||||
if (not frame) then return false; end
|
||||
local visOK, visible = pcall(frame.IsVisible, frame);
|
||||
if (not visOK or not visible) then return false; end
|
||||
local sOK, scale = pcall(frame.GetEffectiveScale, frame);
|
||||
if (not sOK or not scale or scale == 0) then return false; end
|
||||
local cx, cy = GetCursorPosition();
|
||||
cx = cx / scale;
|
||||
cy = cy / scale;
|
||||
local lOK, left = pcall(frame.GetLeft, frame);
|
||||
local rOK, right = pcall(frame.GetRight, frame);
|
||||
local tOK, top = pcall(frame.GetTop, frame);
|
||||
local bOK, bottom = pcall(frame.GetBottom, frame);
|
||||
if (not (lOK and rOK and tOK and bOK)) then return false; end
|
||||
if (not (left and right and top and bottom)) then return false; end
|
||||
return cx >= left and cx <= right and cy >= bottom and cy <= top;
|
||||
end
|
||||
|
||||
local _EventTraceFrame;
|
||||
|
||||
_framesSinceLast = 0;
|
||||
@@ -566,7 +547,7 @@ function EventTraceFrame_Update()
|
||||
button:GetHighlightTexture():SetVertexColor(.8, .8, 1, .15);
|
||||
button:UnlockHighlight();
|
||||
end
|
||||
if (DebugTools_IsMouseOver(button)) then
|
||||
if (button:IsMouseOver()) then
|
||||
EventTraceFrameEvent_OnEnter(button);
|
||||
end
|
||||
end
|
||||
@@ -1016,8 +997,8 @@ local function _CollectFramesUnderMouse(showHidden)
|
||||
local frame = EnumerateFrames(nil);
|
||||
while (frame) do
|
||||
local visOK, visible = pcall(frame.IsVisible, frame);
|
||||
local mouseOver = DebugTools_IsMouseOver(frame);
|
||||
if (mouseOver and (showHidden or (visOK and visible))) then
|
||||
local overOK, mouseOver = pcall(frame.IsMouseOver, frame);
|
||||
if (overOK and mouseOver and (showHidden or (visOK and visible))) then
|
||||
local nameOK, name = pcall(frame.GetName, frame);
|
||||
if (not nameOK or not name) then name = "<anonymous>"; end
|
||||
table.insert(matches, { name = name, frame = frame });
|
||||
|
||||
+3
-1
@@ -118,7 +118,9 @@ target_sources(${PROJECT_NAME} PRIVATE "${CLASSICAPI_VERSION_RC}")
|
||||
# ole32: COM (CoInitializeEx / CoCreateInstance / CoTaskMemFree) for the
|
||||
# SAPI text-to-speech backend in src/voicechat/Tts.cpp. The SAPI GUIDs are
|
||||
# instantiated in that TU via <initguid.h>, so no sapi.lib is needed.
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE minhook advapi32 ole32)
|
||||
# user32: Win32 clipboard API (OpenClipboard / SetClipboardData) for
|
||||
# CopyToClipboard in src/clipboard/Copy.cpp.
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE minhook advapi32 ole32 user32)
|
||||
|
||||
target_include_directories(
|
||||
${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
|
||||
@@ -23,6 +23,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [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` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
@@ -34,11 +35,12 @@ 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:IsEventRegistered` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
| [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` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem` (script) |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject` (scripts) |
|
||||
| [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` |
|
||||
@@ -46,7 +48,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `math.huge`, `math.modf`, `select`, `string.gmatch`, `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.wipe` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.wipe` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `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` |
|
||||
@@ -57,10 +59,10 @@ Full per-function reference: **[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` |
|
||||
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
|
||||
| [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.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [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` |
|
||||
@@ -68,7 +70,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [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` |
|
||||
@@ -89,6 +91,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
| [Account](docs/API.md#account) | `SaveAccount`, `DeleteAccount`, `GetSavedAccounts`, `LoginWithSavedAccount` (passwords encrypted in Windows Credential Manager, scoped per realmlist; plaintext never returned to Lua) |
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
|
||||
+489
-82
@@ -34,6 +34,16 @@ build instructions.
|
||||
- [Class](#class)
|
||||
- [`FillLocalizedClassList(table [, isFemale])`](#filllocalizedclasslisttable-isfemale)
|
||||
|
||||
- [ColorUtil](#colorutil)
|
||||
- [`C_ColorUtil.ConvertRGBToHSV(r, g, b)`](#c_colorutilconvertrgbtohsvr-g-b)
|
||||
- [`C_ColorUtil.ConvertHSVToRGB(h, s, v)`](#c_colorutilconverthsvtorgbh-s-v)
|
||||
- [`C_ColorUtil.ConvertHSVToHSL(h, s, v)`](#c_colorutilconverthsvtohslh-s-v)
|
||||
- [`C_ColorUtil.ConvertHSLToHSV(h, s, l)`](#c_colorutilconverthsltohsvh-s-l)
|
||||
- [`C_ColorUtil.ConvertHSLToRGB(h, s, l)`](#c_colorutilconverthsltorgbh-s-l)
|
||||
- [`C_ColorUtil.GenerateTextColorCode(color)`](#c_colorutilgeneratetextcolorcodecolor)
|
||||
- [`C_ColorUtil.WrapTextInColor(text, color)`](#c_colorutilwraptextincolortext-color)
|
||||
- [`C_ColorUtil.WrapTextInColorCode(text, colorCode)`](#c_colorutilwraptextincolorcodetext-colorcode)
|
||||
|
||||
- [Combat](#combat)
|
||||
- [`InCombatLockdown()`](#incombatlockdown)
|
||||
|
||||
@@ -145,10 +155,14 @@ build instructions.
|
||||
- [Frame](#frame)
|
||||
- [`region:SetPoint("point")` (one-argument form)](#regionsetpointpoint-one-argument-form)
|
||||
- [`region:SetSize(width, height)` / `region:GetSize()`](#regionsetsizewidth-height--regiongetsize)
|
||||
- [`region:IsMouseOver([topOffset, bottomOffset, leftOffset, rightOffset])`](#regionismouseovertopoffset-bottomoffset-leftoffset-rightoffset)
|
||||
- [`region:GetRect()`](#regiongetrect)
|
||||
- [`region:IsDragging()`](#regionisdragging)
|
||||
- [`frame:SetShown(shown)`](#framesetshownshown)
|
||||
- [`frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`](#framesetresizeboundsminwidth-minheight--maxwidth-maxheight)
|
||||
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
|
||||
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
|
||||
- [`frame:GetEffectiveAlpha()`](#framegeteffectivealpha)
|
||||
|
||||
- [FriendList](#friendlist)
|
||||
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
|
||||
@@ -166,7 +180,7 @@ build instructions.
|
||||
- [`GameTooltip:SetInventoryItemByID(itemID)`](#gametooltipsetinventoryitembyiditemid)
|
||||
- [`GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])`](#gametooltipsethyperlinkcompareitemitemlink--offset-shiftbutton-comparisontooltip)
|
||||
- [`GameTooltip:IsEquippedItem()`](#gametooltipisequippeditem)
|
||||
- [`OnTooltipSetItem` script](#ontooltipsetitem-script)
|
||||
- [`OnTooltipSet*` scripts (Item / Spell / Unit / GameObject)](#ontooltipset-scripts)
|
||||
- [`GameTooltip:SetItemByGUID(itemGUID)`](#gametooltipsetitembyguiditemguid)
|
||||
- [`GameTooltip:SetEquipmentSet(name)`](#gametooltipsetequipmentsetname)
|
||||
- [`GameTooltip:SetTotem(slot)`](#gametooltipsettotemslot)
|
||||
@@ -188,6 +202,10 @@ build instructions.
|
||||
- [`Enum.ItemQuality`](#enumitemquality)
|
||||
- [`Enum.PowerType`](#enumpowertype)
|
||||
|
||||
- [Glue](#glue)
|
||||
- [`C_Glue.IsFirstLoadThisSession()`](#c_glueisfirstloadthissession)
|
||||
- [`C_Glue.IsOnGlueScreen()`](#c_glueisongluescreen)
|
||||
|
||||
- [Gossip](#gossip)
|
||||
- [`C_GossipInfo.GetText()`](#c_gossipinfogettext)
|
||||
- [`C_GossipInfo.GetOptions()`](#c_gossipinfogetoptions)
|
||||
@@ -290,6 +308,8 @@ build instructions.
|
||||
- [Lua](#lua)
|
||||
- [`select(index, ...)`](#selectindex-)
|
||||
- [`table.wipe(t)`](#tablewipet)
|
||||
- [`table.count(tbl)`](#tablecounttbl)
|
||||
- [`Mixin(object, ...)` / `CreateFromMixins(...)`](#mixinobject--createfrommixins)
|
||||
- [`string.match` / `string.gmatch`](#stringmatch--stringgmatch)
|
||||
- [`strsplit(sep, str [, pieces])`](#strsplitsep-str--pieces)
|
||||
- [`strjoin(delimiter, ...)`](#strjoindelimiter-)
|
||||
@@ -379,9 +399,6 @@ 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)
|
||||
@@ -393,6 +410,7 @@ build instructions.
|
||||
- [`C_Spell.GetSpellLink(spellID)`](#c_spellgetspelllinkspellid)
|
||||
- [`C_Spell.GetSpellDescription(spellID)`](#c_spellgetspelldescriptionspellid)
|
||||
- [`C_Spell.GetSpellMechanicByID(spellID)`](#c_spellgetspellmechanicbyidspellid)
|
||||
- [`C_Spell.GetSpellEffectMechanics(spellID)`](#c_spellgetspelleffectmechanicsspellid)
|
||||
- [`C_Spell.GetSpellRadius(spellID)` / `GetSpellRadius(slot, bookType)`](#c_spellgetspellradiusspellid--getspellradiusslot-booktype)
|
||||
- [`C_Spell.GetSpellPowerCost(spellIdentifier)`](#c_spellgetspellpowercostspellidentifier)
|
||||
- [`C_Spell.GetSpellReagents(spellID)`](#c_spellgetspellreagentsspellid)
|
||||
@@ -400,6 +418,7 @@ build instructions.
|
||||
- [`IsPassiveSpell(spellID)` / `IsPassiveSpell(slot, bookType)`](#ispassivespellspellid--ispassivespellslot-booktype)
|
||||
- [`C_Spell.IsSpellPassive(spellID)`](#c_spellisspellpassivespellid)
|
||||
- [`IsPlayerSpell(spellID)`](#isplayerspellspellid)
|
||||
- [`CanDualWield()`](#candualwield)
|
||||
- [`IsSpellKnown(spellID, [isPet])`](#isspellknownspellid-ispet)
|
||||
- [`GetSpellBonusDamage(school)`](#getspellbonusdamageschool)
|
||||
- [`GetSpellBonusHealing()`](#getspellbonushealing)
|
||||
@@ -447,6 +466,10 @@ build instructions.
|
||||
- [`GetShapeshiftFormID()`](#getshapeshiftformid)
|
||||
- [`CancelShapeshiftForm()`](#cancelshapeshiftform)
|
||||
|
||||
- [System](#system)
|
||||
- [`GetPhysicalScreenSize()`](#getphysicalscreensize)
|
||||
- [`CopyToClipboard(text [, removeMarkup])`](#copytoclipboardtext--removemarkup)
|
||||
|
||||
- [Talent](#talent)
|
||||
- [`GetTalentSpellID(tabIndex, talentIndex, [rank[, classID]])`](#gettalentspellidtabindex-talentindex-rank-classid)
|
||||
- [`GetTalentIDByIndex(tabIndex, talentIndex[, classID])`](#gettalentidbyindextabindex-talentindex-classid)
|
||||
@@ -514,6 +537,7 @@ build instructions.
|
||||
- [`UnitStandState(unit)`](#unitstandstateunit)
|
||||
- [`UnitInRange(unit)`](#unitinrangeunit)
|
||||
- [`UnitDistanceSquared(unit)`](#unitdistancesquaredunit)
|
||||
- [`UnitPosition(unit)`](#unitpositionunit)
|
||||
- [`UnitInLineOfSight(unit)`](#unitinlineofsightunit)
|
||||
- [`ClosestUnitPosition(creatureID)`](#closestunitpositioncreatureid)
|
||||
- [`UnitHealthMissing(unit)`](#unithealthmissingunit)
|
||||
@@ -1058,6 +1082,80 @@ Sparse class IDs (vanilla skips classID 6 — Death Knight didn't
|
||||
exist yet — and a few others) have NULL records and are silently
|
||||
skipped.
|
||||
|
||||
## ColorUtil
|
||||
|
||||
The modern `C_ColorUtil` color-space and text-color-code helpers. All are
|
||||
pure functions (no engine state); conventions match a retail-family client
|
||||
that ships them natively, verified in-game.
|
||||
|
||||
**Ranges:** hue is in **degrees**, `[0, 360)`; saturation / value / lightness
|
||||
are `[0, 1]`. RGB channels are `[0, 1]` (not 0–255). Achromatic (gray) inputs
|
||||
return a hue of **`-1`** — a sentinel, not `0` — which the reverse conversions
|
||||
accept as "no hue" (equivalent to `saturation == 0`).
|
||||
|
||||
The `color` argument is a table with `r`, `g`, `b` fields in `[0, 1]`
|
||||
(`ColorMixin`-shaped). An optional `a` is honored (default `1`), though the
|
||||
color code always encodes 8 hex digits with alpha.
|
||||
|
||||
### `C_ColorUtil.ConvertRGBToHSV(r, g, b)`
|
||||
|
||||
Returns `(h, s, v)`. Achromatic input → `h == -1`.
|
||||
|
||||
```lua
|
||||
C_ColorUtil.ConvertRGBToHSV(0, 1, 0) -- 120, 1, 1 (pure green)
|
||||
C_ColorUtil.ConvertRGBToHSV(0.5, 0.5, 0.5) -- -1, 0, 0.5 (gray)
|
||||
```
|
||||
|
||||
### `C_ColorUtil.ConvertHSVToRGB(h, s, v)`
|
||||
|
||||
Returns `(r, g, b)`. A negative hue (or `s == 0`) is treated as achromatic
|
||||
and yields `r == g == b == v`.
|
||||
|
||||
```lua
|
||||
C_ColorUtil.ConvertHSVToRGB(120, 1, 1) -- 0, 1, 0
|
||||
```
|
||||
|
||||
### `C_ColorUtil.ConvertHSVToHSL(h, s, v)`
|
||||
|
||||
Returns `(h, s, l)`. Hue passes through unchanged.
|
||||
|
||||
### `C_ColorUtil.ConvertHSLToHSV(h, s, l)`
|
||||
|
||||
Returns `(h, s, v)`. Hue passes through unchanged.
|
||||
|
||||
### `C_ColorUtil.ConvertHSLToRGB(h, s, l)`
|
||||
|
||||
Returns `(r, g, b)`. A negative hue (or `s == 0`) is achromatic → `r == g ==
|
||||
b == l`.
|
||||
|
||||
### `C_ColorUtil.GenerateTextColorCode(color)`
|
||||
|
||||
Returns the bare 8-hex color string `"AARRGGBB"` (alpha first), **not**
|
||||
including the `|c` escape.
|
||||
|
||||
```lua
|
||||
C_ColorUtil.GenerateTextColorCode({r = 1, g = 0, b = 0}) -- "ffff0000"
|
||||
```
|
||||
|
||||
### `C_ColorUtil.WrapTextInColor(text, color)`
|
||||
|
||||
Wraps `text` in the color's escape sequence: `"|c" .. AARRGGBB .. text ..
|
||||
"|r"`.
|
||||
|
||||
```lua
|
||||
C_ColorUtil.WrapTextInColor("Hi", {r = 1, g = 0, b = 0}) -- "|cffff0000Hi|r"
|
||||
```
|
||||
|
||||
### `C_ColorUtil.WrapTextInColorCode(text, colorCode)`
|
||||
|
||||
Wraps `text` in the given bare color code: `"|c" .. colorCode .. text ..
|
||||
"|r"`. Pair with `GenerateTextColorCode` (which produces `colorCode`).
|
||||
|
||||
```lua
|
||||
local code = C_ColorUtil.GenerateTextColorCode({r = 1, g = 0, b = 0})
|
||||
C_ColorUtil.WrapTextInColorCode("Hi", code) -- "|cffff0000Hi|r"
|
||||
```
|
||||
|
||||
## Combat
|
||||
|
||||
### `InCombatLockdown()`
|
||||
@@ -3504,6 +3602,52 @@ The modern combined setter/getter for `SetWidth`+`SetHeight` /
|
||||
textures (including engine-created ones like
|
||||
`button:GetNormalTexture()`), fontstrings.
|
||||
|
||||
### `region:IsMouseOver([topOffset, bottomOffset, leftOffset, rightOffset])`
|
||||
|
||||
`true` when the mouse cursor is within the region's rectangle, `false`
|
||||
otherwise. The four optional offsets shift the corresponding edge before
|
||||
the test (added to that edge, matching modern semantics: positive
|
||||
`topOffset` / `rightOffset` and negative `bottomOffset` / `leftOffset`
|
||||
enlarge the hit area). Registered on the Region base, so it works on any
|
||||
region type — frames, buttons, textures, fontstrings.
|
||||
|
||||
Uses the engine's own rect getters and cursor position, dividing the
|
||||
cursor by the region's effective scale — the exact computation vanilla
|
||||
addons have long open-coded as `MouseIsOver()`. Returns `false` for a
|
||||
region with no resolved rect (unpositioned / zero-size).
|
||||
|
||||
```lua
|
||||
ItemRefTooltip:IsMouseOver() -- true while hovering the tooltip
|
||||
Minimap:IsMouseOver(20, -20, -20, 20) -- true within a 20px halo around it
|
||||
```
|
||||
|
||||
### `region:GetRect()`
|
||||
|
||||
Returns the region's `left, bottom, width, height` in one call (the
|
||||
modern combined form of `GetLeft`+`GetBottom`+`GetWidth`+`GetHeight`).
|
||||
Composed from the engine's own getters, so all UI-scale conversion is its
|
||||
code. On the Region base — works on any region type. Returns nothing for
|
||||
a region with no resolved rect (unpositioned), matching retail.
|
||||
|
||||
```lua
|
||||
UIParent:GetRect() -- 0, 0, <screenWidth>, <screenHeight>
|
||||
Minimap:GetRect() -- <left>, <bottom>, 140, 140
|
||||
```
|
||||
|
||||
### `region:IsDragging()`
|
||||
|
||||
`true` while the region is the frame currently being moved or resized by
|
||||
the mouse — i.e. after `StartMoving`/`StartSizing` and before
|
||||
`StopMovingOrSizing` — `false` otherwise. Reads the engine's single global
|
||||
drag target (the same slot `StopMovingOrSizing` checks to decide whether
|
||||
it owns the active drag). On the Region base; non-frame regions (textures,
|
||||
fontstrings) always report `false`, since only frames can be dragged.
|
||||
|
||||
```lua
|
||||
-- in a frame's OnUpdate while the user drags it:
|
||||
if self:IsDragging() then ... end
|
||||
```
|
||||
|
||||
### `frame:SetShown(shown)`
|
||||
|
||||
`Show()` if `shown` is truthy, `Hide()` otherwise. Registered for
|
||||
@@ -3552,6 +3696,22 @@ f:IsEventRegistered("PLAYER_LOGIN") -- true
|
||||
f:IsEventRegistered("PLAYER_LOGOUT") -- false
|
||||
```
|
||||
|
||||
### `frame:GetEffectiveAlpha()`
|
||||
|
||||
Returns the frame's **effective** alpha — its own `GetAlpha()` multiplied
|
||||
by every ancestor's, i.e. the opacity WoW actually composites at render
|
||||
time (fading `UIParent` fades all of its children). Vanilla exposes only
|
||||
`GetAlpha` (a frame's own alpha); this walks `self → parent → …` via the
|
||||
engine's own `GetAlpha`/`GetParent`, so it sees exactly what the engine
|
||||
does — including the 8-bit quantization of alpha (`SetAlpha(0.5)` stores
|
||||
`127/255 ≈ 0.498`, and effective alpha is the product of those).
|
||||
|
||||
```lua
|
||||
UIParent:GetEffectiveAlpha() -- 1
|
||||
-- with UIParent at 0.5 and Minimap's own alpha 1:
|
||||
Minimap:GetEffectiveAlpha() -- ~0.498 (0.5 truncates to 127/255)
|
||||
```
|
||||
|
||||
## FriendList
|
||||
|
||||
### `C_FriendList.SendWhoQueryByName(name)`
|
||||
@@ -4158,18 +4318,27 @@ GameTooltip:SetBagItem(0, 1)
|
||||
if GameTooltip:IsEquippedItem() then ... end -- false unless that item is also worn
|
||||
```
|
||||
|
||||
### `OnTooltipSetItem` script
|
||||
### `OnTooltipSet*` scripts
|
||||
|
||||
A real frame script — settable with the standard `SetScript` / `GetScript` /
|
||||
`HookScript` — that fires whenever a tooltip's **item** is set. Backports the
|
||||
modern tooltip script so addons annotate item tooltips by hooking one script
|
||||
instead of wrapping every `Set*` method.
|
||||
|
||||
Fires for every item-setting path (`SetBagItem`, `SetInventoryItem`,
|
||||
`SetHyperlink` for an `item:` link, `SetMerchantItem`, `SetAuctionItem`,
|
||||
`SetItemByID`, …). Available on all GameTooltip-type frames (`GameTooltip`,
|
||||
`OnTooltipSetItem` / `OnTooltipSetSpell` / `OnTooltipSetUnit` /
|
||||
`OnTooltipSetGameObject` — real frame scripts, settable with the standard
|
||||
`SetScript` / `GetScript` /
|
||||
`HookScript` — that fire whenever a tooltip's **item**, **spell**, **unit**, or
|
||||
**gameobject** is set. Backport the modern tooltip scripts so addons annotate
|
||||
tooltips by hooking one script per object type instead of wrapping every
|
||||
`Set*` method. Available on all GameTooltip-type frames (`GameTooltip`,
|
||||
`ItemRefTooltip`, `ShoppingTooltip1/2`, `AtlasLootTooltip`, …).
|
||||
|
||||
| Script | Fires after any of |
|
||||
|---|---|
|
||||
| `OnTooltipSetItem` | `SetBagItem`, `SetInventoryItem`, `SetHyperlink` (`item:`), `SetMerchantItem`, `SetAuctionItem`, `SetItemByID`, … |
|
||||
| `OnTooltipSetSpell` | `SetSpell`, `SetSpellByID`, `SetTalent`, `SetShapeshift` |
|
||||
| `OnTooltipSetUnit` | `SetUnit` and unit mouseover |
|
||||
| `OnTooltipSetGameObject` | gameobject mouseover (herb/ore nodes, chests, mailboxes, signs) |
|
||||
|
||||
Vanilla only shipped `OnTooltipAddMoney` / `OnTooltipCleared` /
|
||||
`OnTooltipSetDefaultAnchor`; these four are the backported set.
|
||||
|
||||
The handler receives the tooltip as the **global `this`**, the 1.12
|
||||
frame-script convention — *not* a `self` argument (like every built-in vanilla
|
||||
script: `OnShow`, `OnEvent`, `OnTooltipCleared`, …). Modern-style
|
||||
@@ -4178,21 +4347,25 @@ script: `OnShow`, `OnEvent`, `OnTooltipCleared`, …). Modern-style
|
||||
```lua
|
||||
GameTooltip:HookScript("OnTooltipSetItem", function()
|
||||
local name, link, id = this:GetItem() -- the tooltip is the global `this`
|
||||
if id then
|
||||
this:AddLine("ID: " .. id, 0.6, 0.6, 0.6)
|
||||
end
|
||||
if id then this:AddLine("ID: " .. id, 0.6, 0.6, 0.6) end
|
||||
end)
|
||||
|
||||
GameTooltip:HookScript("OnTooltipSetUnit", function()
|
||||
local name, unit = this:GetUnit()
|
||||
if unit then this:AddLine(UnitClassBase(unit), 0.6, 0.6, 0.6) end
|
||||
end)
|
||||
```
|
||||
|
||||
Vanilla only shipped `OnTooltipAddMoney` / `OnTooltipCleared` /
|
||||
`OnTooltipSetDefaultAnchor`; `OnTooltipSetSpell` / `OnTooltipSetUnit` are not
|
||||
yet provided.
|
||||
**Auras don't fire `OnTooltipSetSpell`** — `SetUnitBuff` / `SetUnitDebuff` /
|
||||
`SetPlayerBuff` / `SetUnitAura` use a separate aura-tooltip builder, so hovering
|
||||
a buff/debuff does not trigger the spell script. This matches retail, where unit
|
||||
auras are a distinct tooltip data type rather than a spell.
|
||||
|
||||
**Caveat — the event fires from inside the item-tooltip build.** Lightweight
|
||||
handler work is fine: reading `this:GetItem()`, `this:AddLine(...)`, printing,
|
||||
etc. But avoid *re-entrant* tooltip rebuilds from the handler — e.g.
|
||||
`GameTooltip_ShowCompareItem()`, or a `Set*` call on another tooltip. Because
|
||||
the handler runs mid-build, re-entering the tooltip / FrameScript machinery can
|
||||
**Caveat — the event fires from inside the tooltip build.** Lightweight handler
|
||||
work is fine: reading `this:GetItem()` / `this:GetUnit()`, `this:AddLine(...)`,
|
||||
printing, etc. But avoid *re-entrant* tooltip rebuilds from the handler — e.g.
|
||||
`GameTooltip_ShowCompareItem()`, or a `Set*` call on another tooltip. Because the
|
||||
handler runs mid-build, re-entering the tooltip / FrameScript machinery can
|
||||
collide with other DLLs that hook the same Lua paths (nampower, SuperWoW,
|
||||
weirdutils, …) and crash the client. If you need heavy work like that, defer it
|
||||
to the next frame:
|
||||
@@ -4539,6 +4712,47 @@ not modern's `ComboPoints` reuse of the same number.
|
||||
local rage = UnitPower("player", Enum.PowerType.Rage)
|
||||
```
|
||||
|
||||
## Glue
|
||||
|
||||
Backports of two `C_Glue` session helpers. `C_Glue` is registered on the glue
|
||||
(login / character-select) Lua state; `IsOnGlueScreen` is *also* registered
|
||||
in-game, so code that runs in either environment can branch on it. Neither
|
||||
needs an engine flag — both answers fall out of which glue boot / Lua state
|
||||
is asking.
|
||||
|
||||
### `C_Glue.IsFirstLoadThisSession()`
|
||||
|
||||
Returns `true` only while the **first** glue screen since the process
|
||||
launched is showing, `false` on every later return to the glue screen. Login
|
||||
↔ character-select transitions stay within the first glue session (they never
|
||||
enter the world), so it stays `true` across them — it flips `false` only once
|
||||
you've entered the world and logged back out. Glue state only.
|
||||
|
||||
```lua
|
||||
if C_Glue.IsFirstLoadThisSession() then
|
||||
-- one-time-per-launch startup (intro cinematic, news, …)
|
||||
end
|
||||
```
|
||||
|
||||
Backed by a process-static count of glue boots (the glue script-registration
|
||||
pass fires once per boot: initial launch + each world→glue logout); boot 1 is
|
||||
the first load.
|
||||
|
||||
### `C_Glue.IsOnGlueScreen()`
|
||||
|
||||
Returns `true` when a GlueXML screen is showing (login or character select —
|
||||
no character in the world), `false` in the world. Registered on **both** Lua
|
||||
states, so it's callable from glue and in-game code alike. The two states are
|
||||
mutually exclusive (the glue state exists only when no character is in the
|
||||
world), so the answer is fixed per state — `true` from the glue registration,
|
||||
`false` from the in-game one.
|
||||
|
||||
```lua
|
||||
if C_Glue.IsOnGlueScreen() then
|
||||
-- at login / character select
|
||||
end
|
||||
```
|
||||
|
||||
## Gossip
|
||||
|
||||
Retail-shaped wrappers around vanilla's flat `GetGossipText` /
|
||||
@@ -7115,6 +7329,55 @@ undefined per the Lua reference manual.
|
||||
|
||||
Errors on non-table input.
|
||||
|
||||
### `table.count(tbl)`
|
||||
|
||||
Returns `(numTableNodes, numArrayNodes, maxArrayIndex)` describing how a table
|
||||
is populated (a modern WoW diagnostic, added retail 11.2.5). These are counts
|
||||
of live entries, **not** the table's allocated capacity:
|
||||
|
||||
- **`numTableNodes`** — total number of key/value pairs.
|
||||
- **`numArrayNodes`** — how many of those have an integer key in the range
|
||||
`[1..numTableNodes]` (the "contiguous array part" heuristic).
|
||||
- **`maxArrayIndex`** — the largest positive integral key (`>= 1`), or `0` if
|
||||
there is none. Negative/zero integral keys don't count.
|
||||
|
||||
```lua
|
||||
table.count({ 10, 20, 30 }) -- 3, 3, 3
|
||||
table.count({ a = 1, b = 2 }) -- 2, 0, 0
|
||||
table.count({ [1] = "x", foo = "y" }) -- 2, 1, 1
|
||||
table.count({ [100] = "x" }) -- 1, 0, 100
|
||||
table.count({ [-3] = "x" }) -- 1, 0, 0
|
||||
```
|
||||
|
||||
Pure iteration over the table (`lua_next`), so no dependency on Lua's internal
|
||||
storage layout. Errors on non-table input.
|
||||
|
||||
### `Mixin(object, ...)` / `CreateFromMixins(...)`
|
||||
|
||||
The FrameXML table-mixin primitives, provided as engine C functions.
|
||||
|
||||
- **`Mixin(object, ...)`** — shallow-copies every key/value from each table
|
||||
argument onto `object` (later arguments win on key collisions) and returns
|
||||
`object`. Non-table arguments are skipped.
|
||||
- **`CreateFromMixins(...)`** — `Mixin({}, ...)`: returns a new table seeded
|
||||
from the given mixins.
|
||||
|
||||
```lua
|
||||
local Greeter = {}
|
||||
function Greeter:Hello() return "hi, "..self.name end
|
||||
local obj = CreateFromMixins(Greeter)
|
||||
obj.name = "Bob"
|
||||
obj:Hello() -- "hi, Bob"
|
||||
```
|
||||
|
||||
Modern WoW provides these as C functions in `TableUtil`; ClassicAPI does the
|
||||
same (rather than leaving them in the `!!!ClassicAPI` addon) so they exist
|
||||
whenever the DLL is injected — even if the addon is disabled. The composite
|
||||
`CreateAndInitFromMixin(mixin, ...)` (which calls a `:Init` method) remains a
|
||||
Lua helper in the addon, mirroring retail's `Mixin.lua`. The taint-based
|
||||
`SecureMixin` family is intentionally omitted — vanilla 1.12 has no execution
|
||||
taint system, so there is nothing for it to guard.
|
||||
|
||||
### `string.match` / `string.gmatch`
|
||||
|
||||
The Lua 5.1 pattern helpers, backported to 1.12's Lua 5.0 (which ships only
|
||||
@@ -9181,40 +9444,6 @@ 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
|
||||
@@ -9523,6 +9752,41 @@ column is exactly what this function returns):
|
||||
> `14` (incapacitated) in 1.12 — the `30` ("sapped") value used by some
|
||||
> addon tables is a later-expansion addition and has no row here.
|
||||
|
||||
### `C_Spell.GetSpellEffectMechanics(spellID)`
|
||||
|
||||
Returns the spell's three per-effect `SpellMechanic` ids (`Spell.dbc`
|
||||
`EffectMechanic[3]`) as a 1-based array table, or `nil` for an invalid /
|
||||
out-of-range spell ID. Each entry uses the same numbering as
|
||||
[`GetSpellMechanicByID`](#c_spellgetspellmechanicbyidspellid) (`0` = that
|
||||
effect carries no mechanic).
|
||||
|
||||
```lua
|
||||
C_Spell.GetSpellEffectMechanics(1822) -- Rake → { 0, 15, 0 }
|
||||
C_Spell.GetSpellEffectMechanics(703) -- Garrote → { 0, 0, 0 }
|
||||
C_Spell.GetSpellEffectMechanics(133) -- Fireball → { 0, 0, 0 }
|
||||
```
|
||||
|
||||
Complements [`GetSpellMechanicByID`](#c_spellgetspellmechanicbyidspellid),
|
||||
which reads only the **spell-level** `Mechanic` field (`+0x14`). Vanilla
|
||||
frequently stores a spell's mechanic on an **effect** instead — periodic
|
||||
damage such as bleeds is the common case. Garrote/Rupture/Rend/Rip tag
|
||||
`bleeding` (15) at the spell level, but **Rake** has spell-level `0` and
|
||||
`EffectMechanic[2] = 15`, so effect-mechanic-aware callers (e.g. bleed
|
||||
classification for immunity tracking) need this array:
|
||||
|
||||
```lua
|
||||
local function IsBleed(spellID)
|
||||
if C_Spell.GetSpellMechanicByID(spellID) == 15 then return true end
|
||||
local em = C_Spell.GetSpellEffectMechanics(spellID)
|
||||
if em then for i = 1, 3 do if em[i] == 15 then return true end end end
|
||||
return false
|
||||
end
|
||||
```
|
||||
|
||||
Reads `Spell.dbc` directly (`EffectMechanic[3]` at `+0x13C`), so it covers
|
||||
every spell the client knows — not just the player's spellbook — with no
|
||||
caching or network round-trip.
|
||||
|
||||
### `C_Spell.GetSpellRadius(spellID)` / `GetSpellRadius(slot, bookType)`
|
||||
|
||||
Returns `(baseRadius, modifiedRadius)` — the spell's AOE radius in yards,
|
||||
@@ -9703,6 +9967,23 @@ Reads a single bit from the engine's spell-knowledge bitmap at
|
||||
The same lookup the engine itself does internally. No spellbook walk,
|
||||
no talent walk, no profession-window dependency.
|
||||
|
||||
### `CanDualWield()`
|
||||
|
||||
Returns `true` if the player can equip a weapon in the off hand.
|
||||
|
||||
```lua
|
||||
if CanDualWield() then ... end -- true for a warrior/rogue past level 10, etc.
|
||||
```
|
||||
|
||||
Server-side this is a plain flag (`Player::m_canDualWield`) that starts
|
||||
`false` and is turned on **only** by the `SPELL_EFFECT_DUAL_WIELD` spell
|
||||
effect — no class has it innately. Exactly one spell in the 1.12 client's
|
||||
`Spell.dbc` carries that effect: `674` ("Dual Wield"), the trained
|
||||
warrior / rogue / hunter passive, which lands in the known-spell bitmap
|
||||
like any learned spell. So `CanDualWield()` is precisely
|
||||
`IsPlayerSpell(674)` — there is no separate client-visible capability
|
||||
field to read.
|
||||
|
||||
### `IsSpellKnown(spellID, [isPet])`
|
||||
|
||||
Returns `true` if the given spellID is currently in the player's (or
|
||||
@@ -10305,34 +10586,47 @@ the item-use path for grenades / on-use ground-target items.
|
||||
|
||||
### `C_Spell.CastAtUnit(spellIDOrName, unit)`
|
||||
|
||||
Casts a ground-target spell at `unit`'s feet, bypassing the AoE
|
||||
reticle click. ClassicAPI's analog of modern's
|
||||
`/cast [@unit] Flamestrike` — `CastAtUnit(spell, "player")` drops it
|
||||
at the player's position, `"target"` at the target's, and so on for
|
||||
any unit token (`"mouseover"`, `"party1"`, `"raid7"`, …). Returns
|
||||
`true` when the placement landed at the unit; `false` for
|
||||
non-ground-target spells (cast still fires normally), unknown
|
||||
spells, or a unit with no resolvable position.
|
||||
Casts a spell **at `unit`, whatever its target type** — ClassicAPI's
|
||||
analog of modern's `/cast [@unit] Spell`:
|
||||
|
||||
- **Ground-target spells** (Flamestrike, Blizzard, Rain of Fire, …) are
|
||||
placed at the unit's feet, bypassing the AoE reticle click.
|
||||
- **Unit-target / normal spells** (Frostbolt, Renew, a buff, …) fire
|
||||
directly on the unit — regardless of your current selection, so you can
|
||||
buff `"party1"` or heal `"player"` while an enemy stays targeted.
|
||||
|
||||
Works with any unit token (`"target"`, `"player"`, `"mouseover"`,
|
||||
`"party1"`, `"raid7"`, …). Returns `true` when the spell was cast at the
|
||||
unit (fired or placed); `false` for a spell not in your spellbook, a unit
|
||||
with no resolvable position, or a unit the engine won't accept as a target
|
||||
for that spell (wrong faction, out of range).
|
||||
|
||||
The first argument takes a spellID or a spell name, with the same
|
||||
exact-rank / `(Rank N)` semantics as `CastAtCursor`.
|
||||
|
||||
```lua
|
||||
-- ground-target: dropped at the unit's feet
|
||||
C_Spell.CastAtUnit(2120, "target") -- Flamestrike at the target's feet
|
||||
C_Spell.CastAtUnit(10, "player") -- Blizzard Rank 1 on yourself
|
||||
C_Spell.CastAtUnit("Blizzard(Rank 6)", "target")
|
||||
|
||||
-- unit-target / normal: cast straight on the unit, ignoring current target
|
||||
C_Spell.CastAtUnit("Frostbolt", "target")
|
||||
C_Spell.CastAtUnit("Renew", "player") -- heal yourself mid-fight
|
||||
C_Spell.CastAtUnit("Power Word: Fortitude", "party1")
|
||||
```
|
||||
|
||||
Same two-stage shape as
|
||||
[`C_Spell.CastAtCursor`](#c_spellcastatcursorspellidorname), but instead of
|
||||
the cursor raycast, step 2 reads the unit's world position from its
|
||||
`GetPosition` virtual (the same one `CheckInteractDistance` /
|
||||
`UnitInRange` use) and commits placement there via
|
||||
`FUN_COMMIT_PLACEMENT_COORDS`. The unit is resolved *before* the cast
|
||||
is dispatched, so an absent/invalid unit fails cleanly without
|
||||
leaving the engine armed in placement mode. A genuinely unrecognized
|
||||
unit-token string raises the engine's standard "Unknown unit" error,
|
||||
matching `UnitHealth("garbage")`.
|
||||
The unit is resolved once to its object; the cast is dispatched with the
|
||||
unit's **GUID** as the implicit target (the engine substitutes it exactly
|
||||
where it would the current selection), so a unit-target spell fires
|
||||
immediately at that unit. A ground spell ignores the GUID and enters
|
||||
placement mode instead, which is then committed at the unit's world
|
||||
position (read from its `GetPosition` virtual — the same one
|
||||
`CheckInteractDistance` / `UnitInRange` use) via
|
||||
`FUN_COMMIT_PLACEMENT_COORDS`. The engine can't be left armed in placement
|
||||
mode on failure: if no placement is active after dispatch the cast already
|
||||
went out, and a non-ground placement (an unaccepted unit target) is
|
||||
cancelled. A genuinely unrecognized unit-token string raises the engine's
|
||||
standard "Unknown unit" error, matching `UnitHealth("garbage")`.
|
||||
|
||||
The companion item version is
|
||||
[`C_Item.UseAtUnit`](#c_itemuseatunititeminfo-unit).
|
||||
@@ -11019,6 +11313,78 @@ Mirrors 3.3.5a's `Script_CancelShapeshiftForm` inner (`FUN_00726CE0`),
|
||||
which does the same effect-array scan + form-id match before issuing
|
||||
its cancel packet. Vanilla just lacks the public Lua surface.
|
||||
|
||||
## System
|
||||
|
||||
Host/OS-level helpers that aren't tied to a game domain.
|
||||
|
||||
### `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.
|
||||
|
||||
### `CopyToClipboard(text [, removeMarkup])`
|
||||
|
||||
Copies `text` to the Windows clipboard and returns the number of bytes
|
||||
copied. Vanilla has no clipboard access at all, so this is a ClassicAPI
|
||||
addition backed by the Win32 clipboard API. Available on both the in-game
|
||||
and login/character-select screens.
|
||||
|
||||
- `text` — the string to copy (numbers are coerced, like Lua does).
|
||||
- `removeMarkup` — optional; when truthy, strip WoW UI escape sequences
|
||||
before copying so the clipboard gets plain text:
|
||||
- `|cAARRGGBB … |r` color codes → removed
|
||||
- `|H…|h text |h` hyperlinks → keeps only the visible text
|
||||
- `|T…|t` textures → removed
|
||||
- `|n` → newline, `||` → literal `|`
|
||||
- unknown `|x` sequences are left verbatim, so real text that merely
|
||||
contains a pipe survives
|
||||
- returns the byte length of the copied (possibly stripped) string —
|
||||
consistent with Lua's `#s`. `0` for an empty string or if the clipboard
|
||||
couldn't be opened.
|
||||
|
||||
```lua
|
||||
CopyToClipboard("hello world") -- 11
|
||||
local link = "\124cffff0000Red\124r" -- real single-pipe markup
|
||||
CopyToClipboard(link, true) -- 3 (clipboard gets "Red")
|
||||
CopyToClipboard(link) -- 15 (raw escapes copied)
|
||||
```
|
||||
|
||||
> The string is treated as UTF-8 (matching the credential and TTS modules)
|
||||
> and written as `CF_UNICODETEXT`; Windows synthesizes `CF_TEXT` for legacy
|
||||
> consumers.
|
||||
>
|
||||
> Note when testing from the chat box: the edit box escapes every typed `|`
|
||||
> into `||`, so a literal `|cff…` you type is no longer a color code by the
|
||||
> time it reaches Lua. Build real markup with `\124` (as above) to exercise
|
||||
> `removeMarkup`.
|
||||
|
||||
## Talent
|
||||
|
||||
### `GetTalentSpellID(tabIndex, talentIndex, [rank[, classID]])`
|
||||
@@ -12243,17 +12609,58 @@ human.
|
||||
|
||||
Reads world positions via the `CGObject::GetPosition` vtable virtual
|
||||
(slot 5, offset `+0x14`) — the same path `UnitInRange` /
|
||||
`CheckInteractDistance` use. **Center-to-center**, not reach-aware (that
|
||||
edge-to-edge nicety is UnitXP_SP3's niche); the value equals what you'd
|
||||
compute from SuperWoW's `UnitPosition`, but it's self-contained (no
|
||||
sibling-DLL dependency, and no clash with SuperWoW's own `UnitPosition`,
|
||||
which is left untouched).
|
||||
`CheckInteractDistance` and our own [`UnitPosition`](#unitpositionunit)
|
||||
use. **Center-to-center**, not reach-aware (that edge-to-edge nicety is
|
||||
UnitXP_SP3's niche); the value equals what you'd compute from the raw
|
||||
`UnitPosition` coordinates, and it's self-contained (no sibling-DLL
|
||||
dependency).
|
||||
|
||||
> No self-quirk (unlike `UnitInRange`): `UnitDistanceSquared("player")`
|
||||
> returns a legitimate `(0, true)`. Because a real `0` (self, or two
|
||||
> exactly co-located units) is indistinguishable by value from the miss
|
||||
> placeholder, always branch on `checkedPosition`.
|
||||
|
||||
### `UnitPosition(unit)`
|
||||
|
||||
Returns `posY, posX, posZ, instanceID` — the unit's world position, in
|
||||
the modern (WoD+) `UnitPosition` shape. `nil` when the unit has no known
|
||||
position.
|
||||
|
||||
```lua
|
||||
local posY, posX, posZ, instanceID = UnitPosition("player")
|
||||
```
|
||||
|
||||
| Return | Meaning |
|
||||
|--------|---------|
|
||||
| `posY` | World Y coordinate (the west axis). |
|
||||
| `posX` | World X coordinate (the north axis). |
|
||||
| `posZ` | World Z coordinate (height). |
|
||||
| `instanceID` | Currently-loaded map id (`Map.dbc` row — e.g. `0` Eastern Kingdoms, `1` Kalimdor). Every visible unit shares the player's instance. |
|
||||
|
||||
**Order matches retail** — `posY` (west) first, then `posX` (north), then
|
||||
`posZ`, mirroring Blizzard's own `UnitPosition`. So an addon written for
|
||||
retail that does `local py, px = UnitPosition(u)` gets its coordinates
|
||||
labelled as expected. (World X is north/south, Y is west/east — WoW's
|
||||
long-standing convention.)
|
||||
|
||||
**Not group-restricted.** Retail returns `nil` for units outside your
|
||||
party/raid (a privacy guard that doesn't exist in 1.12). This backport
|
||||
reads any *visible* unit — `"target"`, `"mouseover"`, nameplate tokens,
|
||||
arbitrary party/raid members in sync range — so it's strictly more
|
||||
permissive, closer to SuperWoW's unit-position access. Units the client
|
||||
can't currently see (out of range, another zone) have no position → `nil`.
|
||||
|
||||
Reads through the same `CGObject::GetPosition` virtual as
|
||||
[`UnitDistanceSquared`](#unitdistancesquaredunit); the two are consistent
|
||||
(the squared delta of two `UnitPosition` reads equals
|
||||
`UnitDistanceSquared`).
|
||||
|
||||
> **SuperWoW interaction.** SuperWoW also defines a global `UnitPosition`
|
||||
> with a different return shape. Both register on the same Lua state, so
|
||||
> if SuperWoW is loaded the last registrant wins — install order decides
|
||||
> which shape is live. Without SuperWoW, this retail-shaped version is the
|
||||
> one you get.
|
||||
|
||||
### `UnitInLineOfSight(unit)`
|
||||
|
||||
Returns `true` if the player has clear line of sight to `unit`, `false`
|
||||
|
||||
@@ -140,6 +140,23 @@ enum Offsets {
|
||||
// and call the inner invoker, which is self-contained.
|
||||
FUN_GAMETOOLTIP_SCRIPT_RESOLVER = 0x005295D0,
|
||||
FUN_FRAME_INVOKE_SCRIPT = 0x00704D50,
|
||||
// The other per-object tooltip builders, co-hooked the same way as
|
||||
// FUN_GAMETOOLTIP_BUILD_ITEM to back OnTooltipSetSpell / OnTooltipSetUnit /
|
||||
// OnTooltipSetGameObject (see Tooltip::SetEvents). Each is the single funnel
|
||||
// its Set*/mouseover paths converge on and clears the tooltip
|
||||
// (FUN_00530050) before repopulating, so firing after it covers every way
|
||||
// that object type gets set:
|
||||
// - Spell FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP (0x0052E610, 7 stack args,
|
||||
// RET 0x1c) — SetSpell/SetSpellByID/SetTalent/SetShapeshift funnel.
|
||||
// - Unit (0x00529fe0, __thiscall(self, guid*), RET 4) — SetUnit +
|
||||
// the two engine mouseover paths (FUN_004919d0 / FUN_00492890).
|
||||
// Returns an int the caller (Script_GameTooltip_SetUnit) tests, so the
|
||||
// co-hook must forward the original's return value.
|
||||
// - GameObject (0x0052aa20, __thiscall(self, guid*), RET 4) — the GO
|
||||
// hover populator; resolves the GO (OBJ_TYPE_GAMEOBJECT) and writes the
|
||||
// GO field at +0x370.
|
||||
FUN_GAMETOOLTIP_BUILD_UNIT = 0x00529FE0,
|
||||
FUN_GAMETOOLTIP_BUILD_GAMEOBJECT = 0x0052AA20,
|
||||
FUN_GAMETOOLTIP_ADD_LINE = 0x00530270, // __thiscall(self, left, right, lColorBGRA*, rColorBGRA*, wrap)
|
||||
OFF_GAMETOOLTIP_NUM_LINES = 0x31C, // int — live line count (AddLine index; +0x320 is the cap)
|
||||
// The displayed item's identity is read via the existing
|
||||
@@ -1297,12 +1314,42 @@ enum Offsets {
|
||||
FUN_SCRIPT_REGION_SETHEIGHT = 0x007A2150,
|
||||
FUN_SCRIPT_REGION_GETWIDTH = 0x007A1E00,
|
||||
FUN_SCRIPT_REGION_GETHEIGHT = 0x007A2030,
|
||||
// Region rect getters (return a number, or nil when the region has no
|
||||
// resolved rect). All on the REGION base registry → callable on any
|
||||
// region type. Used by Frame::Modern's IsMouseOver.
|
||||
FUN_SCRIPT_REGION_GETLEFT = 0x007A1980,
|
||||
FUN_SCRIPT_REGION_GETRIGHT = 0x007A1AA0,
|
||||
FUN_SCRIPT_REGION_GETTOP = 0x007A1BC0,
|
||||
FUN_SCRIPT_REGION_GETBOTTOM = 0x007A1CE0,
|
||||
// In-game GetCursorPosition (0x0046DAD0 is the glue-state copy). Pushes
|
||||
// (x, y) in the same k = FUN_0041ad70()*DAT_007ffd68 space the region
|
||||
// rect getters use — so dividing by the region's effective scale puts
|
||||
// the cursor in the region's logical coordinate space.
|
||||
FUN_SCRIPT_GETCURSORPOSITION = 0x0048B820,
|
||||
// Region effective-scale field: `piVar[0x1f]` (float) read by both
|
||||
// GetEffectiveScale (0x00774FC0) and internally by GetLeft/Top/etc. —
|
||||
// present on every region object, so it's the all-region way to get the
|
||||
// effective scale without the Frame-only GetEffectiveScale method.
|
||||
OFF_REGION_EFFECTIVE_SCALE = 0x7C,
|
||||
|
||||
// Global UI/screen context pointer (every frame's +0xA0 field mirrors
|
||||
// it; GetCursorPosition reads the cursor from *ptr + 0x1118). Its
|
||||
// +0xCFC slot holds the frame currently being moved/sized via
|
||||
// StartMoving/StartSizing (0 when idle) — the drag target. Backs
|
||||
// Frame::Modern's IsDragging: StopMovingOrSizing (0x00776990) gates its
|
||||
// clear on `*(context+0xCFC) == self`, i.e. the IsDragging predicate.
|
||||
VAR_UI_CONTEXT_PTR = 0x00CF0BD8,
|
||||
OFF_UI_CONTEXT_DRAG_TARGET = 0xCFC,
|
||||
FUN_SCRIPT_FRAME_SHOW = 0x00775750,
|
||||
FUN_SCRIPT_FRAME_HIDE = 0x00775810,
|
||||
FUN_SCRIPT_FRAME_SETMINRESIZE = 0x00776020,
|
||||
FUN_SCRIPT_FRAME_SETMAXRESIZE = 0x007762A0,
|
||||
FUN_SCRIPT_FRAME_GETSCRIPT = 0x00774780,
|
||||
FUN_SCRIPT_FRAME_SETSCRIPT = 0x007748D0,
|
||||
// Frame GetAlpha (own alpha, 0..1) + Region GetParent — walked by
|
||||
// Frame::Modern's GetEffectiveAlpha up the parent chain.
|
||||
FUN_SCRIPT_FRAME_GETALPHA = 0x00774DC0,
|
||||
FUN_SCRIPT_REGION_GETPARENT = 0x007A1460,
|
||||
FUN_SCRIPT_TEXTURE_SHOW = 0x0079B770,
|
||||
FUN_SCRIPT_TEXTURE_HIDE = 0x0079B830,
|
||||
FUN_SCRIPT_FONTSTRING_SHOW = 0x0079CDB0,
|
||||
@@ -5321,6 +5368,12 @@ enum Offsets {
|
||||
// (form ID for shapeshift, channel index for periodic, etc.).
|
||||
OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME = 0x16C, // int32[3]
|
||||
OFF_SPELL_RECORD_EFFECT_MISC_VALUE = 0x1A8, // int32[3]
|
||||
// Per-effect SpellMechanic ids (EffectMechanic[3]). One int32[3] (0xC)
|
||||
// after EffectBasePoints (0x130); cross-checked against EffectRadiusIndex
|
||||
// (0x160) and EffectApplyAuraName (0x16C). Vanilla often stores a spell's
|
||||
// mechanic on an effect rather than the spell-level Mechanic (0x14), so
|
||||
// bleed / DoT detection needs this array in addition to that field.
|
||||
OFF_SPELL_RECORD_EFFECT_MECHANIC = 0x13C, // int32[3]
|
||||
SPELL_RECORD_EFFECT_COUNT = 3,
|
||||
|
||||
// SPELL_EFFECT_* id array (`Effect[3]`) — five arrays before
|
||||
|
||||
@@ -140,6 +140,22 @@ double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
const uint32_t expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs == 0)
|
||||
return 0.0;
|
||||
// Report "no timer" once the stored expiration has elapsed — this is not a
|
||||
// heuristic, it's exactly what the engine's own GetPlayerBuffTimeLeft does.
|
||||
// Script_GetPlayerBuffTimeLeft (0x004e4930) → FUN_004e4450 reads this same
|
||||
// expiration table and returns:
|
||||
// if ((int)(now - expiration[slot]) >= 0) return 0; // elapsed -> 0
|
||||
// return expiration[slot] - now; // else remaining
|
||||
// Permanent auras (paladin auras, passives, racials) carry an infinite
|
||||
// Spell.dbc duration (SpellDuration base -1); the server stores the fixed
|
||||
// cast-time in this slot for them, so it's always "already elapsed" and the
|
||||
// engine shows no countdown. The signed `(int)` compare also absorbs the
|
||||
// ~24.86-day GetTickCount wrap (both operands wrap together).
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
const uint32_t nowMs = reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
if (static_cast<int32_t>(nowMs - expirationMs) >= 0)
|
||||
return 0.0; // expiration reached/passed → permanent or no timer
|
||||
return static_cast<double>(expirationMs) * 0.001;
|
||||
}
|
||||
|
||||
@@ -677,6 +693,101 @@ int GroupMemberLevel(uint64_t guid) {
|
||||
return Group::MemberStats::Lookup(guid).level;
|
||||
}
|
||||
|
||||
// True if the group array exposes ANY visible aura (either kind). When it
|
||||
// does, the server's out-of-range aura data is present and authoritative, so
|
||||
// we do NOT supplement from the Aura::Source cache — mirroring how the
|
||||
// descriptor path treats a populated descriptor (DescriptorHasVisibleAura).
|
||||
// Only when the array is entirely empty do we fall 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.
|
||||
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)))
|
||||
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,
|
||||
bool playerOnly) {
|
||||
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 (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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,
|
||||
int oneBasedIndex, Filter filter, bool playerOnly) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
continue;
|
||||
if (++seen == oneBasedIndex) {
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1,
|
||||
GroupMemberLevel(guid), false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Append every cache-fallback aura of `filter`'s kind into the array table.
|
||||
void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
Filter filter, bool playerOnly, int outerIdx,
|
||||
int &nextKey) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1, level,
|
||||
false);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// Single cache-fallback aura matching `spellID` (when nonzero) or `spellName`
|
||||
// (when non-null), of the given kind. Backs the by-ID / by-name group queries.
|
||||
bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
bool harmful, uint32_t spellID,
|
||||
const char *spellName, bool playerOnly) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(guid, harmful, buf, kFallbackMax);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (spellID != 0 && buf[i].spellId != spellID)
|
||||
continue;
|
||||
if (spellName != nullptr) {
|
||||
const char *nm = LocalizedSpellName(SpellRecord(buf[i].spellId));
|
||||
if (nm == nullptr || std::strcmp(nm, spellName) != 0)
|
||||
continue;
|
||||
}
|
||||
if (!GroupFallbackEligible(arr, buf[i], playerOnly))
|
||||
continue;
|
||||
PushEnriched(L, guid, buf[i].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
@@ -686,6 +797,7 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return false;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
int start, end;
|
||||
GroupRange(filter, start, end);
|
||||
int matches = 0;
|
||||
@@ -699,6 +811,10 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter,
|
||||
playerOnly);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -709,6 +825,7 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return false;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
@@ -724,6 +841,17 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
GroupMemberLevel(guid), false);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, spellID, nullptr,
|
||||
playerOnly))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, spellID, nullptr,
|
||||
playerOnly))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -734,6 +862,7 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return false;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
@@ -753,6 +882,17 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
GroupMemberLevel(guid), false);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, 0, spellName,
|
||||
playerOnly))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, 0, spellName,
|
||||
playerOnly))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -763,6 +903,7 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
int start, end;
|
||||
GroupRange(filter, start, end);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
@@ -774,6 +915,10 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, bool playerOnly,
|
||||
filter == Filter::Helpful, 1, level, false);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, playerOnly, outerIdx,
|
||||
nextKey);
|
||||
}
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+157
-2
@@ -70,6 +70,49 @@ uint32_t SpellDurationMs(const uint8_t *spellRecord, bool casterIsPlayer) {
|
||||
return ms > 0 ? static_cast<uint32_t>(ms) : 0;
|
||||
}
|
||||
|
||||
// ---- Recent local-player aura casts --------------------------------------
|
||||
|
||||
// The aura-application hooks (OnAuraAdded / OnAuraStacksChanged) get no caster,
|
||||
// so they compute the UNMODIFIED base duration (`skipMod = 1`). That's correct
|
||||
// for other units' auras, but wrong for one the LOCAL PLAYER just cast: it
|
||||
// should carry the player-talented duration (Improved Shadow Word: Pain → 24s
|
||||
// vs the 18s base). SpellGo normally owns that entry with the talented value,
|
||||
// but when it can't attribute the cast to this target — an empty SMSG_SPELL_GO
|
||||
// hit list files it under the caster instead, or a slot reshuffle evicts and
|
||||
// refills the entry after SpellGo ran — the base value is what's left, so the
|
||||
// duration flickers 24 → 18. SpellGo (which knows caster == player) records the
|
||||
// spellId here so the application hooks can recover the player-modified
|
||||
// duration for it.
|
||||
constexpr int kRecentCastCount = 16;
|
||||
constexpr uint32_t kRecentCastTtlMs = 1500;
|
||||
struct RecentCast {
|
||||
uint32_t spellId;
|
||||
uint32_t tMs; // 0 = empty
|
||||
};
|
||||
RecentCast g_recentCasts[kRecentCastCount];
|
||||
int g_recentCastCursor = 0;
|
||||
|
||||
void RememberPlayerCast(uint32_t spellId) {
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &r : g_recentCasts)
|
||||
if (r.spellId == spellId) {
|
||||
r.tMs = now;
|
||||
return;
|
||||
}
|
||||
g_recentCasts[g_recentCastCursor] = {spellId, now};
|
||||
g_recentCastCursor = (g_recentCastCursor + 1) % kRecentCastCount;
|
||||
}
|
||||
|
||||
bool WasRecentPlayerCast(uint32_t spellId) {
|
||||
if (spellId == 0)
|
||||
return false;
|
||||
const uint32_t now = NowMs();
|
||||
for (const auto &r : g_recentCasts)
|
||||
if (r.spellId == spellId && r.tMs != 0 && now - r.tMs <= kRecentCastTtlMs)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Cache ---------------------------------------------------------------
|
||||
|
||||
// Helpful/harmful classification, recorded from the aura's descriptor slot
|
||||
@@ -135,6 +178,45 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
true};
|
||||
}
|
||||
|
||||
// ---- Out-of-range group-member aura snapshots ---------------------------
|
||||
//
|
||||
// Transition state for `ObserveGroupAuras`: the last aura spell-ID set we saw
|
||||
// for each out-of-range group member, so we can stamp a duration guess only
|
||||
// when an aura genuinely appears (not for auras already present when we first
|
||||
// started watching that member). See the header for the rationale.
|
||||
|
||||
struct GroupSnapshot {
|
||||
uint64_t guid;
|
||||
uint32_t touchMs;
|
||||
bool used;
|
||||
uint16_t ids[Offsets::UNIT_AURA_TOTAL];
|
||||
};
|
||||
constexpr int kGroupSnapshots = 44; // MAX_RAID (40) + slack
|
||||
constexpr uint32_t kGroupSnapshotTtlMs = 30000; // forget members not polled for 30s
|
||||
GroupSnapshot g_groupSnaps[kGroupSnapshots];
|
||||
|
||||
bool SnapshotHasId(const GroupSnapshot &s, uint16_t id) {
|
||||
for (uint16_t v : s.ids)
|
||||
if (v == id)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stamp a base-duration expiration guess for a group aura that just appeared.
|
||||
// casterGuid stays 0 (unknown), so `Store`'s guard preserves any real SpellGo
|
||||
// timing we already hold. Skips non-aura spells and auras with no finite base
|
||||
// duration (nothing meaningful to guess — leave expiration unknown).
|
||||
void StampGroupGuess(uint64_t guid, uint16_t spellId, int8_t kind, uint32_t now) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
const uint32_t base = SpellDurationMs(rec, /*casterIsPlayer*/ false);
|
||||
if (base == 0)
|
||||
return;
|
||||
Store(guid, spellId, /*casterGuid*/ 0, now + base, base, /*fromCast*/ false,
|
||||
kind);
|
||||
}
|
||||
|
||||
// Evict the entry for an aura the engine reports gone. Keyed by (target,
|
||||
// spell) like the rest of the cache. Without this, the GetAuraDataByIndex
|
||||
// fallback would keep surfacing a dropped aura until its computed expiry —
|
||||
@@ -156,6 +238,10 @@ void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
void FlushAll() {
|
||||
for (auto &e : g_cache)
|
||||
e.used = false;
|
||||
// Reset transition baselines too: post-transition group auras re-sync and
|
||||
// should be treated as first-sight (unknown age), not diffed as new.
|
||||
for (auto &s : g_groupSnaps)
|
||||
s.used = false;
|
||||
}
|
||||
|
||||
// -1 = no map seen yet (never a valid Map.dbc id), so the first tick just
|
||||
@@ -191,6 +277,12 @@ void OnWorldTick() {
|
||||
if (e.used && e.expirationMs != 0 && now >= e.expirationMs)
|
||||
e.used = false;
|
||||
}
|
||||
// Forget snapshots for members we haven't polled recently (left the group,
|
||||
// or no longer displayed) so a later re-appearance re-baselines cleanly.
|
||||
for (auto &s : g_groupSnaps) {
|
||||
if (s.used && now - s.touchMs > kGroupSnapshotTtlMs)
|
||||
s.used = false;
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
@@ -252,6 +344,12 @@ void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
durationMs = SpellDurationMs(rec, casterIsPlayer);
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
|
||||
// Let the application hooks recover the player-talented duration for this
|
||||
// spell if they end up owning the target's entry (empty hit list / refill
|
||||
// race) — see WasRecentPlayerCast.
|
||||
if (casterIsPlayer)
|
||||
RememberPlayerCast(spellId);
|
||||
|
||||
if (numTargets == 0) {
|
||||
// No explicit hit list (self-cast with caster-implicit target).
|
||||
Store(caster, spellId, caster, expirationMs, durationMs, true,
|
||||
@@ -283,9 +381,15 @@ void StampApplication(void *unit, uint32_t spellId, int8_t kind) {
|
||||
const uint64_t unitGuid = Unit::Identity::GuidForObject(unit);
|
||||
if (unitGuid == 0)
|
||||
return;
|
||||
const uint32_t durationMs = SpellDurationMs(rec, /*casterIsPlayer*/ false);
|
||||
// If the local player just cast this aura, use its player-talented duration
|
||||
// (and attribute the caster) — SpellGo couldn't always land the talented
|
||||
// value on this target's entry. Otherwise base (we lack other casters'
|
||||
// mods). See WasRecentPlayerCast.
|
||||
const bool byPlayer = WasRecentPlayerCast(spellId);
|
||||
const uint32_t durationMs = SpellDurationMs(rec, byPlayer);
|
||||
const uint64_t caster = byPlayer ? Unit::Identity::PlayerGuid() : 0;
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
Store(unitGuid, spellId, /*casterGuid*/ 0, expirationMs, durationMs,
|
||||
Store(unitGuid, spellId, caster, expirationMs, durationMs,
|
||||
/*fromCast*/ false, kind);
|
||||
}
|
||||
|
||||
@@ -423,4 +527,55 @@ int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
|
||||
return n;
|
||||
}
|
||||
|
||||
void ObserveGroupAuras(uint64_t guid, const uint16_t *auraArray) {
|
||||
if (guid == 0 || auraArray == nullptr)
|
||||
return;
|
||||
const int total = Offsets::UNIT_AURA_TOTAL;
|
||||
const int buffCount = Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
const uint32_t now = NowMs();
|
||||
|
||||
GroupSnapshot *snap = nullptr;
|
||||
for (auto &s : g_groupSnaps)
|
||||
if (s.used && s.guid == guid) {
|
||||
snap = &s;
|
||||
break;
|
||||
}
|
||||
|
||||
if (snap == nullptr) {
|
||||
// First sight of this member — record a baseline without stamping;
|
||||
// their current auras have unknown age.
|
||||
GroupSnapshot *dst = nullptr;
|
||||
for (auto &s : g_groupSnaps)
|
||||
if (!s.used) {
|
||||
dst = &s;
|
||||
break;
|
||||
}
|
||||
if (dst == nullptr) { // full — evict the least-recently-polled
|
||||
dst = &g_groupSnaps[0];
|
||||
for (auto &s : g_groupSnaps)
|
||||
if (s.touchMs < dst->touchMs)
|
||||
dst = &s;
|
||||
}
|
||||
dst->guid = guid;
|
||||
dst->used = true;
|
||||
dst->touchMs = now;
|
||||
for (int i = 0; i < total; ++i)
|
||||
dst->ids[i] = auraArray[i];
|
||||
return;
|
||||
}
|
||||
|
||||
// Stamp a guess for every spell ID present now but absent from the prior
|
||||
// snapshot (a genuine appearance), then refresh the snapshot.
|
||||
for (int slot = 0; slot < total; ++slot) {
|
||||
const uint16_t id = auraArray[slot];
|
||||
if (id == 0 || SnapshotHasId(*snap, id))
|
||||
continue;
|
||||
StampGroupGuess(guid, id, slot < buffCount ? KIND_HELPFUL : KIND_HARMFUL,
|
||||
now);
|
||||
}
|
||||
for (int i = 0; i < total; ++i)
|
||||
snap->ids[i] = auraArray[i];
|
||||
snap->touchMs = now;
|
||||
}
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
@@ -74,4 +74,19 @@ void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count);
|
||||
// the aura is still active server-side: the cache survives descriptor clears.
|
||||
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut);
|
||||
|
||||
// Feeds the cache from an out-of-range group member's aura spell-ID array
|
||||
// (`Group::MemberStats::AuraArray` — 48 slots, buffs 0..BUFF_COUNT-1, debuffs
|
||||
// after). Such a member has no CGUnit and never yields SMSG_SPELL_GO, so the
|
||||
// SpellGo path never learns their auras' timing — but SMSG_PARTY_MEMBER_STATS
|
||||
// carries the spell IDs and the server sends it promptly when an aura is
|
||||
// added/removed. So a spell ID that newly *appears* here did so ~one server
|
||||
// tick + latency after the real application: we stamp a best-effort
|
||||
// `now + base duration` expiration (base only — no remote caster mods, no
|
||||
// caster recorded). Auras already present the first time we see a member have
|
||||
// unknown age and are NOT stamped; only genuine absent->present transitions
|
||||
// are. Idempotent within a frame; call it before enumerating the array so a
|
||||
// just-appeared aura carries its guess on the same poll. The Store guard keeps
|
||||
// any real SpellGo timing we already hold, so this never downgrades better data.
|
||||
void ObserveGroupAuras(uint64_t guid, const uint16_t *auraArray);
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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/>.
|
||||
|
||||
// `Mixin(object, ...)` and `CreateFromMixins(...)` — the FrameXML table-mixin
|
||||
// primitives. Modern WoW provides these as engine C functions (in TableUtil);
|
||||
// we do the same rather than leaving them in the !!!ClassicAPI addon so they
|
||||
// exist whenever the DLL is injected, even if the addon is disabled. The
|
||||
// composite `CreateAndInitFromMixin` (which calls a Lua `:Init` method) stays
|
||||
// Lua-side in the addon, mirroring retail's Mixin.lua.
|
||||
//
|
||||
// Behaviour matches the canonical Lua implementation exactly:
|
||||
// Mixin(object, ...) -- shallow-copy every key/value from each table
|
||||
// argument onto `object`; returns `object`.
|
||||
// CreateFromMixins(...) -- Mixin({}, ...); returns the new table.
|
||||
// Non-table extra arguments are skipped (the Lua form guards nil the same way).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Mixin {
|
||||
|
||||
namespace {
|
||||
|
||||
// Shallow-copy every pair from the table at `srcIdx` onto the table at
|
||||
// `dstIdx` (both absolute, positive stack indices). Stack-balanced.
|
||||
void MixinInto(void *L, int dstIdx, int srcIdx) {
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, srcIdx) != 0) {
|
||||
// stack top: ..., key (-2), value (-1). Copy both so the originals
|
||||
// survive for the next Next() iteration.
|
||||
Game::Lua::PushValue(L, -2); // key copy
|
||||
Game::Lua::PushValue(L, -2); // value copy
|
||||
Game::Lua::SetTable(L, dstIdx); // dst[key] = value; pops the two copies
|
||||
Game::Lua::SetTop(L, -2); // pop value, leave key for Next()
|
||||
}
|
||||
}
|
||||
|
||||
int __fastcall Script_Mixin(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (top < 1 || Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: Mixin(object, ...)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
for (int i = 2; i <= top; ++i)
|
||||
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
|
||||
MixinInto(L, 1, i);
|
||||
Game::Lua::PushValue(L, 1); // return object
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CreateFromMixins(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::NewTable(L); // new object at index top + 1
|
||||
const int objIdx = top + 1;
|
||||
for (int i = 1; i <= top; ++i)
|
||||
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
|
||||
MixinInto(L, objIdx, i);
|
||||
Game::Lua::SetTop(L, objIdx); // the new table is the sole result
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterGlobalFunction("Mixin", &Script_Mixin);
|
||||
Game::Lua::RegisterGlobalFunction("CreateFromMixins", &Script_CreateFromMixins);
|
||||
}
|
||||
|
||||
// Pure table helpers — register on both the in-game and glue states so they're
|
||||
// always present (matching the "always available" intent).
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFns};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::Mixin
|
||||
@@ -265,6 +265,7 @@ void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
Game::Lua::RegisterGlobalFunction("strsplit", &Script_strsplit);
|
||||
Game::Lua::RegisterGlobalFunction("strjoin", &Script_strjoin);
|
||||
Game::Lua::RegisterGlobalFunction("strtrim", &Script_strtrim);
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
// `length = CopyToClipboard(text [, removeMarkup])` — copy `text` to the
|
||||
// Windows clipboard and return the number of bytes copied. Vanilla has no
|
||||
// clipboard access at all (Blizzard never exposed one), so this is a pure
|
||||
// ClassicAPI addition backed by the Win32 clipboard API.
|
||||
//
|
||||
// - `text` the string to copy (numbers are coerced, like Lua does).
|
||||
// - `removeMarkup` optional; when truthy, strip WoW UI escape sequences
|
||||
// (|cAARRGGBB..|r color, |H..|h..|h hyperlinks keeping the
|
||||
// visible text, |T..|t textures, |n newline, || → |) before
|
||||
// copying, so the clipboard gets plain text.
|
||||
// - returns byte length of the copied (possibly stripped) string —
|
||||
// consistent with Lua's `#s`. 0 on an empty string or if
|
||||
// the clipboard couldn't be opened.
|
||||
//
|
||||
// WoW's in-memory strings are UTF-8 here (same assumption the credential and
|
||||
// TTS modules make), so we convert to UTF-16 and set CF_UNICODETEXT; Windows
|
||||
// synthesizes CF_TEXT for legacy consumers.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace Clipboard::Copy {
|
||||
|
||||
namespace {
|
||||
|
||||
// Strip WoW UI escape sequences, leaving readable text. See header comment for
|
||||
// the set handled; unknown `|x` sequences are left verbatim so real text that
|
||||
// merely contains a pipe survives.
|
||||
std::string StripMarkup(const char *s, unsigned len) {
|
||||
std::string out;
|
||||
out.reserve(len);
|
||||
unsigned i = 0;
|
||||
while (i < len) {
|
||||
const char c = s[i];
|
||||
if (c != '|') {
|
||||
out.push_back(c);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const char n = (i + 1 < len) ? s[i + 1] : '\0';
|
||||
switch (n) {
|
||||
case '|': // escaped pipe
|
||||
out.push_back('|');
|
||||
i += 2;
|
||||
break;
|
||||
case 'n': // newline token
|
||||
out.push_back('\n');
|
||||
i += 2;
|
||||
break;
|
||||
case 'r': // color reset
|
||||
i += 2;
|
||||
break;
|
||||
case 'c': // color open: |c + up to 8 hex digits
|
||||
i += 2;
|
||||
for (int k = 0; k < 8 && i < len; ++k) {
|
||||
const char h = s[i];
|
||||
const bool isHex = (h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') ||
|
||||
(h >= 'A' && h <= 'F');
|
||||
if (!isHex)
|
||||
break;
|
||||
++i;
|
||||
}
|
||||
break;
|
||||
case 'h': // hyperlink boundary (text separator or closer) — drop
|
||||
i += 2;
|
||||
break;
|
||||
case 'H': // hyperlink data opener — skip through to the next |h
|
||||
i += 2;
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == 'h'))
|
||||
++i;
|
||||
break; // the |h is dropped by the 'h' case on the next iteration
|
||||
case 't': // stray texture closer — drop
|
||||
i += 2;
|
||||
break;
|
||||
case 'T': // texture opener — skip through to the next |t
|
||||
i += 2;
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == 't'))
|
||||
++i;
|
||||
break; // the |t is dropped by the 't' case on the next iteration
|
||||
default: // unknown sequence — keep the pipe literally
|
||||
out.push_back('|');
|
||||
++i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Place a UTF-8 byte range on the clipboard as CF_UNICODETEXT. Returns true on
|
||||
// success. Does not free the allocation on success — ownership passes to the
|
||||
// clipboard.
|
||||
bool SetClipboardUtf8(const char *data, int byteLen) {
|
||||
if (!OpenClipboard(nullptr))
|
||||
return false;
|
||||
|
||||
bool ok = false;
|
||||
if (EmptyClipboard()) {
|
||||
// Empty string still writes a valid (empty) clipboard entry.
|
||||
const int wlen =
|
||||
byteLen > 0 ? MultiByteToWideChar(CP_UTF8, 0, data, byteLen, nullptr, 0) : 0;
|
||||
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, (static_cast<SIZE_T>(wlen) + 1) * sizeof(wchar_t));
|
||||
if (h != nullptr) {
|
||||
auto *dst = static_cast<wchar_t *>(GlobalLock(h));
|
||||
if (dst != nullptr) {
|
||||
if (wlen > 0)
|
||||
MultiByteToWideChar(CP_UTF8, 0, data, byteLen, dst, wlen);
|
||||
dst[wlen] = L'\0';
|
||||
GlobalUnlock(h);
|
||||
if (SetClipboardData(CF_UNICODETEXT, h) != nullptr)
|
||||
ok = true; // clipboard owns h now
|
||||
}
|
||||
if (!ok)
|
||||
GlobalFree(h);
|
||||
}
|
||||
}
|
||||
|
||||
CloseClipboard();
|
||||
return ok;
|
||||
}
|
||||
|
||||
int __fastcall Script_CopyToClipboard(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) && !Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: CopyToClipboard(text [, removeMarkup])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
|
||||
const char *text = Game::Lua::ToString(L, 1);
|
||||
const unsigned len = Game::Lua::StrLen(L, 1);
|
||||
const bool removeMarkup = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
|
||||
int copiedLen;
|
||||
if (removeMarkup) {
|
||||
const std::string stripped = StripMarkup(text, len);
|
||||
copiedLen = static_cast<int>(stripped.size());
|
||||
SetClipboardUtf8(stripped.data(), copiedLen);
|
||||
} else {
|
||||
copiedLen = static_cast<int>(len);
|
||||
SetClipboardUtf8(text, copiedLen);
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(copiedLen));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterGlobalFunction("CopyToClipboard", &Script_CopyToClipboard);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFns};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Clipboard::Copy
|
||||
@@ -0,0 +1,292 @@
|
||||
// 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_ColorUtil` — the modern color-space + text-color-code helpers, backported
|
||||
// to 1.12. Every one is pure math / string formatting (no engine state), so
|
||||
// this is a plain reimplementation rather than a hook.
|
||||
//
|
||||
// Conventions verified in-game against a retail-family client that ships
|
||||
// C_ColorUtil natively:
|
||||
// - HSV/HSL hue is in DEGREES, [0, 360); saturation / value / lightness are
|
||||
// [0, 1]. Achromatic (gray) inputs yield hue = -1 (a sentinel, not 0).
|
||||
// - GenerateTextColorCode(color) returns the BARE 8-hex string "AARRGGBB"
|
||||
// (e.g. red -> "ffff0000"), alpha forced to ff (the arg type is colorRGB,
|
||||
// no alpha channel).
|
||||
// - WrapTextInColorCode(text, code) returns "|c" .. code .. text .. "|r".
|
||||
// - WrapTextInColor(text, color) = WrapTextInColorCode(text,
|
||||
// GenerateTextColorCode(color)) -> "|cffff0000Hi|r".
|
||||
//
|
||||
// The color arg is a table read by raw r/g/b fields (ColorMixin-shaped); an
|
||||
// optional a field is honored (default 1.0) though retail's colorRGB type
|
||||
// omits it — for the documented no-alpha case both give ff.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace Color::Util {
|
||||
|
||||
namespace {
|
||||
|
||||
double Clamp01(double v) { return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); }
|
||||
|
||||
int ToByte(double v) {
|
||||
const double c = Clamp01(v);
|
||||
return static_cast<int>(c * 255.0 + 0.5); // round to nearest
|
||||
}
|
||||
|
||||
double Max3(double a, double b, double c) {
|
||||
double m = a > b ? a : b;
|
||||
return m > c ? m : c;
|
||||
}
|
||||
double Min3(double a, double b, double c) {
|
||||
double m = a < b ? a : b;
|
||||
return m < c ? m : c;
|
||||
}
|
||||
double MinLuma(double l) { return l < 1.0 - l ? l : 1.0 - l; }
|
||||
|
||||
// --- Pure conversions --------------------------------------------------
|
||||
|
||||
void RgbToHsv(double r, double g, double b, double &h, double &s, double &v) {
|
||||
const double mx = Max3(r, g, b), mn = Min3(r, g, b), d = mx - mn;
|
||||
v = mx;
|
||||
s = (mx <= 0.0) ? 0.0 : d / mx;
|
||||
if (d <= 0.0) { // achromatic
|
||||
h = -1.0;
|
||||
return;
|
||||
}
|
||||
if (mx == r)
|
||||
h = 60.0 * std::fmod((g - b) / d, 6.0);
|
||||
else if (mx == g)
|
||||
h = 60.0 * ((b - r) / d + 2.0);
|
||||
else
|
||||
h = 60.0 * ((r - g) / d + 4.0);
|
||||
if (h < 0.0)
|
||||
h += 360.0;
|
||||
}
|
||||
|
||||
void HsvToRgb(double h, double s, double v, double &r, double &g, double &b) {
|
||||
if (s <= 0.0 || h < 0.0) { // achromatic (incl. the -1 sentinel)
|
||||
r = g = b = v;
|
||||
return;
|
||||
}
|
||||
h = std::fmod(h, 360.0);
|
||||
if (h < 0.0)
|
||||
h += 360.0;
|
||||
const double hh = h / 60.0;
|
||||
const int i = static_cast<int>(std::floor(hh));
|
||||
const double f = hh - i;
|
||||
const double p = v * (1.0 - s);
|
||||
const double q = v * (1.0 - s * f);
|
||||
const double t = v * (1.0 - s * (1.0 - f));
|
||||
switch (i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
default: r = v; g = p; b = q; break;
|
||||
}
|
||||
}
|
||||
|
||||
void HsvToHsl(double h, double sv, double v, double &ho, double &sl, double &l) {
|
||||
ho = h; // hue (incl. -1) passes through unchanged
|
||||
l = v * (1.0 - sv / 2.0);
|
||||
sl = (l <= 0.0 || l >= 1.0) ? 0.0 : (v - l) / MinLuma(l);
|
||||
}
|
||||
|
||||
void HslToHsv(double h, double sl, double l, double &ho, double &sv, double &v) {
|
||||
ho = h;
|
||||
v = l + sl * MinLuma(l);
|
||||
sv = (v <= 0.0) ? 0.0 : 2.0 * (1.0 - l / v);
|
||||
}
|
||||
|
||||
void HslToRgb(double h, double s, double l, double &r, double &g, double &b) {
|
||||
if (s <= 0.0 || h < 0.0) { // achromatic
|
||||
r = g = b = l;
|
||||
return;
|
||||
}
|
||||
h = std::fmod(h, 360.0);
|
||||
if (h < 0.0)
|
||||
h += 360.0;
|
||||
const double c = (1.0 - std::fabs(2.0 * l - 1.0)) * s;
|
||||
const double hh = h / 60.0;
|
||||
const double x = c * (1.0 - std::fabs(std::fmod(hh, 2.0) - 1.0));
|
||||
const double m = l - c / 2.0;
|
||||
double r1, g1, b1;
|
||||
switch (static_cast<int>(std::floor(hh)) % 6) {
|
||||
case 0: r1 = c; g1 = x; b1 = 0.0; break;
|
||||
case 1: r1 = x; g1 = c; b1 = 0.0; break;
|
||||
case 2: r1 = 0.0; g1 = c; b1 = x; break;
|
||||
case 3: r1 = 0.0; g1 = x; b1 = c; break;
|
||||
case 4: r1 = x; g1 = 0.0; b1 = c; break;
|
||||
default: r1 = c; g1 = 0.0; b1 = x; break;
|
||||
}
|
||||
r = r1 + m;
|
||||
g = g1 + m;
|
||||
b = b1 + m;
|
||||
}
|
||||
|
||||
// --- Lua glue ----------------------------------------------------------
|
||||
|
||||
// Read arg `n` as a number, or raise the usage error.
|
||||
double Arg(void *L, int n, const char *usage) {
|
||||
if (!Game::Lua::IsNumber(L, n)) {
|
||||
Game::Lua::Error(L, "%s", usage);
|
||||
return 0.0; // unreachable
|
||||
}
|
||||
return Game::Lua::ToNumber(L, n);
|
||||
}
|
||||
|
||||
// Read a numeric field from the color table at (absolute, positive) index
|
||||
// `idx`. Raw access — ColorMixin stores r/g/b/a as direct keys.
|
||||
double ColorField(void *L, int idx, const char *key, double dflt) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::RawGet(L, idx);
|
||||
const double v = Game::Lua::IsNumber(L, -1) ? Game::Lua::ToNumber(L, -1) : dflt;
|
||||
Game::Lua::SetTop(L, -2); // pop the value
|
||||
return v;
|
||||
}
|
||||
|
||||
// "AARRGGBB" from a color table at `idx`, alpha default ff.
|
||||
std::string HexFromColor(void *L, int idx) {
|
||||
const int a = ToByte(ColorField(L, idx, "a", 1.0));
|
||||
const int r = ToByte(ColorField(L, idx, "r", 0.0));
|
||||
const int g = ToByte(ColorField(L, idx, "g", 0.0));
|
||||
const int b = ToByte(ColorField(L, idx, "b", 0.0));
|
||||
char buf[9];
|
||||
std::snprintf(buf, sizeof(buf), "%02x%02x%02x%02x", a, r, g, b);
|
||||
return std::string(buf, 8);
|
||||
}
|
||||
|
||||
int __fastcall Script_ConvertRGBToHSV(void *L) {
|
||||
const double r = Arg(L, 1, "Usage: C_ColorUtil.ConvertRGBToHSV(r, g, b)");
|
||||
const double g = Arg(L, 2, "Usage: C_ColorUtil.ConvertRGBToHSV(r, g, b)");
|
||||
const double b = Arg(L, 3, "Usage: C_ColorUtil.ConvertRGBToHSV(r, g, b)");
|
||||
double h, s, v;
|
||||
RgbToHsv(r, g, b, h, s, v);
|
||||
Game::Lua::PushNumber(L, h);
|
||||
Game::Lua::PushNumber(L, s);
|
||||
Game::Lua::PushNumber(L, v);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_ConvertHSVToRGB(void *L) {
|
||||
const double h = Arg(L, 1, "Usage: C_ColorUtil.ConvertHSVToRGB(h, s, v)");
|
||||
const double s = Arg(L, 2, "Usage: C_ColorUtil.ConvertHSVToRGB(h, s, v)");
|
||||
const double v = Arg(L, 3, "Usage: C_ColorUtil.ConvertHSVToRGB(h, s, v)");
|
||||
double r, g, b;
|
||||
HsvToRgb(h, s, v, r, g, b);
|
||||
Game::Lua::PushNumber(L, r);
|
||||
Game::Lua::PushNumber(L, g);
|
||||
Game::Lua::PushNumber(L, b);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_ConvertHSVToHSL(void *L) {
|
||||
const double h = Arg(L, 1, "Usage: C_ColorUtil.ConvertHSVToHSL(h, s, v)");
|
||||
const double s = Arg(L, 2, "Usage: C_ColorUtil.ConvertHSVToHSL(h, s, v)");
|
||||
const double v = Arg(L, 3, "Usage: C_ColorUtil.ConvertHSVToHSL(h, s, v)");
|
||||
double ho, sl, l;
|
||||
HsvToHsl(h, s, v, ho, sl, l);
|
||||
Game::Lua::PushNumber(L, ho);
|
||||
Game::Lua::PushNumber(L, sl);
|
||||
Game::Lua::PushNumber(L, l);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_ConvertHSLToHSV(void *L) {
|
||||
const double h = Arg(L, 1, "Usage: C_ColorUtil.ConvertHSLToHSV(h, s, l)");
|
||||
const double s = Arg(L, 2, "Usage: C_ColorUtil.ConvertHSLToHSV(h, s, l)");
|
||||
const double l = Arg(L, 3, "Usage: C_ColorUtil.ConvertHSLToHSV(h, s, l)");
|
||||
double ho, sv, v;
|
||||
HslToHsv(h, s, l, ho, sv, v);
|
||||
Game::Lua::PushNumber(L, ho);
|
||||
Game::Lua::PushNumber(L, sv);
|
||||
Game::Lua::PushNumber(L, v);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_ConvertHSLToRGB(void *L) {
|
||||
const double h = Arg(L, 1, "Usage: C_ColorUtil.ConvertHSLToRGB(h, s, l)");
|
||||
const double s = Arg(L, 2, "Usage: C_ColorUtil.ConvertHSLToRGB(h, s, l)");
|
||||
const double l = Arg(L, 3, "Usage: C_ColorUtil.ConvertHSLToRGB(h, s, l)");
|
||||
double r, g, b;
|
||||
HslToRgb(h, s, l, r, g, b);
|
||||
Game::Lua::PushNumber(L, r);
|
||||
Game::Lua::PushNumber(L, g);
|
||||
Game::Lua::PushNumber(L, b);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_GenerateTextColorCode(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: C_ColorUtil.GenerateTextColorCode(color)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const std::string hex = HexFromColor(L, 1);
|
||||
Game::Lua::PushString(L, hex.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_WrapTextInColor(void *L) {
|
||||
const char *text = Game::Lua::ToString(L, 1);
|
||||
if (text == nullptr || Game::Lua::Type(L, 2) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: C_ColorUtil.WrapTextInColor(text, color)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
std::string out = "|c" + HexFromColor(L, 2) + text + "|r";
|
||||
Game::Lua::PushString(L, out.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_WrapTextInColorCode(void *L) {
|
||||
const char *text = Game::Lua::ToString(L, 1);
|
||||
const char *code = Game::Lua::ToString(L, 2);
|
||||
if (text == nullptr || code == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: C_ColorUtil.WrapTextInColorCode(text, colorCode)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
std::string out = std::string("|c") + code + text + "|r";
|
||||
Game::Lua::PushString(L, out.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertRGBToHSV",
|
||||
&Script_ConvertRGBToHSV);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSVToRGB",
|
||||
&Script_ConvertHSVToRGB);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSVToHSL",
|
||||
&Script_ConvertHSVToHSL);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSLToHSV",
|
||||
&Script_ConvertHSLToHSV);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSLToRGB",
|
||||
&Script_ConvertHSLToRGB);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "GenerateTextColorCode",
|
||||
&Script_GenerateTextColorCode);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "WrapTextInColor",
|
||||
&Script_WrapTextInColor);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "WrapTextInColorCode",
|
||||
&Script_WrapTextInColorCode);
|
||||
}
|
||||
|
||||
// Pure helpers — useful on both the in-game and glue states.
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFns};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Color::Util
|
||||
@@ -26,6 +26,12 @@
|
||||
// - `region:SetSize(w, h)` / `region:GetSize()` — registered on the
|
||||
// REGION base method registry, so they resolve on frames, buttons,
|
||||
// textures, fontstrings — everything.
|
||||
// - `region:IsMouseOver([top, bottom, left, right])` — cursor-in-rect
|
||||
// test with optional per-edge offsets, also on the REGION registry.
|
||||
// - `region:GetRect()` — left, bottom, width, height, composed from the
|
||||
// engine's Region getters. Also on the REGION registry.
|
||||
// - `region:IsDragging()` — true while the region is the frame currently
|
||||
// being moved/sized. Reads the engine's global drag target.
|
||||
// - `frame:SetShown(shown)` — Show/Hide are per-branch script functions
|
||||
// (each with its own object typecheck), so SetShown is registered
|
||||
// separately on Frame / Texture / FontString with matching delegates.
|
||||
@@ -34,6 +40,9 @@
|
||||
// - `frame:HookScript(scriptType, handler)` — absent in vanilla
|
||||
// entirely. Chains after any existing handler and invokes `handler`
|
||||
// with modern positional args (see the implementation comment).
|
||||
// - `frame:GetEffectiveAlpha()` — own alpha × the parent chain's, the
|
||||
// product WoW composites at render time. Frame-scoped (matches
|
||||
// GetAlpha); walks the parents via the engine's own getters.
|
||||
//
|
||||
// Implementation style: each new method delegates to the ENGINE's own
|
||||
// Script_* implementations by reshaping the Lua stack and calling them
|
||||
@@ -83,6 +92,119 @@ int __fastcall Script_GetSize(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ---- IsMouseOver (Region base — all region types) --------------------------
|
||||
|
||||
// `region:IsMouseOver([topOffset, bottomOffset, leftOffset, rightOffset])` —
|
||||
// true when the cursor is within the region's rect, each edge optionally
|
||||
// shifted by the matching offset (added to that edge, modern semantics:
|
||||
// +top/-bottom/-left/+right enlarges). Vanilla addons have always done this
|
||||
// in Lua as MouseIsOver(); we register it as the native Region method later
|
||||
// clients expose.
|
||||
//
|
||||
// The math mirrors the engine exactly: GetLeft/Right/Top/Bottom (Region
|
||||
// registry, all types) and GetCursorPosition both return values scaled by the
|
||||
// same k = FUN_0041ad70()*DAT_007ffd68 factor, but the rect getters also
|
||||
// divide by the region's effective scale — so dividing the cursor by that same
|
||||
// effective scale (read from obj+0x7C, the field GetLeft/GetEffectiveScale
|
||||
// themselves read) puts both in the region's logical space. A getter returns
|
||||
// nil for an unpositioned region → we report false.
|
||||
|
||||
// Calls a Region getter that returns one number; false if it pushed nil/none.
|
||||
bool CallRegionNumber(void *L, uintptr_t fn, double *out) {
|
||||
Game::Lua::SetTop(L, 1); // (self)
|
||||
CallScript(fn, L); // (self, value | nil)
|
||||
if (!Game::Lua::IsNumber(L, 2))
|
||||
return false;
|
||||
*out = Game::Lua::ToNumber(L, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
const double offBottom = Game::Lua::IsNumber(L, 3) ? Game::Lua::ToNumber(L, 3) : 0.0;
|
||||
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);
|
||||
if (obj == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const float effScale = *reinterpret_cast<const float *>(
|
||||
reinterpret_cast<const uint8_t *>(obj) + Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
|
||||
double left, right, top, bottom;
|
||||
if (effScale == 0.0f ||
|
||||
!CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETLEFT, &left) ||
|
||||
!CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETRIGHT, &right) ||
|
||||
!CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETTOP, &top) ||
|
||||
!CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETBOTTOM, &bottom)) {
|
||||
Game::Lua::PushBool(L, false); // no resolved rect
|
||||
return 1;
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
CallScript(Offsets::FUN_SCRIPT_GETCURSORPOSITION, L); // (x, y)
|
||||
const double x = Game::Lua::ToNumber(L, 1) / effScale;
|
||||
const double y = Game::Lua::ToNumber(L, 2) / effScale;
|
||||
|
||||
left += offLeft;
|
||||
right += offRight;
|
||||
top += offTop;
|
||||
bottom += offBottom;
|
||||
|
||||
const bool inside = x >= left && x <= right && y >= bottom && y <= top;
|
||||
Game::Lua::PushBool(L, inside);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- GetRect (Region base — all region types) ------------------------------
|
||||
|
||||
// `region:GetRect()` → left, bottom, width, height. Composed from the
|
||||
// engine's own Region getters (so all UI-scale conversion is its code).
|
||||
// Matches retail: returns nothing for a region with no resolved rect —
|
||||
// gated on GetLeft, which pushes nil when the position isn't resolved (a
|
||||
// size-only region with no anchor has width/height but no rect).
|
||||
int __fastcall Script_GetRect(void *L) {
|
||||
double left, bottom, width, height;
|
||||
if (!CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETLEFT, &left))
|
||||
return 0;
|
||||
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETBOTTOM, &bottom);
|
||||
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETWIDTH, &width);
|
||||
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETHEIGHT, &height);
|
||||
Game::Lua::SetTop(L, 1);
|
||||
Game::Lua::PushNumber(L, left);
|
||||
Game::Lua::PushNumber(L, bottom);
|
||||
Game::Lua::PushNumber(L, width);
|
||||
Game::Lua::PushNumber(L, height);
|
||||
return 4;
|
||||
}
|
||||
|
||||
// ---- IsDragging (Region base — all region types) ---------------------------
|
||||
|
||||
// `region:IsDragging()` — true while this region is the frame currently
|
||||
// being moved/sized (StartMoving/StartSizing active, before
|
||||
// StopMovingOrSizing). The engine tracks a single drag target in the global
|
||||
// UI context at +0xCFC; StopMovingOrSizing itself gates on
|
||||
// `context+0xCFC == self`, which is exactly this predicate. Reading the
|
||||
// global context (rather than self+0xA0) keeps it correct for non-frame
|
||||
// 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 *ctx = *reinterpret_cast<void *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_UI_CONTEXT_PTR));
|
||||
void *dragTarget =
|
||||
ctx != nullptr
|
||||
? *reinterpret_cast<void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(ctx) +
|
||||
Offsets::OFF_UI_CONTEXT_DRAG_TARGET)
|
||||
: nullptr;
|
||||
Game::Lua::PushBool(L, self != nullptr && self == dragTarget);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- SetShown (per-branch Show/Hide delegates) -----------------------------
|
||||
|
||||
// `object:SetShown(shown)` — any falsy value hides, truthy shows,
|
||||
@@ -95,6 +217,33 @@ int __fastcall Script_SetShown(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- GetEffectiveAlpha (Frame) ---------------------------------------------
|
||||
|
||||
// `frame:GetEffectiveAlpha()` — the frame's own alpha times every
|
||||
// ancestor's alpha, i.e. the product WoW composites at render time (fading
|
||||
// UIParent fades all children). Vanilla exposes only GetAlpha (own alpha);
|
||||
// we walk self → parent → … via the engine's own GetAlpha (Frame) and
|
||||
// GetParent (Region), so no field offsets or scale math are re-derived. The
|
||||
// iteration cap is a defensive guard against a pathological chain (WoW frame
|
||||
// trees are shallow; the loop normally ends when GetParent returns nil at
|
||||
// the top of the tree).
|
||||
int __fastcall Script_GetEffectiveAlpha(void *L) {
|
||||
Game::Lua::SetTop(L, 1); // (obj) = self
|
||||
double eff = 1.0;
|
||||
for (int guard = 0; guard < 128; ++guard) {
|
||||
CallScript(Offsets::FUN_SCRIPT_FRAME_GETALPHA, L); // (obj, alpha)
|
||||
eff *= Game::Lua::ToNumber(L, 2);
|
||||
Game::Lua::SetTop(L, 1); // (obj)
|
||||
CallScript(Offsets::FUN_SCRIPT_REGION_GETPARENT, L); // (obj, parent|nil)
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_NIL)
|
||||
break;
|
||||
Game::Lua::Remove(L, 1); // drop obj → (parent) becomes index 1
|
||||
}
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushNumber(L, eff);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- SetResizeBounds (Frame) -----------------------------------------------
|
||||
|
||||
// `frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])` —
|
||||
@@ -274,6 +423,9 @@ const Game::HookAutoRegister _setPointHook{
|
||||
const Game::Lua::FrameMethodEntry g_regionMethods[] = {
|
||||
{"SetSize", &Script_SetSize},
|
||||
{"GetSize", &Script_GetSize},
|
||||
{"IsMouseOver", &Script_IsMouseOver},
|
||||
{"GetRect", &Script_GetRect},
|
||||
{"IsDragging", &Script_IsDragging},
|
||||
};
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
@@ -282,6 +434,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
{"SetResizeBounds", &Script_SetResizeBounds},
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"IsEventRegistered", &Script_IsEventRegistered},
|
||||
{"GetEffectiveAlpha", &Script_GetEffectiveAlpha},
|
||||
};
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_textureMethods[] = {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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_Glue` session helpers.
|
||||
//
|
||||
// `C_Glue.IsFirstLoadThisSession()` — glue-only. Returns true only while the
|
||||
// FIRST glue screen since the process launched is showing, and false on
|
||||
// every return to glue after a world login (a genuine world→glue logout;
|
||||
// login↔character-select stays within the first glue session). Retail uses
|
||||
// it so the login UI runs one-time startup (intro cinematic, news) only on
|
||||
// the initial launch, not on each relog.
|
||||
//
|
||||
// Backed by a process-static boot counter, no engine state needed: the glue
|
||||
// registration callback fires exactly once per glue boot
|
||||
// (FUN_LOAD_GLUE_SCRIPT_FUNCTIONS — initial launch + each world→glue return),
|
||||
// so the counter equals "how many glue screens this session" and the first
|
||||
// one is boot #1. The glue Lua state is recreated per boot, so C_Glue is
|
||||
// re-registered each time; the counter is process-static and survives that.
|
||||
//
|
||||
// `C_Glue.IsOnGlueScreen()` — true when a GlueXML screen is showing (no
|
||||
// character logged in), false in the world. Registered on BOTH Lua states,
|
||||
// and the answer is a compile-time constant per state: the glue and in-game
|
||||
// Lua states are mutually exclusive (glue exists only when no character is in
|
||||
// the world), so the glue registration returns true and the in-game one
|
||||
// returns false — no runtime flag to consult. Available in both states so
|
||||
// shared code that runs in either environment can branch on it.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace Glue::Session {
|
||||
|
||||
namespace {
|
||||
|
||||
// Glue-screen boots since process start. 1 during the very first glue screen.
|
||||
int g_glueBootCount = 0;
|
||||
|
||||
int __fastcall Script_IsFirstLoadThisSession(void *L) {
|
||||
Game::Lua::PushBool(L, g_glueBootCount <= 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsOnGlueScreen_True(void *L) {
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsOnGlueScreen_False(void *L) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
++g_glueBootCount; // one glue boot per callback invocation
|
||||
Game::Lua::RegisterTableFunction("C_Glue", "IsFirstLoadThisSession",
|
||||
&Script_IsFirstLoadThisSession);
|
||||
Game::Lua::RegisterTableFunction("C_Glue", "IsOnGlueScreen",
|
||||
&Script_IsOnGlueScreen_True);
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
// In-world: only IsOnGlueScreen makes sense (and it's always false).
|
||||
// IsFirstLoadThisSession is a glue concept, so it's not registered here.
|
||||
Game::Lua::RegisterTableFunction("C_Glue", "IsOnGlueScreen",
|
||||
&Script_IsOnGlueScreen_False);
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Glue::Session
|
||||
+101
-24
@@ -30,11 +30,23 @@
|
||||
//
|
||||
// The feed is event-driven, not polled: it hangs off `Bag::UpdateDelayed`,
|
||||
// which co-hooks the engine's three `BAG_UPDATE` fire sites and signals once
|
||||
// per frame in which a bag actually changed. On that signal we scan bags
|
||||
// 0..4, diff the resident item GUIDs against the previous scan, flag anything
|
||||
// newly acquired, and prune flags for items that left. `BAG_NEW_ITEMS_UPDATED`
|
||||
// (no payload — matching retail) fires whenever the new-item set changes. So
|
||||
// nothing runs on quiet frames.
|
||||
// per frame in which a bag actually changed. On that signal we scan bags 0..4,
|
||||
// flag any resident GUID that isn't in the owned "seen-set", reconcile the
|
||||
// seen-set against everything owned now, and prune flags for items that have
|
||||
// genuinely left. `BAG_NEW_ITEMS_UPDATED` (no payload — matching retail) fires
|
||||
// whenever the new-item set changes. Nothing runs on quiet frames.
|
||||
//
|
||||
// The seen-set is deliberately NOT rebuilt from each scan: a bag update in
|
||||
// progress transiently drops items from the read — sometimes the entire
|
||||
// backpack at once, for a frame or two — and a naive "seen = this scan"
|
||||
// approach would then re-flag the momentarily-missing item as new when it
|
||||
// reappears (the exact bug this system had). Instead each entry carries a
|
||||
// consecutive-absent-scan counter (`SeenReconcile`): present items reset it to
|
||||
// 0, absent ones age by one, and only items absent `kMaxMissStreak` scans
|
||||
// running are evicted. Counting scans (not wall-clock) means a pause with no
|
||||
// bag activity never ages an owned item; any reappearance resets the streak.
|
||||
// This keeps the set BOUNDED to ~currently-owned items — genuinely-removed
|
||||
// items age out — while absorbing the transient reads.
|
||||
//
|
||||
// The one exception is establishing the login baseline: a lightweight
|
||||
// WorldTick handler waits ~1.5s after the player is first resolvable (long
|
||||
@@ -47,7 +59,7 @@
|
||||
// — even though only *bag* items are ever flagged new. That's what stops a
|
||||
// move INTO a bag from any of those (unequip, or a pull from the bank) from
|
||||
// looking like a fresh loot: the item's GUID was already in the owned set, so
|
||||
// the bag diff treats it as a move. The bank is reachable without the bank
|
||||
// the seen-set treats it as already-owned, not new. The bank is reachable without the bank
|
||||
// window open because its GUIDs are read straight from the invMgr array (the
|
||||
// same gate-bypass `C_Item.GetItemCount` uses; the array is server-populated
|
||||
// at login).
|
||||
@@ -88,7 +100,23 @@ constexpr int kMaxItems = 512;
|
||||
// engine ms counter) so it's independent of frame rate.
|
||||
constexpr uint32_t kSettleMs = 1500;
|
||||
|
||||
uint64_t g_seen[kMaxItems]; // all owned GUIDs (bags + equipment + bank) at the last scan
|
||||
// Consecutive bag-change scans an item may be absent from the read before it's
|
||||
// dropped from the seen-set. A bag update in progress transiently drops items
|
||||
// from the scan — sometimes the whole backpack at once — for a frame or two;
|
||||
// counting SCANS (not wall-clock) means a pause with no bag activity never ages
|
||||
// an owned item, and any reappearance resets the streak to 0, so a transient
|
||||
// miss never reaches the threshold. Genuinely-removed items hit it within a few
|
||||
// subsequent bag changes and age out — that's what keeps the set BOUNDED to
|
||||
// ~currently-owned items rather than everything ever owned.
|
||||
constexpr int kMaxMissStreak = 4;
|
||||
|
||||
// One seen-set entry: an owned item's GUID and how many consecutive scans it
|
||||
// has been absent from the read (0 = present this scan).
|
||||
struct SeenEntry {
|
||||
uint64_t guid;
|
||||
int miss;
|
||||
};
|
||||
SeenEntry g_seen[kMaxItems];
|
||||
int g_seenCount = 0;
|
||||
uint64_t g_new[kMaxItems]; // subset currently flagged "new"
|
||||
int g_newCount = 0;
|
||||
@@ -115,6 +143,44 @@ bool AddUnique(uint64_t *arr, int *count, uint64_t guid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- seen-set (bounded, absent-scan-count evicted) -----------------------
|
||||
|
||||
bool SeenContains(uint64_t guid) {
|
||||
for (int i = 0; i < g_seenCount; ++i)
|
||||
if (g_seen[i].guid == guid)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// One scan's reconcile: age every entry by one missed scan, then reset the
|
||||
// streak (and insert) for everything owned right now, then evict entries that
|
||||
// have been absent `kMaxMissStreak` scans running. `owned` is the union of the
|
||||
// current bag scan and non-bag owned items (equipment + bank).
|
||||
void SeenReconcile(const uint64_t *owned, int ownedCount) {
|
||||
for (int i = 0; i < g_seenCount; ++i)
|
||||
++g_seen[i].miss;
|
||||
for (int j = 0; j < ownedCount; ++j) {
|
||||
const uint64_t guid = owned[j];
|
||||
if (guid == 0)
|
||||
continue;
|
||||
bool found = false;
|
||||
for (int i = 0; i < g_seenCount; ++i)
|
||||
if (g_seen[i].guid == guid) {
|
||||
g_seen[i].miss = 0;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (!found && g_seenCount < kMaxItems)
|
||||
g_seen[g_seenCount++] = {guid, 0};
|
||||
}
|
||||
for (int i = 0; i < g_seenCount;) {
|
||||
if (g_seen[i].miss >= kMaxMissStreak)
|
||||
g_seen[i] = g_seen[--g_seenCount];
|
||||
else
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoveFrom(uint64_t *arr, int *count, uint64_t guid) {
|
||||
for (int i = 0; i < *count; ++i) {
|
||||
if (arr[i] == guid) {
|
||||
@@ -257,12 +323,6 @@ void FireChanged() {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtNewItems), "");
|
||||
}
|
||||
|
||||
void CopyToSeen(const uint64_t *current, int count) {
|
||||
for (int i = 0; i < count; ++i)
|
||||
g_seen[i] = current[i];
|
||||
g_seenCount = count;
|
||||
}
|
||||
|
||||
// Login-baseline handler. Runs every frame but does no work once the
|
||||
// baseline is taken — it only watches for (re)login and, after a short
|
||||
// settle, takes the single authoritative baseline scan. Steady state is just
|
||||
@@ -290,11 +350,14 @@ void OnTick() {
|
||||
if (player == nullptr)
|
||||
return; // not resolvable yet — retry next tick
|
||||
|
||||
// Everything currently owned — bags AND worn equipment — becomes the
|
||||
// Everything currently owned — bags AND worn equipment + bank — becomes the
|
||||
// baseline; nothing owned at login is "new". Including equipment means a
|
||||
// later unequip (paperdoll → bag) is recognized as a move, not a loot.
|
||||
g_seenCount = ScanBags(player, g_seen);
|
||||
AppendNonBagOwned(g_seen, &g_seenCount);
|
||||
uint64_t owned[kMaxItems];
|
||||
int ownedCount = ScanBags(player, owned);
|
||||
AppendNonBagOwned(owned, &ownedCount);
|
||||
g_seenCount = 0;
|
||||
SeenReconcile(owned, ownedCount);
|
||||
g_newCount = 0;
|
||||
g_seeded = true;
|
||||
}
|
||||
@@ -316,21 +379,35 @@ void OnBagChanged() {
|
||||
const int bagCount = ScanBags(player, currentBags);
|
||||
|
||||
bool changed = false;
|
||||
// Drop flags for items no longer in the bags (used, sold, equipped, banked).
|
||||
// Flag bag items we've never owned. An item merely dropped from a recent
|
||||
// scan (bag update mid-swap) is still in the seen-set (its miss streak is
|
||||
// below the threshold), so it is NOT re-flagged when it reappears — that
|
||||
// was the false-"new" bug. Checked before the reconcile so a genuinely new
|
||||
// item isn't inserted first.
|
||||
for (int i = 0; i < bagCount; ++i) {
|
||||
if (!SeenContains(currentBags[i]))
|
||||
changed |= AddUnique(g_new, &g_newCount, currentBags[i]);
|
||||
}
|
||||
|
||||
// Reconcile the seen-set against everything owned now (bags + equipment +
|
||||
// bank): resets present items, ages absent ones, evicts long-gone ones.
|
||||
uint64_t owned[kMaxItems];
|
||||
int ownedCount = 0;
|
||||
for (int i = 0; i < bagCount; ++i)
|
||||
AddUnique(owned, &ownedCount, currentBags[i]);
|
||||
AppendNonBagOwned(owned, &ownedCount);
|
||||
SeenReconcile(owned, ownedCount);
|
||||
|
||||
// Drop flags only for items that have genuinely left (fallen out of the
|
||||
// seen-set entirely), not ones merely missing from this one scan.
|
||||
for (int i = 0; i < g_newCount;) {
|
||||
if (!Contains(currentBags, bagCount, g_new[i])) {
|
||||
if (!SeenContains(g_new[i])) {
|
||||
g_new[i] = g_new[--g_newCount];
|
||||
changed = true;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < bagCount; ++i) {
|
||||
if (!Contains(g_seen, g_seenCount, currentBags[i]))
|
||||
changed |= AddUnique(g_new, &g_newCount, currentBags[i]);
|
||||
}
|
||||
CopyToSeen(currentBags, bagCount);
|
||||
AppendNonBagOwned(g_seen, &g_seenCount);
|
||||
|
||||
if (changed)
|
||||
FireChanged();
|
||||
|
||||
+19
-8
@@ -140,20 +140,31 @@ bool Resolve() {
|
||||
|
||||
namespace {
|
||||
|
||||
// 4th arg is the GUID high dword. Ghidra types it `float` from usage, but
|
||||
// it's a plain dword on the stack (the engine assigns it the current
|
||||
// selection's high dword when 0); `unsigned` puts the correct 4 bytes there.
|
||||
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType,
|
||||
unsigned targetGuidLo, float targetGuidHi);
|
||||
unsigned targetGuidLo, unsigned targetGuidHi);
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
|
||||
|
||||
void DispatchSlot(int slot0Based, int bookType) {
|
||||
void DispatchSlot(int slot0Based, int bookType, uint64_t targetGuid) {
|
||||
auto dispatch = reinterpret_cast<CastDispatch_t>(
|
||||
Offsets::FUN_SPELL_CAST_DISPATCH);
|
||||
// (0, 0) GUID = no implicit target; the engine handles placement.
|
||||
dispatch(static_cast<unsigned>(slot0Based), bookType, 0, 0.0f);
|
||||
// GUID 0 = no implicit target (engine uses current selection / placement).
|
||||
// A real GUID casts a unit-target spell straight at that unit.
|
||||
dispatch(static_cast<unsigned>(slot0Based), bookType,
|
||||
static_cast<unsigned>(targetGuid),
|
||||
static_cast<unsigned>(targetGuid >> 32));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DispatchSpellCast(int spellID) {
|
||||
bool IsPlacementActive() {
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_SPELL_PLACEMENT_STATE) != 0;
|
||||
}
|
||||
|
||||
bool DispatchSpellCast(int spellID, uint64_t targetGuid) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
|
||||
@@ -167,11 +178,11 @@ bool DispatchSpellCast(int spellID) {
|
||||
if (slot1 <= 0)
|
||||
return false; // not in the player's spellbook
|
||||
|
||||
DispatchSlot(slot1 - 1, bookType);
|
||||
DispatchSlot(slot1 - 1, bookType, targetGuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DispatchSpellCastByName(const char *name) {
|
||||
bool DispatchSpellCastByName(const char *name, uint64_t targetGuid) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
|
||||
@@ -185,7 +196,7 @@ bool DispatchSpellCastByName(const char *name) {
|
||||
if (slot < 0)
|
||||
return false; // not in the player's spellbook
|
||||
|
||||
DispatchSlot(slot, bookType);
|
||||
DispatchSlot(slot, bookType, targetGuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -13,8 +13,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::AtCursor {
|
||||
|
||||
// True while the engine has an in-flight targeting placement of any kind
|
||||
// (ground reticle, unit/item/corpse select). `VAR_SPELL_PLACEMENT_STATE != 0`.
|
||||
// Used by `CastAtUnit` to tell a unit-target spell that fired immediately
|
||||
// (no placement) from a ground spell that's waiting for a location.
|
||||
bool IsPlacementActive();
|
||||
|
||||
// Resolves the engine's in-flight ground-target placement at the
|
||||
// player's current cursor world position. Intended to be called
|
||||
// immediately after initiating a ground-target cast (spell or item)
|
||||
@@ -51,7 +59,14 @@ bool CommitAtCoords(const float coords[3]);
|
||||
// name form defers to the engine's own resolver, which parses a
|
||||
// trailing `(Rank N)` — `"Blizzard"` casts the highest known rank,
|
||||
// `"Blizzard(Rank 6)"` casts that rank.
|
||||
bool DispatchSpellCast(int spellID);
|
||||
bool DispatchSpellCastByName(const char *name);
|
||||
//
|
||||
// `targetGuid` is the implicit cast target: `0` = the engine's default
|
||||
// (current selection, or ground placement for AoE); a unit's GUID makes
|
||||
// a unit-target/normal spell fire directly on that unit (the engine
|
||||
// substitutes it exactly where it would the current target). Ground
|
||||
// spells ignore it and still enter placement. `CastAtUnit` passes the
|
||||
// resolved unit's GUID; `CastAtCursor` leaves it 0.
|
||||
bool DispatchSpellCast(int spellID, uint64_t targetGuid = 0);
|
||||
bool DispatchSpellCastByName(const char *name, uint64_t targetGuid = 0);
|
||||
|
||||
} // namespace Spell::AtCursor
|
||||
|
||||
+47
-26
@@ -11,33 +11,46 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Spell.CastAtUnit(spellID, unit)` — cast a ground-target spell at
|
||||
// the given unit's feet, bypassing the manual click on the AoE
|
||||
// reticle. `CastAtUnit(spellID, "player")` drops it at the player's
|
||||
// position, `"target"` at the target's, etc.
|
||||
// `C_Spell.CastAtUnit(spellIDOrName, unit)` — cast a spell at the given
|
||||
// unit, whatever its target type. One primitive covering both:
|
||||
//
|
||||
// Same two-step shape as `C_Spell.CastAtCursor`, but the placement is
|
||||
// committed at the unit's world position instead of the cursor
|
||||
// raycast:
|
||||
// - **Ground-target spells** (Blizzard, Flamestrike, Rain of Fire…) are
|
||||
// placed at the unit's feet — the reticle is committed at the unit's
|
||||
// world position, no manual click.
|
||||
// - **Unit-target / normal spells** (Frostbolt, Healing Touch, a buff…)
|
||||
// fire directly on the unit — its GUID is fed to the engine's cast
|
||||
// dispatcher as the implicit target, so no current-target juggling and
|
||||
// no "select target" reticle.
|
||||
//
|
||||
// 1. `Spell::AtCursor::DispatchSpellCast(spellID)` initiates the
|
||||
// cast; the engine enters placement mode for a ground-target
|
||||
// spell.
|
||||
// 2. Read the unit's world position (vtable `GetPosition`) and
|
||||
// `Spell::AtCursor::CommitAtCoords()` commits the placement
|
||||
// there.
|
||||
// Mechanism: resolve the token once to the unit object, read its position
|
||||
// and GUID, then dispatch the cast WITH that GUID
|
||||
// (`Spell::AtCursor::DispatchSpellCast(spell, guid)`). The engine's
|
||||
// dispatcher substitutes the GUID exactly where it would the current
|
||||
// selection, so a unit-target spell fires immediately at that unit. A
|
||||
// ground spell ignores the GUID and enters placement mode instead — so we
|
||||
// branch on `IsPlacementActive()`:
|
||||
//
|
||||
// Returns `true` when the placement was committed at the unit; `false`
|
||||
// when the spell isn't ground-target (placement cancelled), the spell
|
||||
// isn't in the spellbook, or the unit has no resolvable position. The
|
||||
// cast itself still proceeds for non-ground spells, just with no
|
||||
// implicit target — matching `CastAtCursor`'s semantics.
|
||||
// - No placement after dispatch → the cast already went out on the unit
|
||||
// (unit-target / normal). Success.
|
||||
// - Ground placement active → commit at the unit's feet. Success.
|
||||
// - Other placement still active → the engine didn't accept the unit as a
|
||||
// target (e.g. wrong faction / out of range); `CommitAtCoords` cancels
|
||||
// it. Failure.
|
||||
//
|
||||
// Returns `true` when the spell was cast at the unit; `false` when the
|
||||
// spell isn't in the spellbook, the unit has no resolvable position, or the
|
||||
// unit wasn't a valid target. A genuinely unrecognized token string raises
|
||||
// the engine's standard "Unknown unit" error inside the resolver, same as
|
||||
// `UnitHealth("garbage")`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::AtUnit {
|
||||
|
||||
namespace {
|
||||
@@ -53,27 +66,35 @@ int __fastcall Script_C_Spell_CastAtUnit(void *L) {
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Resolve the unit's position before dispatching — fail fast so a
|
||||
// bad/absent unit doesn't leave the engine sitting in placement
|
||||
// mode. (A genuinely unrecognized token string raises the engine's
|
||||
// standard "Unknown unit" error inside the resolver, same as
|
||||
// `UnitHealth("garbage")`.)
|
||||
// Resolve the unit once → object, then read position + GUID from it.
|
||||
// Fail fast so a bad/absent unit doesn't leave the engine mid-cast.
|
||||
void *unitObj = Unit::Position::ResolveToken(token);
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
if (unitObj == nullptr || !Unit::Position::Read(unitObj, pos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const uint64_t targetGuid = Unit::Identity::GuidForObject(unitObj);
|
||||
|
||||
const bool dispatched =
|
||||
byNumber
|
||||
? Spell::AtCursor::DispatchSpellCast(
|
||||
static_cast<int>(Game::Lua::ToNumber(L, 1)))
|
||||
: Spell::AtCursor::DispatchSpellCastByName(Game::Lua::ToString(L, 1));
|
||||
static_cast<int>(Game::Lua::ToNumber(L, 1)), targetGuid)
|
||||
: Spell::AtCursor::DispatchSpellCastByName(Game::Lua::ToString(L, 1),
|
||||
targetGuid);
|
||||
if (!dispatched) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Unit-target / normal spells fired straight at the GUID — no placement.
|
||||
// Ground spells are now sitting in placement mode; commit at the unit's
|
||||
// feet (CommitAtCoords cancels + fails for a non-ground placement, i.e.
|
||||
// an unaccepted unit target).
|
||||
if (!Spell::AtCursor::IsPlacementActive()) {
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
const bool placed = Spell::AtCursor::CommitAtCoords(pos);
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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_Spell.GetSpellEffectMechanics(spellID)` -> { m1, m2, m3 }
|
||||
//
|
||||
// Returns a spell's three per-effect mechanic ids (Spell.dbc
|
||||
// `EffectMechanic[3]`) as a 1-based array table, or nil for an invalid /
|
||||
// out-of-range spell ID. Each entry is a standard WoW SpellMechanic id
|
||||
// (0 = that effect carries no mechanic); the numbering matches
|
||||
// C_Spell.GetSpellMechanicByID (1 = Charm, 7 = Root, 12 = Stun,
|
||||
// 15 = Bleed, 17 = Polymorph, ...).
|
||||
//
|
||||
// Complements GetSpellMechanicByID, which reads only the SPELL-level
|
||||
// `Mechanic` field (+0x14). Vanilla frequently stores the meaningful
|
||||
// mechanic on an *effect* rather than the spell — periodic damage such
|
||||
// as bleeds is the common case — so effect-mechanic-aware callers (e.g.
|
||||
// bleed classification for immunity tracking) need this array too:
|
||||
//
|
||||
// local em = C_Spell.GetSpellEffectMechanics(spellID)
|
||||
// if em then for i = 1, 3 do if em[i] == 15 then --[[ bleed ]] end end end
|
||||
//
|
||||
// Reads Spell.dbc directly, so it covers every spell the client knows —
|
||||
// not just the player's spellbook — with no caching or network round-trip
|
||||
// (Spell.dbc is resident from boot).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/Arg.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::EffectMechanic {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_GetSpellEffectMechanics(void *L) {
|
||||
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return 0; // nil for invalid / out-of-range spell IDs
|
||||
|
||||
auto *mechanics = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_MECHANIC);
|
||||
|
||||
// Fixed 1-based array of all three effects (0 = effect has no mechanic),
|
||||
// matching the raw EffectMechanic[3] layout so callers can index by slot.
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1)); // key
|
||||
Game::Lua::PushNumber(L, static_cast<double>(mechanics[i])); // value
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellEffectMechanics",
|
||||
&Script_GetSpellEffectMechanics);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Spell::EffectMechanic
|
||||
+33
-20
@@ -443,34 +443,46 @@ static int __fastcall Script_C_IsSpellPassive(void *L) {
|
||||
// arrays and not in the talent tree).
|
||||
// - Same shape modern WoW (5.4.8+) uses for the same function — see
|
||||
// `[0x011C25D8]` in 5.4.8's `Wow.exe`.
|
||||
// Shared read of the engine's known-spell bitmap (the same bit the helper
|
||||
// at `0x0060C740` consults): true iff the local player knows `spellID`.
|
||||
// Bounds-checked; false for out-of-range IDs or before the bitmap exists.
|
||||
static bool PlayerKnowsSpell(int spellID) {
|
||||
if (spellID < 1)
|
||||
return false;
|
||||
const int spellCount = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount)
|
||||
return false;
|
||||
auto *bitmap = *reinterpret_cast<const uint32_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
const uint32_t mask = 1u << (spellID & 31);
|
||||
return (bitmap[spellID >> 5] & mask) != 0;
|
||||
}
|
||||
|
||||
static int __fastcall Script_IsPlayerSpell(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: IsPlayerSpell(spellID)");
|
||||
return 0;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (spellID < 1) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBoolean(L, PlayerKnowsSpell(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int spellCount = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
// `CanDualWield()` — true if the local player can equip a weapon in the off
|
||||
// hand. The server tracks this as a plain bool (`Player::m_canDualWield`)
|
||||
// flipped on only by `SPELL_EFFECT_DUAL_WIELD` (effect 40); no class has it
|
||||
// innately. Verified against the client Spell.dbc: exactly ONE spell carries
|
||||
// that effect — 674 "Dual Wield", the trained warrior/rogue/hunter passive —
|
||||
// and it lands in the known-spell bitmap like any learned spell. So there's
|
||||
// no separate client-visible capability flag to read; `IsPlayerSpell(674)`
|
||||
// is the exact equivalent of the server's `CanDualWield()`.
|
||||
static constexpr int kDualWieldSpellID = 674;
|
||||
|
||||
auto *bitmap = *reinterpret_cast<const uint32_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint32_t mask = 1u << (spellID & 31);
|
||||
const bool isKnown = (bitmap[spellID >> 5] & mask) != 0;
|
||||
Game::Lua::PushBoolean(L, isKnown);
|
||||
static int __fastcall Script_CanDualWield(void *L) {
|
||||
Game::Lua::PushBoolean(L, PlayerKnowsSpell(kDualWieldSpellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -623,6 +635,7 @@ static void RegisterLuaFunctions() {
|
||||
&Script_FindSpellBookSlotByID);
|
||||
Game::Lua::RegisterGlobalFunction("IsPassiveSpell", &Script_IsPassiveSpell);
|
||||
Game::Lua::RegisterGlobalFunction("IsPlayerSpell", &Script_IsPlayerSpell);
|
||||
Game::Lua::RegisterGlobalFunction("CanDualWield", &Script_CanDualWield);
|
||||
Game::Lua::RegisterGlobalFunction("IsSpellKnown", &Script_IsSpellKnown);
|
||||
Game::Lua::RegisterGlobalFunction("IsHarmfulSpell", &Script_IsHarmfulSpell);
|
||||
Game::Lua::RegisterGlobalFunction("IsHelpfulSpell", &Script_IsHelpfulSpell);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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 "Game.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace Table::Count {
|
||||
|
||||
// `numTableNodes, numArrayNodes, maxArrayIndex = table.count(tbl)` — a modern
|
||||
// WoW diagnostic (added retail 11.2.5) reporting how a table is populated.
|
||||
// These are COUNTS of live entries, not the table's allocated capacity:
|
||||
//
|
||||
// numTableNodes total number of key/value pairs.
|
||||
// numArrayNodes count of pairs whose key is an integer in [1..numTableNodes]
|
||||
// (the "looks like a contiguous array part" heuristic).
|
||||
// maxArrayIndex the largest positive integral key (>= 1), or 0 if none.
|
||||
// Verified against retail: negative/zero integral keys do
|
||||
// NOT count ({[-3]=x} -> 1, 0, 0).
|
||||
//
|
||||
// Pure iteration — no Lua-internal struct layout needed. Note we only ever
|
||||
// read a key via ToNumber, never ToString: lua_tostring on a NUMERIC key
|
||||
// rewrites it to a string in place and corrupts lua_next's traversal.
|
||||
static int __fastcall Script_table_count(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: table.count(table)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<double> integralKeys;
|
||||
int total = 0;
|
||||
double maxKey = 0.0; // largest positive integral key seen; 0 = none
|
||||
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, 1) != 0) {
|
||||
++total;
|
||||
// key at -2, value at -1.
|
||||
if (Game::Lua::Type(L, -2) == Game::Lua::TYPE_NUMBER) {
|
||||
const double k = Game::Lua::ToNumber(L, -2);
|
||||
if (k == std::floor(k)) { // integral key
|
||||
integralKeys.push_back(k);
|
||||
if (k >= 1.0 && k > maxKey) // only positive keys count
|
||||
maxKey = k;
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTop(L, -2); // pop value, keep key for the next Next()
|
||||
}
|
||||
|
||||
int numArray = 0;
|
||||
for (double k : integralKeys)
|
||||
if (k >= 1.0 && k <= static_cast<double>(total))
|
||||
++numArray;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(numArray));
|
||||
Game::Lua::PushNumber(L, maxKey);
|
||||
return 3;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("table", "count", &Script_table_count);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Table::Count
|
||||
@@ -62,7 +62,7 @@
|
||||
#include "item/Location.h"
|
||||
#include "item/StatAccum.h"
|
||||
#include "item/TooltipItem.h"
|
||||
#include "tooltip/SetItemEvent.h"
|
||||
#include "tooltip/SetEvents.h"
|
||||
#include "ui/ColorData.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -598,7 +598,7 @@ int __fastcall Script_SetHyperlinkCompareItem(void *L) {
|
||||
// this internal build so a handler doesn't run mid-way through the
|
||||
// line-shift below (it fires for the *hovered* item's real Set* instead).
|
||||
{
|
||||
Tooltip::SetItemEvent::Suppressor noEvent;
|
||||
Tooltip::SetEvents::Suppressor noEvent;
|
||||
reinterpret_cast<BuildItemTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_ITEM)(
|
||||
self, static_cast<uint32_t>(equippedID), &equippedGUID, &equippedGUID, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
// 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/>.
|
||||
|
||||
// `GameTooltip:HookScript("OnTooltipSet{Item,Spell,Unit,GameObject}", fn)` —
|
||||
// backport of the modern tooltip scripts that fire whenever a tooltip's item /
|
||||
// spell / unit / gameobject is set, so addons can annotate tooltips by hooking
|
||||
// one script instead of wrapping every Set* method. Fully native — real frame
|
||||
// scripts, settable via the standard SetScript / GetScript / HookScript.
|
||||
//
|
||||
// 1.12 already has the machinery for tooltip scripts, just not these ones:
|
||||
//
|
||||
// - The CGGameTooltip vtable resolver FUN_GAMETOOLTIP_SCRIPT_RESOLVER maps a
|
||||
// script name to its handler slot on the tooltip (OnTooltipCleared etc.,
|
||||
// each an 8-byte {handler, context} pair). SetScript/GetScript/HookScript
|
||||
// all go through it. We co-hook it: for our OnTooltipSet* names (which the
|
||||
// engine doesn't know) we hand back a pointer to a C-side per-tooltip cell
|
||||
// — the tooltip object (alloc size 0x460) has no free 8-byte slot, and the
|
||||
// resolver's only contract is "return the address the script lives at", so
|
||||
// external storage works exactly like an in-object slot.
|
||||
//
|
||||
// - Each object type has a single builder funnel that every Set*/mouseover
|
||||
// path converges on (item FUN_GAMETOOLTIP_BUILD_ITEM, spell _BUILD_SPELL,
|
||||
// unit _BUILD_UNIT, gameobject _BUILD_GAMEOBJECT). We co-hook each and,
|
||||
// after the build, fire the matching handler via FUN_FRAME_INVOKE_SCRIPT
|
||||
// (the same no-arg runner the clear uses for OnTooltipCleared) with the
|
||||
// tooltip as `self`.
|
||||
//
|
||||
// Suppressed during our own Tooltip::Compare equipped-item build (see
|
||||
// Suppressor), and guarded against a handler that re-enters a builder.
|
||||
|
||||
#include "SetEvents.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Tooltip::SetEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
// One entry per script kind. Order is arbitrary but the cell slot array and
|
||||
// the builder hooks index by it.
|
||||
enum ScriptKind { SK_ITEM = 0, SK_SPELL, SK_UNIT, SK_GAMEOBJECT, SK_COUNT };
|
||||
|
||||
// Script names, matched case-insensitively against the resolver's input.
|
||||
constexpr const char *kNamesLower[SK_COUNT] = {
|
||||
"ontooltipsetitem",
|
||||
"ontooltipsetspell",
|
||||
"ontooltipsetunit",
|
||||
"ontooltipsetgameobject",
|
||||
};
|
||||
|
||||
// Per-tooltip storage: one 8-byte {handler, context} slot per script kind.
|
||||
// Tooltips are few and permanent (GameTooltip, ItemRefTooltip,
|
||||
// ShoppingTooltip1/2, AtlasLootTooltip, WorldMapTooltip, plus the odd addon
|
||||
// tooltip), so a small fixed table with a linear scan is plenty and gives each
|
||||
// cell a stable address to hand out.
|
||||
struct Cell {
|
||||
void *tooltip;
|
||||
uint32_t slot[SK_COUNT][2]; // [kind] = {handler, exec context}
|
||||
};
|
||||
constexpr int kMaxTooltips = 32;
|
||||
Cell g_cells[kMaxTooltips];
|
||||
int g_cellCount = 0;
|
||||
|
||||
uint32_t *SlotFor(void *tooltip, int kind, bool create) {
|
||||
for (int i = 0; i < g_cellCount; ++i)
|
||||
if (g_cells[i].tooltip == tooltip)
|
||||
return g_cells[i].slot[kind];
|
||||
if (!create || g_cellCount >= kMaxTooltips)
|
||||
return nullptr;
|
||||
Cell &c = g_cells[g_cellCount++];
|
||||
c.tooltip = tooltip;
|
||||
for (auto &s : c.slot) {
|
||||
s[0] = 0;
|
||||
s[1] = 0;
|
||||
}
|
||||
return c.slot[kind];
|
||||
}
|
||||
|
||||
bool EqualsIgnoreCase(const char *s, const char *literal) {
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
for (;; ++s, ++literal) {
|
||||
unsigned char a = static_cast<unsigned char>(*s);
|
||||
const unsigned char b = static_cast<unsigned char>(*literal);
|
||||
if (a >= 'A' && a <= 'Z')
|
||||
a = static_cast<unsigned char>(a + 32);
|
||||
if (a != b)
|
||||
return false;
|
||||
if (b == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Suppression / recursion state -------------------------------------
|
||||
int g_suppress = 0; // > 0 while our own compare build runs
|
||||
int g_firing = 0; // recursion guard around any handler fire
|
||||
|
||||
// The engine's real script invoker: binds the global `this` = frame and runs
|
||||
// the handler under its own protected lua_pcall. We call it directly rather
|
||||
// than the FUN_00702690 wrapper, whose exec-context mutation is unsafe from the
|
||||
// deep nested Lua->C->Lua stack our co-hook fires within — see Offsets.h.
|
||||
using InvokeScript_t = void(__fastcall *)(uint32_t handler, void *frame);
|
||||
|
||||
// Fire the handler (if any) for `kind` on `self`.
|
||||
//
|
||||
// We fire from inside a builder, which is itself mid-execution of the engine's
|
||||
// Script_Set* (and, in the wild, nested under addon Set* wrappers + several
|
||||
// other DLLs' hooks). The invoker pushes onto the Lua stack and does NOT
|
||||
// restore the top to where the outer, still-running C code left it — harmless
|
||||
// from the engine's top-level dispatch, but from here it shifts the stack and
|
||||
// the outer code (and other DLLs' hooks) then index garbage → deref → crash.
|
||||
// Snapshot and restore the stack top around the fire so the outer code sees
|
||||
// exactly the stack it left.
|
||||
void FireScript(void *self, int kind) {
|
||||
if (g_suppress > 0 || g_firing > 0)
|
||||
return;
|
||||
uint32_t *slot = SlotFor(self, kind, /*create*/ false);
|
||||
if (slot == nullptr || slot[0] == 0)
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
++g_firing;
|
||||
reinterpret_cast<InvokeScript_t>(Offsets::FUN_FRAME_INVOKE_SCRIPT)(slot[0], self);
|
||||
--g_firing;
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
// --- Resolver co-hook (CGGameTooltip vtable script-name → slot) ---------
|
||||
using Resolver_t = int(__fastcall *)(void *self, void *edx, const char *name);
|
||||
Resolver_t g_resolverOriginal = nullptr;
|
||||
|
||||
int __fastcall Resolver_h(void *self, void *edx, const char *name) {
|
||||
const int engineSlot = g_resolverOriginal(self, edx, name);
|
||||
if (engineSlot != 0) // a base-frame or existing tooltip script — leave it
|
||||
return engineSlot;
|
||||
for (int k = 0; k < SK_COUNT; ++k)
|
||||
if (EqualsIgnoreCase(name, kNamesLower[k]))
|
||||
return reinterpret_cast<int>(SlotFor(self, k, /*create*/ true));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Builder co-hooks --------------------------------------------------
|
||||
// Each builder is a __thiscall funnel modelled as __fastcall(self /*ecx*/,
|
||||
// edx /*unused reg placeholder*/, ...real stack args...). The declared stack
|
||||
// arg count MUST match the callee's RET N or the caller's stack is corrupted.
|
||||
// Builders whose callers use the return value (spell, unit) are declared to
|
||||
// return it so the co-hook forwards it.
|
||||
|
||||
// Item — FUN_GAMETOOLTIP_BUILD_ITEM, 9 stack args, return unused.
|
||||
using BuildItem_t = void(__fastcall *)(void *self, void *edx, uint32_t itemID,
|
||||
const void *guid, const void *guid2, int a4,
|
||||
int a5, int a6, int headerFlag, int a8, int a9);
|
||||
BuildItem_t g_buildItemOriginal = nullptr;
|
||||
|
||||
void __fastcall BuildItem_h(void *self, void *edx, uint32_t itemID, const void *guid,
|
||||
const void *guid2, int a4, int a5, int a6, int headerFlag,
|
||||
int a8, int a9) {
|
||||
g_buildItemOriginal(self, edx, itemID, guid, guid2, a4, a5, a6, headerFlag, a8, a9);
|
||||
FireScript(self, SK_ITEM);
|
||||
}
|
||||
|
||||
// Spell — FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP, 7 stack args, returns an int.
|
||||
using BuildSpell_t = int(__fastcall *)(void *self, void *edx, int spellID, int a2,
|
||||
int a3, int isPet, int a5, int a6, int a7);
|
||||
BuildSpell_t g_buildSpellOriginal = nullptr;
|
||||
|
||||
int __fastcall BuildSpell_h(void *self, void *edx, int spellID, int a2, int a3,
|
||||
int isPet, int a5, int a6, int a7) {
|
||||
const int ret = g_buildSpellOriginal(self, edx, spellID, a2, a3, isPet, a5, a6, a7);
|
||||
FireScript(self, SK_SPELL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Unit — FUN_GAMETOOLTIP_BUILD_UNIT, 1 stack arg (guid), returns an int the
|
||||
// caller (Script_GameTooltip_SetUnit) tests.
|
||||
using BuildUnit_t = int(__fastcall *)(void *self, void *edx, const void *guid);
|
||||
BuildUnit_t g_buildUnitOriginal = nullptr;
|
||||
|
||||
int __fastcall BuildUnit_h(void *self, void *edx, const void *guid) {
|
||||
const int ret = g_buildUnitOriginal(self, edx, guid);
|
||||
FireScript(self, SK_UNIT);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// GameObject — FUN_GAMETOOLTIP_BUILD_GAMEOBJECT, 1 stack arg (guid), void.
|
||||
using BuildGO_t = void(__fastcall *)(void *self, void *edx, const void *guid);
|
||||
BuildGO_t g_buildGOOriginal = nullptr;
|
||||
|
||||
void __fastcall BuildGO_h(void *self, void *edx, const void *guid) {
|
||||
g_buildGOOriginal(self, edx, guid);
|
||||
FireScript(self, SK_GAMEOBJECT);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _resolverHook{
|
||||
Offsets::FUN_GAMETOOLTIP_SCRIPT_RESOLVER,
|
||||
reinterpret_cast<void *>(&Resolver_h),
|
||||
reinterpret_cast<void **>(&g_resolverOriginal)};
|
||||
|
||||
static const Game::HookAutoRegister _buildItemHook{
|
||||
Offsets::FUN_GAMETOOLTIP_BUILD_ITEM,
|
||||
reinterpret_cast<void *>(&BuildItem_h),
|
||||
reinterpret_cast<void **>(&g_buildItemOriginal)};
|
||||
|
||||
static const Game::HookAutoRegister _buildSpellHook{
|
||||
Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP,
|
||||
reinterpret_cast<void *>(&BuildSpell_h),
|
||||
reinterpret_cast<void **>(&g_buildSpellOriginal)};
|
||||
|
||||
static const Game::HookAutoRegister _buildUnitHook{
|
||||
Offsets::FUN_GAMETOOLTIP_BUILD_UNIT,
|
||||
reinterpret_cast<void *>(&BuildUnit_h),
|
||||
reinterpret_cast<void **>(&g_buildUnitOriginal)};
|
||||
|
||||
static const Game::HookAutoRegister _buildGOHook{
|
||||
Offsets::FUN_GAMETOOLTIP_BUILD_GAMEOBJECT,
|
||||
reinterpret_cast<void *>(&BuildGO_h),
|
||||
reinterpret_cast<void **>(&g_buildGOOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
Suppressor::Suppressor() { ++g_suppress; }
|
||||
Suppressor::~Suppressor() {
|
||||
if (g_suppress > 0)
|
||||
--g_suppress;
|
||||
}
|
||||
|
||||
} // namespace Tooltip::SetEvents
|
||||
@@ -13,12 +13,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Tooltip::SetItemEvent {
|
||||
namespace Tooltip::SetEvents {
|
||||
|
||||
// RAII guard that suppresses OnTooltipSetItem firing for the duration of an
|
||||
// engine item-build we invoke *internally* (Tooltip::Compare building the
|
||||
// equipped item into a shopping tooltip). Without it, a handler would run in
|
||||
// the middle of our line-shift / delta rendering and see a half-built tooltip.
|
||||
// RAII guard that suppresses tooltip set-event firing (OnTooltipSetItem etc.)
|
||||
// for the duration of an engine build we invoke *internally* — currently
|
||||
// Tooltip::Compare building the equipped item into a shopping tooltip. Without
|
||||
// it, a handler would run in the middle of our line-shift / delta rendering and
|
||||
// see a half-built tooltip. Only the item builder runs during that internal
|
||||
// build, so in practice this gates OnTooltipSetItem; it's global for safety.
|
||||
// Nestable (ref-counted).
|
||||
struct Suppressor {
|
||||
Suppressor();
|
||||
@@ -27,4 +29,4 @@ struct Suppressor {
|
||||
Suppressor &operator=(const Suppressor &) = delete;
|
||||
};
|
||||
|
||||
} // namespace Tooltip::SetItemEvent
|
||||
} // namespace Tooltip::SetEvents
|
||||
@@ -1,168 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
// `GameTooltip:HookScript("OnTooltipSetItem", fn)` — backport of the modern
|
||||
// tooltip script that fires whenever a tooltip's item is set, so addons can
|
||||
// annotate item tooltips by hooking one script instead of wrapping every
|
||||
// Set* method (the pattern AtlasLoot resorts to in vanilla, rebuilding the
|
||||
// tooltip and clobbering its item linkage). Fully native — a real frame
|
||||
// script, settable via the standard SetScript / GetScript / HookScript.
|
||||
//
|
||||
// 1.12 already has the machinery for tooltip scripts, just not this one:
|
||||
//
|
||||
// - The CGGameTooltip vtable resolver FUN_GAMETOOLTIP_SCRIPT_RESOLVER maps a
|
||||
// script name to its handler slot on the tooltip (OnTooltipCleared etc.,
|
||||
// each an 8-byte {handler, context} pair). SetScript/GetScript/HookScript
|
||||
// all go through it. We co-hook it: for "OnTooltipSetItem" (a name the
|
||||
// engine doesn't know) we hand back a pointer to a C-side per-tooltip cell
|
||||
// — the tooltip object (alloc size 0x460) has no free 8-byte slot, and the
|
||||
// resolver's only contract is "return the address the script lives at", so
|
||||
// external storage works exactly like an in-object slot.
|
||||
//
|
||||
// - Every item setter (SetHyperlink item:, SetBagItem, SetInventoryItem,
|
||||
// SetMerchantItem, SetAuctionItem, …) funnels through the shared item
|
||||
// builder FUN_GAMETOOLTIP_BUILD_ITEM. We co-hook it and, after the build,
|
||||
// fire the handler via FUN_FRAME_RUN_SCRIPT (the same no-arg runner the
|
||||
// clear uses for OnTooltipCleared) with the tooltip as `self`.
|
||||
//
|
||||
// Suppressed during our own Tooltip::Compare equipped-item build (see
|
||||
// Suppressor), and guarded against a handler that re-enters the builder.
|
||||
|
||||
#include "SetItemEvent.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Tooltip::SetItemEvent {
|
||||
|
||||
namespace {
|
||||
|
||||
// Per-tooltip 8-byte handler slot {handler, context}. Tooltips are few and
|
||||
// permanent (GameTooltip, ItemRefTooltip, ShoppingTooltip1/2, AtlasLootTooltip,
|
||||
// WorldMapTooltip, plus the odd addon tooltip), so a small fixed table with a
|
||||
// linear scan is plenty and gives each cell a stable address to hand out.
|
||||
struct Cell {
|
||||
void *tooltip;
|
||||
uint32_t slot[2]; // [0] = handler, [1] = exec context (written by SetScript)
|
||||
};
|
||||
constexpr int kMaxTooltips = 32;
|
||||
Cell g_cells[kMaxTooltips];
|
||||
int g_cellCount = 0;
|
||||
|
||||
uint32_t *CellFor(void *tooltip, bool create) {
|
||||
for (int i = 0; i < g_cellCount; ++i)
|
||||
if (g_cells[i].tooltip == tooltip)
|
||||
return g_cells[i].slot;
|
||||
if (!create || g_cellCount >= kMaxTooltips)
|
||||
return nullptr;
|
||||
Cell &c = g_cells[g_cellCount++];
|
||||
c.tooltip = tooltip;
|
||||
c.slot[0] = 0;
|
||||
c.slot[1] = 0;
|
||||
return c.slot;
|
||||
}
|
||||
|
||||
bool EqualsIgnoreCase(const char *s, const char *literal) {
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
for (;; ++s, ++literal) {
|
||||
unsigned char a = static_cast<unsigned char>(*s);
|
||||
const unsigned char b = static_cast<unsigned char>(*literal);
|
||||
if (a >= 'A' && a <= 'Z')
|
||||
a = static_cast<unsigned char>(a + 32);
|
||||
if (a != b)
|
||||
return false;
|
||||
if (b == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr char kScriptNameLower[] = "ontooltipsetitem"; // matched case-insensitively
|
||||
|
||||
// --- Suppression / recursion state -------------------------------------
|
||||
int g_suppress = 0; // > 0 while our own compare build runs
|
||||
int g_firing = 0; // recursion guard around the handler fire
|
||||
|
||||
// --- Resolver co-hook (CGGameTooltip vtable script-name → slot) ---------
|
||||
using Resolver_t = int(__fastcall *)(void *self, void *edx, const char *name);
|
||||
Resolver_t g_resolverOriginal = nullptr;
|
||||
|
||||
int __fastcall Resolver_h(void *self, void *edx, const char *name) {
|
||||
const int engineSlot = g_resolverOriginal(self, edx, name);
|
||||
if (engineSlot != 0) // a base-frame or existing tooltip script — leave it
|
||||
return engineSlot;
|
||||
if (EqualsIgnoreCase(name, kScriptNameLower))
|
||||
return reinterpret_cast<int>(CellFor(self, /*create*/ true));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Item-builder co-hook (fires the handler after the item is set) -----
|
||||
using Build_t = void(__fastcall *)(void *self, void *edx, uint32_t itemID, const void *guid,
|
||||
const void *guid2, int a4, int a5, int a6, int headerFlag,
|
||||
int a8, int a9);
|
||||
Build_t g_buildOriginal = nullptr;
|
||||
|
||||
// The engine's real script invoker: binds the global `this` = frame and runs
|
||||
// the handler under its own protected lua_pcall. We call it directly rather
|
||||
// than the FUN_00702690 wrapper, whose exec-context mutation is unsafe from the
|
||||
// deep nested Lua->C->Lua stack our co-hook fires within — see Offsets.h.
|
||||
using InvokeScript_t = void(__fastcall *)(uint32_t handler, void *frame);
|
||||
|
||||
void __fastcall Build_h(void *self, void *edx, uint32_t itemID, const void *guid,
|
||||
const void *guid2, int a4, int a5, int a6, int headerFlag, int a8,
|
||||
int a9) {
|
||||
g_buildOriginal(self, edx, itemID, guid, guid2, a4, a5, a6, headerFlag, a8, a9);
|
||||
if (g_suppress > 0 || g_firing > 0)
|
||||
return;
|
||||
uint32_t *slot = CellFor(self, /*create*/ false);
|
||||
if (slot == nullptr || slot[0] == 0)
|
||||
return;
|
||||
|
||||
// We fire from inside the item builder, which is itself mid-execution of
|
||||
// the engine's Script_Set* (and, in the wild, nested under addon Set*
|
||||
// wrappers + several other DLLs' hooks). The invoker pushes onto the Lua
|
||||
// stack and does NOT restore the top to where the outer, still-running C
|
||||
// code left it — harmless from the engine's top-level dispatch, but from
|
||||
// here it shifts the stack and the outer code (and other DLLs' hooks) then
|
||||
// index garbage → deref → crash. Snapshot and restore the stack top around
|
||||
// the fire so the outer code sees exactly the stack it left.
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
++g_firing;
|
||||
reinterpret_cast<InvokeScript_t>(Offsets::FUN_FRAME_INVOKE_SCRIPT)(slot[0], self);
|
||||
--g_firing;
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _resolverHook{
|
||||
Offsets::FUN_GAMETOOLTIP_SCRIPT_RESOLVER,
|
||||
reinterpret_cast<void *>(&Resolver_h),
|
||||
reinterpret_cast<void **>(&g_resolverOriginal)};
|
||||
|
||||
static const Game::HookAutoRegister _buildHook{
|
||||
Offsets::FUN_GAMETOOLTIP_BUILD_ITEM,
|
||||
reinterpret_cast<void *>(&Build_h),
|
||||
reinterpret_cast<void **>(&g_buildOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
Suppressor::Suppressor() { ++g_suppress; }
|
||||
Suppressor::~Suppressor() {
|
||||
if (g_suppress > 0)
|
||||
--g_suppress;
|
||||
}
|
||||
|
||||
} // namespace Tooltip::SetItemEvent
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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/>.
|
||||
|
||||
// UnitPosition backport.
|
||||
//
|
||||
// `UnitPosition("unit") → posY, posX, posZ, instanceID` — the modern
|
||||
// (WoD+) world-position accessor, ported to 1.12. The world position of
|
||||
// any visible unit already lives on its CGObject; we read it through the
|
||||
// shared `Unit::Position` helper (the object's GetPosition virtual, vtable
|
||||
// slot 5 — the same path UnitDistanceSquared / UnitInRange use).
|
||||
//
|
||||
// Two semantic notes:
|
||||
//
|
||||
// - **Order matches retail.** The engine's C3Vector is {x, y, z} with the
|
||||
// standard WoW convention (x = north, y = west, z = up). Retail's
|
||||
// UnitPosition returns (posY, posX, posZ) — west, north, up — so we push
|
||||
// the engine's y, then x, then z. An addon ported from retail that reads
|
||||
// `posY, posX = UnitPosition(u)` gets coordinates labelled exactly as it
|
||||
// expects.
|
||||
// - **No group restriction.** Retail returns nil for units outside your
|
||||
// party/raid (a taint/privacy guard that doesn't exist in 1.12). We can
|
||||
// read any *visible* unit, so this is strictly more permissive — closer
|
||||
// to SuperWoW's unit-position access than to retail's restricted form.
|
||||
// Units the client can't see (out of range, different zone) have no
|
||||
// known position → nil.
|
||||
//
|
||||
// `instanceID` is the currently-loaded Map.dbc id (VAR_CURRENT_MAP_ID) —
|
||||
// every visible unit shares the player's instance, so the player's map id
|
||||
// is the unit's instance id.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::PositionApi {
|
||||
|
||||
namespace {
|
||||
|
||||
// `UnitPosition("unit") → posY, posX, posZ, instanceID`. nil when the unit
|
||||
// has no known position (recognized-but-absent token, or a visible-object
|
||||
// miss). Genuinely unrecognized token strings raise a Lua error, same as
|
||||
// UnitHealth("garbage") — the engine's resolver owns that contract.
|
||||
int __fastcall Script_UnitPosition(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitPosition(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(pos[1])); // posY (world Y, west)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(pos[0])); // posX (world X, north)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(pos[2])); // posZ (up)
|
||||
const int mapId = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(mapId));
|
||||
return 4;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitPosition", &Script_UnitPosition);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerInfo", "UnitPosition",
|
||||
&Script_UnitPosition);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::PositionApi
|
||||
Reference in New Issue
Block a user