mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae9b83ad7 | ||
|
|
26e5fdc052 | ||
|
|
4281cd8253 | ||
|
|
9239756153 | ||
|
|
f13aa1943d | ||
|
|
438ad97f8f | ||
|
|
7f4b20c6ec | ||
|
|
3f11f942bd | ||
|
|
9622ea7742 | ||
|
|
659dd7e159 | ||
|
|
1ae102b554 |
@@ -42,3 +42,4 @@ Util\Rectangle.lua
|
||||
Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
function MouseIsOver(region, topOffset, bottomOffset, leftOffset, rightOffset)
|
||||
return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset);
|
||||
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 });
|
||||
|
||||
@@ -35,9 +35,10 @@ 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`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject` (scripts) |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
@@ -69,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` |
|
||||
@@ -90,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` |
|
||||
|
||||
|
||||
+190
-24
@@ -155,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)
|
||||
@@ -198,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)
|
||||
@@ -529,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)
|
||||
@@ -3593,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
|
||||
@@ -3641,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)`
|
||||
@@ -4641,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` /
|
||||
@@ -10474,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).
|
||||
@@ -12484,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`
|
||||
|
||||
@@ -1314,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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