mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba4520493a | ||
|
|
dc80ade21b | ||
|
|
6addd6d5b1 |
@@ -14,6 +14,7 @@ Util\Constants.lua
|
||||
Util\String.lua
|
||||
Util\TableUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\PixelUtil.lua
|
||||
Util\FrameUtil.lua
|
||||
Util\FormattingUtil.lua
|
||||
Util\TimeUtil.lua
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
-- Backport of FrameXML/PixelUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- PixelUtil snaps frame sizes/points to whole physical pixels so
|
||||
-- borders and textures stay crisp at fractional UI scales. The table
|
||||
-- ports verbatim — its only math deps (Round, Lerp,
|
||||
-- ClampedPercentageBetween) all live in MathUtil, which loads first.
|
||||
--
|
||||
-- The one piece vanilla lacks is the native `GetPhysicalScreenSize()`
|
||||
-- (added in 7.0). The ClassicAPI DLL provides it (see
|
||||
-- src/screen/Info.cpp) by reading the engine's `gxResolution` CVar, so
|
||||
-- it's a real global registered at engine boot — no Lua polyfill
|
||||
-- needed here.
|
||||
|
||||
PixelUtil = {};
|
||||
|
||||
function PixelUtil.GetPixelToUIUnitFactor()
|
||||
local physicalWidth, physicalHeight = GetPhysicalScreenSize();
|
||||
return 768.0 / physicalHeight;
|
||||
end
|
||||
|
||||
function PixelUtil.GetNearestPixelSize(uiUnitSize, layoutScale, minPixels)
|
||||
if uiUnitSize == 0 and (not minPixels or minPixels == 0) then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local uiUnitFactor = PixelUtil.GetPixelToUIUnitFactor();
|
||||
local numPixels = Round((uiUnitSize * layoutScale) / uiUnitFactor);
|
||||
if minPixels then
|
||||
if uiUnitSize < 0.0 then
|
||||
if numPixels > -minPixels then
|
||||
numPixels = -minPixels;
|
||||
end
|
||||
else
|
||||
if numPixels < minPixels then
|
||||
numPixels = minPixels;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return numPixels * uiUnitFactor / layoutScale;
|
||||
end
|
||||
|
||||
function PixelUtil.ConvertPixelsToUI(desiredPixels, layoutScale)
|
||||
return PixelUtil.GetNearestPixelSize(desiredPixels, layoutScale);
|
||||
end
|
||||
|
||||
function PixelUtil.ConvertPixelsToUIForRegion(desiredPixels, region)
|
||||
return PixelUtil.GetNearestPixelSize(desiredPixels, region:GetEffectiveScale());
|
||||
end
|
||||
|
||||
function PixelUtil.SetWidth(region, width, minPixels)
|
||||
region:SetWidth(PixelUtil.GetNearestPixelSize(width, region:GetEffectiveScale(), minPixels));
|
||||
end
|
||||
|
||||
function PixelUtil.SetHeight(region, height, minPixels)
|
||||
region:SetHeight(PixelUtil.GetNearestPixelSize(height, region:GetEffectiveScale(), minPixels));
|
||||
end
|
||||
|
||||
function PixelUtil.SetSize(region, width, height, minWidthPixels, minHeightPixels)
|
||||
PixelUtil.SetWidth(region, width, minWidthPixels);
|
||||
PixelUtil.SetHeight(region, height, minHeightPixels);
|
||||
end
|
||||
|
||||
function PixelUtil.SetPoint(region, point, relativeTo, relativePoint, offsetX, offsetY, minOffsetXPixels, minOffsetYPixels)
|
||||
region:SetPoint(point, relativeTo, relativePoint,
|
||||
PixelUtil.GetNearestPixelSize(offsetX, region:GetEffectiveScale(), minOffsetXPixels),
|
||||
PixelUtil.GetNearestPixelSize(offsetY, region:GetEffectiveScale(), minOffsetYPixels)
|
||||
);
|
||||
end
|
||||
|
||||
function PixelUtil.SetStatusBarValue(statusBar, value)
|
||||
local width = statusBar:GetWidth();
|
||||
if width and width > 0.0 then
|
||||
local min, max = statusBar:GetMinMaxValues();
|
||||
local percent = ClampedPercentageBetween(value, min, max);
|
||||
if percent == 0.0 or percent == 1.0 then
|
||||
statusBar:SetValue(value);
|
||||
else
|
||||
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetWidth() * percent, statusBar:GetEffectiveScale());
|
||||
local roundedValue = Lerp(min, max, numPixels / width);
|
||||
statusBar:SetValue(roundedValue);
|
||||
end
|
||||
else
|
||||
statusBar:SetValue(value);
|
||||
end
|
||||
end
|
||||
@@ -1,16 +1,3 @@
|
||||
-- Backport of FrameXML/TimeUtil.lua (feasible subset) to vanilla 1.12 /
|
||||
-- Lua 5.0.
|
||||
--
|
||||
-- Ports the deprecated-but-widely-used duration formatters (SecondsToTime,
|
||||
-- SecondsToClock, SecondsToTimeAbbrev) plus the unit constants/helpers. The
|
||||
-- SecondsFormatterMixin (the modern replacement) is omitted — it's a heavy
|
||||
-- mixin class little addon code targets on classic.
|
||||
--
|
||||
-- 5.0 notes: `%` -> math.mod, `s:format()` -> string.format. The FrameXML
|
||||
-- format-string globals these functions consume (DAYS_ABBR, D_HOURS,
|
||||
-- HOURS_MINUTES_SECONDS, the *_ONELETTER_ABBR set, …) are defined in the
|
||||
-- locale layer (locales/enUS.lua), which loads first.
|
||||
|
||||
SECONDS_PER_MIN = 60;
|
||||
SECONDS_PER_HOUR = 60 * SECONDS_PER_MIN;
|
||||
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
|
||||
@@ -52,67 +39,3 @@ function SecondsToClock(seconds, displayZeroHours)
|
||||
return string.format(MINUTES_SECONDS, units.minutes, units.seconds);
|
||||
end
|
||||
end
|
||||
|
||||
-- 1.12's native unit strings are bare labels ("Hr", "Min") with no `%d`,
|
||||
-- unlike modern's "%d Hr"; insert the count correctly for either shape.
|
||||
local function FormatUnit(fmt, count)
|
||||
if string.find(fmt, "%%d") then
|
||||
return string.format(fmt, count);
|
||||
end
|
||||
return count .. " " .. fmt;
|
||||
end
|
||||
|
||||
function SecondsToTime(seconds, noSeconds, notAbbreviated, maxCount, roundUp)
|
||||
local time = "";
|
||||
local count = 0;
|
||||
local tempTime;
|
||||
seconds = roundUp and math.ceil(seconds) or math.floor(seconds);
|
||||
maxCount = maxCount or 2;
|
||||
|
||||
-- With a single term, use a higher 1.5x threshold; with 2+, 1.0x.
|
||||
local threshold = (maxCount > 1) and 1.0 or 1.5;
|
||||
|
||||
if seconds >= SECONDS_PER_DAY * threshold then
|
||||
count = count + 1;
|
||||
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_DAY)
|
||||
or math.floor(seconds / SECONDS_PER_DAY);
|
||||
time = FormatUnit(notAbbreviated and D_DAYS or DAYS_ABBR, tempTime);
|
||||
seconds = math.mod(seconds, SECONDS_PER_DAY);
|
||||
end
|
||||
if count < maxCount and seconds >= SECONDS_PER_HOUR * threshold then
|
||||
count = count + 1;
|
||||
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
|
||||
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_HOUR)
|
||||
or math.floor(seconds / SECONDS_PER_HOUR);
|
||||
time = time .. FormatUnit(notAbbreviated and D_HOURS or HOURS_ABBR, tempTime);
|
||||
seconds = math.mod(seconds, SECONDS_PER_HOUR);
|
||||
end
|
||||
if count < maxCount and seconds >= SECONDS_PER_MIN * threshold then
|
||||
count = count + 1;
|
||||
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
|
||||
tempTime = (count == maxCount and roundUp) and math.ceil(seconds / SECONDS_PER_MIN)
|
||||
or math.floor(seconds / SECONDS_PER_MIN);
|
||||
time = time .. FormatUnit(notAbbreviated and D_MINUTES or MINUTES_ABBR, tempTime);
|
||||
seconds = math.mod(seconds, SECONDS_PER_MIN);
|
||||
end
|
||||
if count < maxCount and seconds > 0 and not noSeconds then
|
||||
if time ~= "" then time = time .. TIME_UNIT_DELIMITER; end
|
||||
time = time .. FormatUnit(notAbbreviated and D_SECONDS or SECONDS_ABBR, seconds);
|
||||
end
|
||||
return time;
|
||||
end
|
||||
|
||||
-- Returns (formatString, value) for the single largest applicable unit.
|
||||
function SecondsToTimeAbbrev(seconds, thresholdOverride)
|
||||
local threshold = thresholdOverride or 1.5;
|
||||
if seconds >= SECONDS_PER_DAY * threshold then
|
||||
return DAY_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_DAY);
|
||||
end
|
||||
if seconds >= SECONDS_PER_HOUR * threshold then
|
||||
return HOUR_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_HOUR);
|
||||
end
|
||||
if seconds >= SECONDS_PER_MIN * threshold then
|
||||
return MINUTE_ONELETTER_ABBR, math.ceil(seconds / SECONDS_PER_MIN);
|
||||
end
|
||||
return SECOND_ONELETTER_ABBR, seconds;
|
||||
end
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
BINDING_NAME_AOELOOT = "AoE Loot"
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
@@ -15,25 +23,3 @@ SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
TIME_UNIT_DELIMITER = " ";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
DAYS_ABBR = "%d |4Day:Days;";
|
||||
HOURS_ABBR = "%d |4Hr:Hr;";
|
||||
MINUTES_ABBR = "%d |4Min:Min;";
|
||||
SECONDS_ABBR = "%d |4Sec:Sec;";
|
||||
D_DAYS = "%d |4Day:Days;";
|
||||
D_HOURS = "%d |4Hour:Hours;";
|
||||
D_MINUTES = "%d |4Minute:Minutes;";
|
||||
D_SECONDS = "%d |4Second:Seconds;";
|
||||
DAY_ONELETTER_ABBR = "%d d";
|
||||
HOUR_ONELETTER_ABBR = "%d h";
|
||||
MINUTE_ONELETTER_ABBR = "%d m";
|
||||
SECOND_ONELETTER_ABBR = "%d s";
|
||||
|
||||
@@ -38,7 +38,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
@@ -47,7 +47,8 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
|
||||
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
|
||||
+95
@@ -201,6 +201,7 @@ build instructions.
|
||||
- [`C_Item.UnlockAllItems()`](#c_itemunlockallitems)
|
||||
- [`C_Item.UnlockItem(itemLocation)`](#c_itemunlockitemitemlocation)
|
||||
- [`C_Item.UseAtCursor(itemInfo)`](#c_itemuseatcursoriteminfo)
|
||||
- [`C_Item.UseAtUnit(itemInfo, unit)`](#c_itemuseatunititeminfo-unit)
|
||||
- [`C_Item.UseItemByName(itemInfo [, unit])`](#c_itemuseitembynameiteminfo--unit)
|
||||
- [`Get*ItemID` — companions to the engine's `Get*ItemLink` family](#getitemid--companions-to-the-engines-getitemlink-family)
|
||||
- [`GetAverageItemLevel()`](#getaverageitemlevel)
|
||||
@@ -269,6 +270,9 @@ build instructions.
|
||||
- [`C_QuestLog.IsQuestDataCachedByID(questID)`](#c_questlogisquestdatacachedbyidquestid)
|
||||
- [`GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])`](#getquestlogleaderboardidobjectiveindex--questindex)
|
||||
|
||||
- [Screen](#screen)
|
||||
- [`GetPhysicalScreenSize()`](#getphysicalscreensize)
|
||||
|
||||
- [Spell](#spell)
|
||||
- [`C_Spell.DoesSpellExist(spellID)`](#c_spelldoesspellexistspellid)
|
||||
- [`C_Spell.GetSchoolString(schoolMask)`](#c_spellgetschoolstringschoolmask)
|
||||
@@ -301,6 +305,7 @@ build instructions.
|
||||
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
|
||||
- [`CastSpellNoToggle(name | spellID)`](#castspellnotogglename--spellid)
|
||||
- [`C_Spell.CastAtCursor(spellID)`](#c_spellcastatcursorspellid)
|
||||
- [`C_Spell.CastAtUnit(spellID, unit)`](#c_spellcastatunitspellid-unit)
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
- [`UnitCastingInfo(unit)` / `CastingInfo()`](#unitcastinginfounit--castinginfo)
|
||||
- [`UnitChannelInfo(unit)` / `ChannelInfo()`](#unitchannelinfounit--channelinfo)
|
||||
@@ -4759,6 +4764,32 @@ Cancels placement automatically when the cursor isn't on terrain —
|
||||
the item-use packet is never sent, so an off-screen click doesn't
|
||||
waste the grenade.
|
||||
|
||||
### `C_Item.UseAtUnit(itemInfo, unit)`
|
||||
|
||||
Unit-position analog of
|
||||
[`C_Item.UseAtCursor`](#c_itemuseatcursoriteminfo): uses `itemInfo` at
|
||||
`unit`'s feet rather than the cursor. ClassicAPI's `[@unit]` for
|
||||
ground-target on-use items. `itemInfo` accepts the same forms as
|
||||
`UseAtCursor` (itemID, bare `"item:N"`, full chat link, localized
|
||||
name); `unit` is any unit token (`"player"`, `"target"`,
|
||||
`"mouseover"`, `"party1"`, …).
|
||||
|
||||
```lua
|
||||
C_Item.UseAtUnit(4068, "target") -- Iron Grenade at the target's feet
|
||||
C_Item.UseAtUnit("Iron Grenade", "player")
|
||||
```
|
||||
|
||||
Same chain as `UseAtCursor` (`Item::Location::FindByArgInBags` +
|
||||
`FUN_ITEM_USE`) but committing the placement at the unit's world
|
||||
position via
|
||||
[`Spell::AtCursor::CommitAtCoords`](#c_spellcastatunitspellid-unit)
|
||||
instead of the cursor raycast. Returns `true` when the placement
|
||||
landed at the unit; `false` for non-ground-target items (the item
|
||||
still fires with any implicit target), unparseable input,
|
||||
item-not-in-bags, or an unresolvable unit. The unit is resolved
|
||||
before the item fires, so an absent unit fails without consuming the
|
||||
item.
|
||||
|
||||
### `C_Item.UseItemByName(itemInfo [, unit])`
|
||||
|
||||
Finds the first item in the player's bags matching `itemInfo` and
|
||||
@@ -6581,6 +6612,40 @@ A separate function keeps the existing call wire-compatible.
|
||||
> first, skip zero slots, then the item array. 1-based `objectiveIndex`
|
||||
> counts only non-empty slots.
|
||||
|
||||
## Screen
|
||||
|
||||
### `GetPhysicalScreenSize()`
|
||||
|
||||
Returns `(widthPixels, heightPixels)` — the display's physical
|
||||
resolution in pixels. Modern WoW added this as a native in 7.0
|
||||
(Legion); FrameXML's `PixelUtil` builds its entire pixel↔UI-unit
|
||||
conversion on it (`768.0 / physicalHeight`), which is what lets addons
|
||||
snap frames to whole pixels for crisp borders at fractional UI scales.
|
||||
|
||||
```lua
|
||||
local w, h = GetPhysicalScreenSize() -- e.g. 1920, 1080
|
||||
```
|
||||
|
||||
Vanilla 1.12 has no such native — and no pre-parsed pixel-dimension
|
||||
global. `GetScreenWidth()`/`GetScreenHeight()` return UI-space units
|
||||
(derived from the UIParent aspect ratio at `[UIParent + 0x7c]`), not
|
||||
pixels. The engine only ever holds the current mode as the
|
||||
`gxResolution` CVar string (`"WIDTHxHEIGHT"`); both `SetScreenResolution`
|
||||
(`0x0048bfd0`) and the display-init path sscanf it on demand rather
|
||||
than caching dimensions anywhere. So we read the `gxResolution` CVar
|
||||
directly through the engine's by-name lookup (`FUN_FIND_CVAR`, value at
|
||||
`+0x20`) and parse it — the same source of truth the engine uses.
|
||||
|
||||
Returns `1024, 768` (→ factor 1.0, making PixelUtil a 1:1 no-op) when
|
||||
the CVar is missing or unparseable. Caveat: in windowed mode
|
||||
`gxResolution` reflects the configured render resolution, which may
|
||||
differ from the OS window's client size — vanilla exposes no separate
|
||||
window-pixel query.
|
||||
|
||||
The companion `PixelUtil` table (`PixelUtil.SetPoint`/`SetSize`/
|
||||
`SetStatusBarValue`/…) is provided Lua-side by the ClassicAPI addon and
|
||||
consumes this function.
|
||||
|
||||
## Spell
|
||||
|
||||
> **All `C_Spell.*` functions in this section accept any spell
|
||||
@@ -7520,6 +7585,36 @@ The companion item version is
|
||||
[`C_Item.UseAtCursor`](#c_itemuseatcursoriteminfo) — same chain via
|
||||
the item-use path for grenades / on-use ground-target items.
|
||||
|
||||
### `C_Spell.CastAtUnit(spellID, 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(spellID, "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
|
||||
spellIDs, or a unit with no resolvable position.
|
||||
|
||||
```lua
|
||||
C_Spell.CastAtUnit(2120, "target") -- Flamestrike at the target's feet
|
||||
C_Spell.CastAtUnit(10, "player") -- Blizzard centered on yourself
|
||||
```
|
||||
|
||||
Same two-stage shape as
|
||||
[`C_Spell.CastAtCursor`](#c_spellcastatcursorspellid), 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 companion item version is
|
||||
[`C_Item.UseAtUnit`](#c_itemuseatunititeminfo-unit).
|
||||
|
||||
### `C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`
|
||||
|
||||
Cancel a buff on the player by spellID or by spell name. Modern WoW's
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.UseAtUnit(itemInfo, unit)` — use the named item at the given
|
||||
// unit's feet. For items whose on-use spell is ground-target (Iron
|
||||
// Grenade, demolition charges, etc.), bypasses the manual click on the
|
||||
// AoE reticle. `UseAtUnit(itemInfo, "target")` drops the reticle at
|
||||
// the target's position.
|
||||
//
|
||||
// Unit-position analog of `C_Item.UseAtCursor` — same bag-walk +
|
||||
// `FUN_ITEM_USE` dispatch, but the placement is committed at the
|
||||
// unit's world position (via `Spell::AtCursor::CommitAtCoords`)
|
||||
// instead of the cursor raycast.
|
||||
//
|
||||
// Returns `true` when the placement landed at the unit; `false` for
|
||||
// non-ground-target items (the item still fires, no implicit target),
|
||||
// unparseable input, item-not-in-bags, or an unresolvable unit.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::UseAtUnit {
|
||||
|
||||
namespace {
|
||||
|
||||
using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targetGuid,
|
||||
int flag);
|
||||
|
||||
int __fastcall Script_C_Item_UseAtUnit(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Resolve the unit position first — fail fast before firing the
|
||||
// item if the unit is absent. (FindByArgInBags below stomps the
|
||||
// Lua stack, so read the token argument before that runs.)
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint64_t zeroTarget = 0;
|
||||
auto useItem = reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE);
|
||||
useItem(found.item, &zeroTarget, 0);
|
||||
|
||||
const bool placed = Spell::AtCursor::CommitAtCoords(pos);
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "UseAtUnit",
|
||||
&Script_C_Item_UseAtUnit);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::UseAtUnit
|
||||
@@ -0,0 +1,111 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetPhysicalScreenSize()` → (widthPixels, heightPixels) — the
|
||||
// display's physical resolution in pixels. Modern WoW added this as a
|
||||
// native in 7.0 (Legion); FrameXML's `PixelUtil` hangs its entire
|
||||
// pixel↔UI-unit conversion off it (`768.0 / physicalHeight`), which is
|
||||
// what lets addons snap frames to whole pixels.
|
||||
//
|
||||
// Vanilla 1.12 has no such native and no pre-parsed pixel-dimension
|
||||
// global — the engine only ever holds the current mode as the
|
||||
// `gxResolution` CVar string ("WIDTHxHEIGHT"). `Script_GetScreenWidth`
|
||||
// / `GetScreenHeight` return UI-space units (derived from the UIParent
|
||||
// aspect ratio at `[UIParent + 0x7c]`), not pixels, so they can't
|
||||
// serve. `SetScreenResolution` (`FUN_0048bfd0`) and the display-init
|
||||
// path both sscanf the `gxResolution` value on demand rather than
|
||||
// caching width/height anywhere — so the CVar string is the
|
||||
// authoritative current-resolution source, and we parse it the same
|
||||
// way the engine does.
|
||||
//
|
||||
// Returns 1024x768 (→ factor 1.0, i.e. 1 pixel == 1 UI unit, making
|
||||
// PixelUtil a no-op pass-through) when the CVar is missing or
|
||||
// unparseable. Caveat: in windowed mode `gxResolution` reflects the
|
||||
// configured render resolution, which may differ from the OS window's
|
||||
// client size — vanilla exposes no separate window-pixel query.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Screen::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
using FindCVar_t = const uint8_t *(__fastcall *)(const char *name);
|
||||
|
||||
// Parses "WIDTHxHEIGHT" (e.g. "1920x1080"). Tolerant of the separator
|
||||
// (any run of non-digits between the two numbers) so an uppercase 'X'
|
||||
// or stray spaces don't break it. Returns false unless both numbers
|
||||
// are present and positive.
|
||||
bool ParseResolution(const char *s, int *outW, int *outH) {
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
|
||||
const char *p = s;
|
||||
int w = 0;
|
||||
bool gotW = false;
|
||||
while (*p >= '0' && *p <= '9') {
|
||||
w = w * 10 + (*p - '0');
|
||||
++p;
|
||||
gotW = true;
|
||||
}
|
||||
if (!gotW)
|
||||
return false;
|
||||
|
||||
while (*p != '\0' && (*p < '0' || *p > '9')) // skip the 'x' separator
|
||||
++p;
|
||||
|
||||
int h = 0;
|
||||
bool gotH = false;
|
||||
while (*p >= '0' && *p <= '9') {
|
||||
h = h * 10 + (*p - '0');
|
||||
++p;
|
||||
gotH = true;
|
||||
}
|
||||
if (!gotH || w <= 0 || h <= 0)
|
||||
return false;
|
||||
|
||||
*outW = w;
|
||||
*outH = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetPhysicalScreenSize(void *L) {
|
||||
int width = 1024;
|
||||
int height = 768; // fallback → 768/768 = factor 1.0 (no-op snapping)
|
||||
|
||||
auto findCVar = reinterpret_cast<FindCVar_t>(Offsets::FUN_FIND_CVAR);
|
||||
const uint8_t *cvar = findCVar("gxResolution");
|
||||
if (cvar != nullptr) {
|
||||
const char *value = *reinterpret_cast<const char *const *>(
|
||||
cvar + Offsets::OFF_CVAR_VALUE_STR);
|
||||
ParseResolution(value, &width, &height);
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, width);
|
||||
Game::Lua::PushNumber(L, height);
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetPhysicalScreenSize",
|
||||
&Script_GetPhysicalScreenSize);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Screen::Info
|
||||
+51
-23
@@ -70,9 +70,17 @@ void CancelPlacement() {
|
||||
fn();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
void Commit(const float coords[3]) {
|
||||
auto commit = reinterpret_cast<CommitPlacementCoords_t>(
|
||||
Offsets::FUN_COMMIT_PLACEMENT_COORDS);
|
||||
commit(coords);
|
||||
}
|
||||
|
||||
bool Resolve() {
|
||||
// Shared placement-readiness gate for both the cursor and explicit-
|
||||
// coords commit paths. Decision tree (preserved from the original
|
||||
// Resolve): no placement → false (no cancel); non-ground placement →
|
||||
// cancel + false; not in world → false; otherwise ready → true.
|
||||
bool PlacementReady() {
|
||||
const uint32_t state = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_SPELL_PLACEMENT_STATE);
|
||||
if (state == 0)
|
||||
@@ -81,12 +89,25 @@ bool Resolve() {
|
||||
if ((state & Offsets::SPELL_PLACEMENT_GROUND_MASK) == 0) {
|
||||
// Spell wants a unit / item / corpse / etc. — not ground.
|
||||
// Cancel so the cursor recovers; caller learns via the false
|
||||
// return that no cursor-placement happened.
|
||||
// return that no placement happened.
|
||||
CancelPlacement();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InWorld())
|
||||
return InWorld();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CommitAtCoords(const float coords[3]) {
|
||||
if (!PlacementReady())
|
||||
return false;
|
||||
Commit(coords);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Resolve() {
|
||||
if (!PlacementReady())
|
||||
return false;
|
||||
|
||||
void *clickInfo = *reinterpret_cast<void *const *>(
|
||||
@@ -114,9 +135,7 @@ bool Resolve() {
|
||||
const float *coords = reinterpret_cast<const float *>(
|
||||
static_cast<const uint8_t *>(clickInfo) + Offsets::OFF_CLICK_INFO_COORDS);
|
||||
|
||||
auto commit = reinterpret_cast<CommitPlacementCoords_t>(
|
||||
Offsets::FUN_COMMIT_PLACEMENT_COORDS);
|
||||
commit(coords);
|
||||
Commit(coords);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,16 +145,11 @@ using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
|
||||
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType,
|
||||
unsigned targetGuidLo, float targetGuidHi);
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (spellID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool DispatchSpellCast(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
|
||||
// Hand the spellID off as a numeric string — our `Spell::CastByID`
|
||||
// hook on `FUN_RESOLVE_SPELL_NAME_TO_SLOT` translates pure-numeric
|
||||
@@ -148,17 +162,31 @@ int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
Offsets::FUN_RESOLVE_SPELL_NAME_TO_SLOT);
|
||||
int bookType = 0;
|
||||
const int slot = nameToSlot(numBuf, &bookType);
|
||||
if (slot < 0) {
|
||||
// Spell not in the player's spellbook — can't cast it from
|
||||
// here either way. Match `CastSpellByName`'s silent failure.
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (slot < 0)
|
||||
return false; // not in the player's spellbook
|
||||
|
||||
auto dispatch = reinterpret_cast<CastDispatch_t>(
|
||||
Offsets::FUN_SPELL_CAST_DISPATCH);
|
||||
dispatch(static_cast<unsigned>(slot), bookType,
|
||||
0, 0.0f); // no implicit target unit; engine handles placement
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
// Spell not in the player's spellbook → match `CastSpellByName`'s
|
||||
// silent failure.
|
||||
if (!DispatchSpellCast(spellID)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool placed = Resolve();
|
||||
Game::Lua::PushBool(L, placed);
|
||||
|
||||
@@ -30,4 +30,22 @@ namespace Spell::AtCursor {
|
||||
// click-info struct pointer.
|
||||
bool Resolve();
|
||||
|
||||
// Commits an in-flight ground-target placement at explicit world
|
||||
// coords (x, y, z) instead of the cursor raycast. Same placement
|
||||
// guards as `Resolve` (no placement active → false; non-ground
|
||||
// placement → cancel + false; not in world → false), but skips the
|
||||
// cursor entirely. Used by `C_Spell.CastAtUnit` / `C_Item.UseAtUnit`
|
||||
// to drop the reticle at a unit's feet. Returns true when the
|
||||
// placement was committed.
|
||||
bool CommitAtCoords(const float coords[3]);
|
||||
|
||||
// Resolves a numeric `spellID` to a spellbook slot and dispatches the
|
||||
// cast through the engine — the shared front half of
|
||||
// `C_Spell.CastAtCursor` / `CastAtUnit`. For a ground-target spell
|
||||
// the engine enters placement mode (commit follows via `Resolve` /
|
||||
// `CommitAtCoords`); for a normal spell it fires immediately. Returns
|
||||
// true if the cast was dispatched (spell is in the player's
|
||||
// spellbook), false otherwise.
|
||||
bool DispatchSpellCast(int spellID);
|
||||
|
||||
} // namespace Spell::AtCursor
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `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.
|
||||
//
|
||||
// Same two-step shape as `C_Spell.CastAtCursor`, but the placement is
|
||||
// committed at the unit's world position instead of the cursor
|
||||
// raycast:
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "Game.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
namespace Spell::AtUnit {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtUnit(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
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")`.)
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!Spell::AtCursor::DispatchSpellCast(spellID)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool placed = Spell::AtCursor::CommitAtCoords(pos);
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "CastAtUnit",
|
||||
&Script_C_Spell_CastAtUnit);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Macro-parser pattern: tag macros calling
|
||||
// `C_Spell.CastAtUnit(<spellID>, ...)` with the spellID so action-bar
|
||||
// UIs highlight the slot. The first arg is the numeric spellID, so the
|
||||
// extractor just reads the leading numeric literal — same shape as
|
||||
// `CastAtCursor`'s.
|
||||
int ExtractCastAtUnitArg(const char *p, const char *end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
int value = 0;
|
||||
const char *digitStart = p;
|
||||
while (p < end && *p >= '0' && *p <= '9') {
|
||||
if (value > 100000000) // sanity cap before overflow
|
||||
return 0;
|
||||
value = value * 10 + (*p - '0');
|
||||
++p;
|
||||
}
|
||||
return (p > digitStart && value > 0) ? value : 0;
|
||||
}
|
||||
|
||||
const Spell::MacroPrimarySpell::PatternAutoRegister _patreg{
|
||||
"C_Spell.CastAtUnit(", &ExtractCastAtUnitArg};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Spell::AtUnit
|
||||
@@ -0,0 +1,68 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
// Shared world-position read for any CGObject-derived unit. The
|
||||
// position comes from the object's `GetPosition` virtual (vtable
|
||||
// slot 5) — the same one `CheckInteractDistance` / `UnitInRange` use.
|
||||
// Header-only so callers (UnitInRange, CastAtUnit, UseAtUnit) don't
|
||||
// each re-derive the vtable-slot math.
|
||||
|
||||
namespace Unit::Position {
|
||||
|
||||
// Resolves a unit token ("player", "target", "party1", "mouseover",
|
||||
// …) to a `CGUnit *`. Returns nullptr for a recognized-but-absent
|
||||
// token (e.g. "party5" with no such member). NOTE: the engine's
|
||||
// resolver raises a Lua error for genuinely unrecognized token
|
||||
// strings — same contract as `UnitHealth("garbage")` — so only feed
|
||||
// it real unit tokens, or strings you're willing to let error.
|
||||
inline void *ResolveToken(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Reads `obj`'s world position into `out[3]` (x, y, z). Calls
|
||||
// `obj->vtable[5](outBuf)`; the returned float* may equal `outBuf`
|
||||
// (object filled it) or point at a cached position field — either
|
||||
// way the values land in `out`. Returns false if `obj` is null or
|
||||
// the virtual yields no position (object has no known position yet).
|
||||
inline bool Read(void *obj, float out[3]) {
|
||||
if (obj == nullptr)
|
||||
return false;
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
using GetPosition_t = float *(__thiscall *)(void *self, float outBuf[3]);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
vtable[Offsets::OFF_CGOBJECT_VTBL_GET_POSITION / 4]);
|
||||
float *p = fn(obj, out);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
if (p != out) {
|
||||
out[0] = p[0];
|
||||
out[1] = p[1];
|
||||
out[2] = p[2];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convenience: resolve a token and read its position in one step.
|
||||
inline bool ReadToken(const char *token, float out[3]) {
|
||||
return Read(ResolveToken(token), out);
|
||||
}
|
||||
|
||||
} // namespace Unit::Position
|
||||
+5
-39
@@ -15,42 +15,17 @@
|
||||
// uses, applied with a fixed 40-yard threshold.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Unit::Range {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetPosition_t = float *(__thiscall *)(void *self, float outBuf[3]);
|
||||
|
||||
constexpr float kRangeYards = 40.0f;
|
||||
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
|
||||
|
||||
void *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Calls `obj->vtable[5](outBuf)`. The returned float* may equal
|
||||
// outBuf (the object filled it directly) or point at a cached
|
||||
// position field on the object — either way, deref it for x/y/z.
|
||||
// Returns nullptr if the object's vtable virtual returns null (no
|
||||
// known position for this object yet).
|
||||
float *ReadPosition(void *obj, float outBuf[3]) {
|
||||
if (obj == nullptr)
|
||||
return nullptr;
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
vtable[Offsets::OFF_CGOBJECT_VTBL_GET_POSITION / 4]);
|
||||
return fn(obj, outBuf);
|
||||
}
|
||||
|
||||
int __fastcall Script_UnitInRange(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitInRange(\"unit\")");
|
||||
@@ -69,19 +44,10 @@ int __fastcall Script_UnitInRange(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
void *unit = ResolveUnit(token);
|
||||
void *player = ResolveUnit("player");
|
||||
if (unit == nullptr || player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
float unitBuf[3] = {};
|
||||
float playerBuf[3] = {};
|
||||
const float *unitPos = ReadPosition(unit, unitBuf);
|
||||
const float *playerPos = ReadPosition(player, playerBuf);
|
||||
if (unitPos == nullptr || playerPos == nullptr) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
|
||||
Reference in New Issue
Block a user