mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46597f6587 | ||
|
|
4e1c1863e9 | ||
|
|
a4c57614bc | ||
|
|
e1a9d9b5c4 | ||
|
|
cd280ac307 | ||
|
|
564495248c | ||
|
|
1f3f3cb9f6 |
@@ -43,3 +43,4 @@ Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Server-side DoT-duration adjustments the 1.12 client is never told about.
|
||||
--
|
||||
-- ClassicAPI derives C_UnitAuras expirationTime for a debuff on another unit
|
||||
-- from the cast packet (SMSG_SPELL_GO). The vanilla server never retransmits a
|
||||
-- LATER server-side duration change on a non-self unit -- verified in the
|
||||
-- server source: the change runs through SetAuraDuration / RefreshHolder, and
|
||||
-- the only duration packet (SMSG_UPDATE_AURA_DURATION) is self-scoped to the
|
||||
-- aura-bearer, so an observing caster hears nothing (and on a mob the packet
|
||||
-- isn't even built). We can't hear the change, but we DO see the *triggering*
|
||||
-- cast, so C_UnitAuras.RegisterAuraDurationModifier mirrors the server's edit
|
||||
-- off that trigger.
|
||||
--
|
||||
-- A trigger is matched by exact spellID (stable across ranks -- these are the
|
||||
-- IDs the server binds its spell script to); the affected aura by
|
||||
-- SpellFamilyName + a family-flag overlap (rank-proof, exactly how the server
|
||||
-- scripts locate it). Flags below are from the client's Spell.dbc. When the
|
||||
-- trigger is a whole class of spells rather than a named ability,
|
||||
-- RegisterAuraDurationModifierByTrigger matches it by SpellFamilyName + school
|
||||
-- index instead (see Shadow Weaving below).
|
||||
--
|
||||
-- NOT included on purpose: Carnage's Rip/Rake refresh fires behind a
|
||||
-- server-side probability roll on Ferocious Bite. The client can't know the
|
||||
-- roll's outcome, so inferring it would show wrong timers. If you accept that,
|
||||
-- register it yourself, e.g. (DRUID = 7):
|
||||
-- C_UnitAuras.RegisterAuraDurationModifier(<ferociousBiteID>, 7, tonumber("800000",16), 108, "refresh") -- Rip
|
||||
-- C_UnitAuras.RegisterAuraDurationModifier(<ferociousBiteID>, 7, tonumber("1000",16), 494, "refresh") -- Rake
|
||||
-- Conflagrate's *full*-consume variant (other servers) removes Immolate
|
||||
-- outright; that clears the aura slot and ClassicAPI evicts it via the normal
|
||||
-- removal path, so the reduce rule below is harmless there and correct on
|
||||
-- Turtle (where Immolate keeps ticking with 3s shaved off).
|
||||
|
||||
if TURTLE_WOW_VERSION then
|
||||
local WARLOCK, SHAMAN = 5, 11
|
||||
-- Lua 5.0 has no 0x hex literals; tonumber(hex, 16) keeps the flags readable.
|
||||
local IMMOLATE_FLAG = tonumber("4", 16) -- CF_WARLOCK_IMMOLATE (bit 2)
|
||||
local FLAMESHOCK_FLAG = tonumber("10000000", 16) -- CF_SHAMAN_FLAME_SHOCK (bit 28)
|
||||
|
||||
-- Conflagrate (all ranks) -> shave 3s off the caster's Immolate.
|
||||
local CONFLAGRATE = { 17962, 18930, 18931, 18932 }
|
||||
for i = 1, table.getn(CONFLAGRATE) do
|
||||
C_UnitAuras.RegisterAuraDurationModifier(CONFLAGRATE[i], WARLOCK, IMMOLATE_FLAG, 0, "reduce", 3)
|
||||
end
|
||||
|
||||
-- Molten Blast (all ranks) -> refresh the caster's Flame Shock to full.
|
||||
local MOLTEN_BLAST = { 36916, 36917, 36918, 36919, 36920, 36921 }
|
||||
for i = 1, table.getn(MOLTEN_BLAST) do
|
||||
C_UnitAuras.RegisterAuraDurationModifier(MOLTEN_BLAST[i], SHAMAN, FLAMESHOCK_FLAG, 0, "refresh")
|
||||
end
|
||||
end
|
||||
-- Shadow Weaving (priest): any shadow-school priest cast refreshes the caster's
|
||||
-- Shadow Vulnerability (15258, 15s, stacks to 5). The 1->5 stack changes fire
|
||||
-- OnAuraStacksChanged (already handled); the 5->5 refresh is the silent blind
|
||||
-- spot this covers. Matched by family(6) + school(5) so it spans every priest
|
||||
-- shadow spell/rank incl. Turtle additions, excluding holy/discipline casts
|
||||
-- (Smite, heals) which are priest-family but not shadow-school.
|
||||
--
|
||||
-- Gated on the 5/5 talent (15334), where the proc is 100% -> deterministic;
|
||||
-- below 5/5 it's chance-based and inferring on every cast would over-refresh.
|
||||
-- Deferred to SPELLS_CHANGED because talent/spell state isn't ready at load,
|
||||
-- and re-checked there so a respec INTO 5/5 registers without a reload.
|
||||
-- Caveat: DoT *ticks* (SW:P / Devouring Plague) refresh it server-side too but
|
||||
-- emit no cast packet, so a pure DoT-only phase (no direct casts) under-counts.
|
||||
local PRIEST, SHADOW = 6, 5
|
||||
local SW_FLAG, SW_ICON = tonumber("4000000", 16), 9 -- 15258 SpellFamilyFlags + icon
|
||||
|
||||
EventUtil.RegisterOnceFrameEventAndCallback("SPELLS_CHANGED", function()
|
||||
if IsPlayerSpell(15334) then
|
||||
C_UnitAuras.RegisterAuraDurationModifierByTrigger(PRIEST, SHADOW, PRIEST, SW_FLAG, SW_ICON, "refresh")
|
||||
end
|
||||
end)
|
||||
@@ -547,7 +547,7 @@ function EventTraceFrame_Update()
|
||||
button:GetHighlightTexture():SetVertexColor(.8, .8, 1, .15);
|
||||
button:UnlockHighlight();
|
||||
end
|
||||
if (button:IsMouseOver()) then
|
||||
if MouseIsOver(button) then
|
||||
EventTraceFrameEvent_OnEnter(button);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -456,14 +456,11 @@
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<!-- Breadcrumb showing the active navigation path,
|
||||
e.g. "_G > MyFrame > backdrop". Updated each
|
||||
`TableInspectorFrame_Update` tick. -->
|
||||
<FontString name="$parentBreadcrumb" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Size x="380" y="16"/>
|
||||
<Size x="300" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="80" y="-34"/>
|
||||
<Offset x="156" y="-34"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
@@ -510,6 +507,17 @@
|
||||
<OnClick>TableInspector_Back();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentRefreshButton" inherits="UIPanelButtonTemplate" text="Refresh">
|
||||
<Size x="70" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentBackButton" relativePoint="RIGHT">
|
||||
<Offset x="4" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>TableInspector_Refresh();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Slider name="$parentScroll" orientation="VERTICAL">
|
||||
<Size x="16" y="0"/>
|
||||
<Anchors>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
-- TableInspector_Show(tbl, label) -- open with `tbl` as root, `label` shown in breadcrumb
|
||||
-- TableInspector_Push(tbl, label) -- drill in (called by row click)
|
||||
-- TableInspector_Back() -- pop one level
|
||||
-- TableInspector_Refresh() -- re-read the current level's live table (scroll kept)
|
||||
-- TableInspector_Close() -- hide the window
|
||||
-- /tinspect <luaexpr> -- evaluate `<luaexpr>` as Lua and open if result is a table
|
||||
------------------------------------------------------------------------------
|
||||
@@ -184,6 +185,13 @@ function TableInspector_Back()
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Refresh()
|
||||
local top = _navStack[table.getn(_navStack)];
|
||||
if (not top or type(top.tbl) ~= "table") then return; end
|
||||
_RebuildEntries();
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Close()
|
||||
if (_TableInspectorFrame) then _TableInspectorFrame:Hide(); end
|
||||
_navStack = {};
|
||||
|
||||
@@ -61,7 +61,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
@@ -71,7 +71,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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`, `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` |
|
||||
| [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.RegisterAuraDurationModifier`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `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` |
|
||||
|
||||
|
||||
+150
@@ -129,6 +129,7 @@ build instructions.
|
||||
- [`QUEST_REMOVED` event](#quest_removed-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
- [`UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)](#update_mouseover_unit-event-loss-fire-fix)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
|
||||
- [Expansion](#expansion)
|
||||
@@ -459,6 +460,8 @@ build instructions.
|
||||
- [`IsStealthed()`](#isstealthed)
|
||||
- [`IsFalling()`](#isfalling)
|
||||
- [`IsSwimming()`](#isswimming)
|
||||
- [`IsIndoors()`](#isindoors)
|
||||
- [`IsOutdoors()`](#isoutdoors)
|
||||
- [`IsAssistingRitual()`](#isassistingritual)
|
||||
- [`IsInGroup()`](#isingroup)
|
||||
- [`IsInRaid()`](#isinraid)
|
||||
@@ -553,6 +556,7 @@ build instructions.
|
||||
- [`C_UnitAuras.GetPlayerAuraBySpellID(spellID)`](#c_unitaurasgetplayeraurabyspellidspellid)
|
||||
- [`C_UnitAuras.GetAuraDataBySpellName(unit, spellName [, filter])`](#c_unitaurasgetauradatabyspellnameunit-spellname--filter)
|
||||
- [`C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)`](#c_unitaurasregistercombodurationspellid-baseseconds-maxseconds)
|
||||
- [`C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`](#c_unitaurasregisterauradurationmodifiertriggerspellid-affectedfamily-affectedfamilyflags-affectedicon-op--valueseconds)
|
||||
- [`C_UnitAuras.GetUnitAuras(unit [, filter])`](#c_unitaurasgetunitaurasunit--filter)
|
||||
- [`C_UnitAuras.GetAuraDispelTypeColor(dispelName)`](#c_unitaurasgetauradispeltypecolordispelname)
|
||||
|
||||
@@ -3120,6 +3124,60 @@ byte reads (cheap) and naturally handles the case where one packet
|
||||
carries multiple rep updates whose threshold crossings ripple AT_WAR
|
||||
state across unrelated slots.
|
||||
|
||||
### `UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)
|
||||
|
||||
`UPDATE_MOUSEOVER_UNIT` exists in the vanilla event table and fires when a
|
||||
mouseover unit is **gained**, but the 1.12 engine **never fires it on
|
||||
loss** — moving the cursor off a unit clears the mouseover and signals
|
||||
nothing. Modern WoW fires it for both gain and loss, so addons observing
|
||||
`UnitExists("mouseover")` can react when it becomes false. We restore the
|
||||
loss fire.
|
||||
|
||||
Payload is unchanged (none in vanilla — same as retail): the event
|
||||
carries no args; handlers read `UnitExists("mouseover")`, which is
|
||||
`nil`/false at fire time on the loss path.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "UPDATE_MOUSEOVER_UNIT" then
|
||||
if UnitExists("mouseover") then
|
||||
-- gained (or changed to) a mouseover unit
|
||||
else
|
||||
-- mouseover unit lost (the vanilla gap this fills)
|
||||
end
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
**Transitions and who fires them:**
|
||||
|
||||
| From | To | Fires? | Source |
|
||||
|------|----|:------:|--------|
|
||||
| unit | nothing | yes | ClassicAPI (silent in stock 1.12) |
|
||||
| unit | gameobject | yes | ClassicAPI (mouseover unit genuinely lost) |
|
||||
| gameobject | nothing | no | — (never fired a gain; not a unit) |
|
||||
| anything | unit | yes | engine's own inline fire (gain / unit→unit) |
|
||||
|
||||
**Implementation notes**
|
||||
|
||||
The mouseover GUID lives in two globals written **only** by the engine's
|
||||
mouseover set/clear chokepoint (`0x00492890`), so every gain / loss /
|
||||
change flows through it — on a unit gain it fires `UPDATE_MOUSEOVER_UNIT`
|
||||
itself, on loss it clears the GUID silently. We co-hook that function:
|
||||
resolve whether the mouseover is a *unit* before and after the original,
|
||||
and fire on the unit→non-unit transition. Gating on unit *type* (not just
|
||||
"GUID present → absent") is load-bearing — the same GUID slot also holds
|
||||
gameobjects and items, which never fire a gain, so the type check is what
|
||||
keeps moving off a herb node or chest from spuriously firing. The
|
||||
`!hasUnit` half prevents a double-fire on any →unit transition the engine
|
||||
already covered.
|
||||
|
||||
The target is change-gated by its callers (if it ran per-frame the gain
|
||||
event would spam while hovering), so it's a cool hook — no per-frame
|
||||
overhead.
|
||||
|
||||
### `UPDATE_SHAPESHIFT_FORM` event
|
||||
|
||||
Fires whenever the local player's shapeshift form changes — entering
|
||||
@@ -11099,6 +11157,37 @@ if IsSwimming() then
|
||||
end
|
||||
```
|
||||
|
||||
### `IsIndoors()`
|
||||
|
||||
Returns `1` if the local player is under a WMO roof — inside a building,
|
||||
cave, or instance interior — and `nil` otherwise. `nil` before the player
|
||||
object exists (pre-world).
|
||||
|
||||
Recomputed live from the engine's WMO geometry query, so it flips as you
|
||||
cross an interior threshold rather than tracking the current zone/area —
|
||||
you can be indoors and outdoors within the same subzone. Returns `1`/`nil`
|
||||
(not `true`/`false`) to match the historical retail contract and
|
||||
SuperWoW 2.2, so both `if IsIndoors()` and `IsIndoors() == 1` work.
|
||||
|
||||
```lua
|
||||
if IsIndoors() then
|
||||
-- suppress a mount cast that would just fail
|
||||
end
|
||||
```
|
||||
|
||||
### `IsOutdoors()`
|
||||
|
||||
Returns `1` if the local player is outdoors (open sky, or simply not
|
||||
inside a WMO interior) and `nil` otherwise. Exact complement of
|
||||
[`IsIndoors()`](#isindoors) for a resolvable player; both return `nil`
|
||||
pre-world.
|
||||
|
||||
```lua
|
||||
if IsOutdoors() then
|
||||
CastSpellByName("Mount Up")
|
||||
end
|
||||
```
|
||||
|
||||
### `IsAssistingRitual()`
|
||||
|
||||
Returns `true` if the local player is currently clicking a warlock
|
||||
@@ -13174,6 +13263,67 @@ is the escape hatch for *other* custom servers with the same class of
|
||||
gap — call it from your own config or addon; a registered override
|
||||
beats both the DBC and the built-in values.
|
||||
|
||||
### `C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`
|
||||
|
||||
ClassicAPI extension for **server-side DoT-duration changes on another
|
||||
unit that the client is never told about**. Returns `true` on success.
|
||||
|
||||
`expirationTime` for a debuff on another unit is computed from the cast
|
||||
packet (`SMSG_SPELL_GO`) at cast time. When the server later changes that
|
||||
debuff's remaining duration — a refresh, or a partial consume — 1.12 sends
|
||||
**no** packet an observing caster can see (verified in the server source:
|
||||
the change runs through `SetAuraDuration`/`RefreshHolder`, and the only
|
||||
duration packet is self-scoped to the aura-bearer; on a mob it isn't even
|
||||
built). So the cached `expirationTime` would go stale.
|
||||
|
||||
The client *does* always see the **triggering** cast, though. Register a
|
||||
rule and ClassicAPI mirrors the server's edit on the cached aura when that
|
||||
trigger lands:
|
||||
|
||||
| Arg | Meaning |
|
||||
|-----|---------|
|
||||
| `triggerSpellID` | The spell whose cast triggers the change (match one rank per call — use the exact IDs the server binds). |
|
||||
| `affectedFamily` | `SpellFamilyName` of the affected aura (e.g. `5` warlock, `11` shaman, `7` druid). |
|
||||
| `affectedFamilyFlags` | A `SpellFamilyFlags` bitmask; the affected aura matches if it overlaps. Family + flag is rank-proof (covers every rank at once). |
|
||||
| `affectedIcon` | `SpellIconID` the affected aura must have, or `0` to match any. |
|
||||
| `op` | `"refresh"` (reset to full duration), `"reduce"` (subtract `valueSeconds`, removing the aura if it would go non-positive), `"set"` (to `valueSeconds`), `"remove"`. |
|
||||
| `valueSeconds` | Amount for `reduce`/`set`; ignored otherwise. |
|
||||
|
||||
The rule only fires for the aura **cast by the same unit** as the trigger
|
||||
(these mechanics act on the caster's own DoT), and misses are excluded for
|
||||
free (a missed trigger isn't in the packet's hit list).
|
||||
|
||||
**`C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op [, valueSeconds])`**
|
||||
is the same, but matches the *trigger* by `SpellFamilyName` + school index
|
||||
(`0` physical … `2` fire, `5` shadow, `6` arcane; `< 0` = any) instead of an
|
||||
exact spellID — one rule covers a whole class of spells (every rank, plus
|
||||
server-added ones). Use it when the trigger is a category rather than a named
|
||||
ability.
|
||||
|
||||
`!!!ClassicAPI` ships a default Turtle ruleset
|
||||
([Util/AuraDurationModifiers.lua](../AddOns/!!!ClassicAPI/Util/AuraDurationModifiers.lua)):
|
||||
Conflagrate shaves 3s off the caster's Immolate, Molten Blast refreshes the
|
||||
caster's Flame Shock, and — for a 5/5 Shadow Weaving priest (talent `15334`,
|
||||
where the proc is 100%) — any shadow-school priest cast refreshes the target's
|
||||
Shadow Vulnerability (`15258`). Deliberately excluded: probabilistic effects
|
||||
(Carnage's roll-gated Rip/Rake refresh, and Shadow Weaving below 5/5) — the
|
||||
client can't see the server's roll, so inferring them would show wrong timers.
|
||||
Two accepted best-effort caveats on the Shadow Weaving rule: DoT *ticks*
|
||||
(SW:P / Devouring Plague) also refresh it server-side but emit no cast packet,
|
||||
so a pure DoT-only phase under-counts slightly; and a full-consume Conflagrate
|
||||
variant on other servers *removes* Immolate, which ClassicAPI already evicts
|
||||
via the normal removal path (so the reduce rule is harmless there).
|
||||
|
||||
```lua
|
||||
-- Flags are hex; Lua 5.0 has no 0x literals, so use tonumber(hex, 16).
|
||||
-- Conflagrate (Rank 4) shaves 3s off the caster's Immolate (warlock, flag 0x4):
|
||||
C_UnitAuras.RegisterAuraDurationModifier(18932, 5, tonumber("4", 16), 0, "reduce", 3)
|
||||
-- Molten Blast refreshes the caster's Flame Shock (shaman, flag 0x10000000):
|
||||
C_UnitAuras.RegisterAuraDurationModifier(36916, 11, tonumber("10000000", 16), 0, "refresh")
|
||||
-- Any priest (6) shadow-school (5) cast refreshes Shadow Vulnerability (15258):
|
||||
C_UnitAuras.RegisterAuraDurationModifierByTrigger(6, 5, 6, tonumber("4000000", 16), 9, "refresh")
|
||||
```
|
||||
|
||||
### `C_UnitAuras.GetUnitAuras(unit [, filter])`
|
||||
|
||||
Returns a numerically-indexed array of `AuraData` tables for every
|
||||
|
||||
@@ -435,6 +435,17 @@ and other DLLs claim for custom events.
|
||||
|
||||
## Notes on specific entries
|
||||
|
||||
- **`[336] UPDATE_MOUSEOVER_UNIT` now fires on loss too (ClassicAPI).**
|
||||
Stock 1.12 fires this only when a mouseover unit is *gained*; moving
|
||||
the cursor off a unit clears the mouseover and signals nothing.
|
||||
ClassicAPI co-hooks the engine's mouseover set/clear chokepoint
|
||||
(`0x00492890`, the sole writer of the mouseover GUID) and fires
|
||||
`UPDATE_MOUSEOVER_UNIT` on the unit→non-unit transition, matching
|
||||
retail. Gated on the *old* mouseover being a unit and the *new* one
|
||||
not, so moving off a gameobject / item (which never fired a gain)
|
||||
doesn't spuriously fire. No payload either way — handlers read
|
||||
`UnitExists("mouseover")`, false at fire time on loss. See
|
||||
[API.md](API.md#update_mouseover_unit-event-loss-fire-fix).
|
||||
- **Repeated `UNIT_*` entries at low indices** (e.g. four slots named
|
||||
`UNIT_DAMAGE` at 128–131, five `UNIT_STATS` at 144–148, seven
|
||||
`UNIT_RESISTANCES` at 149–155) — these are the per-stat /
|
||||
|
||||
@@ -316,6 +316,16 @@ enum Offsets {
|
||||
// C_Item.GetItemName exactly. Inner formatter FUN_005D8B00.
|
||||
FUN_ITEM_BUILD_INSTANCE_NAME = 0x005D8BC0,
|
||||
|
||||
// Inner name formatter behind FUN_ITEM_BUILD_INSTANCE_NAME, callable
|
||||
// WITHOUT a CGItem: `__fastcall(char *out /*ecx*/, uint outSize /*edx*/,
|
||||
// uint32 itemID, int suffixID)`. Fetches the ItemStats record for itemID
|
||||
// and, when suffixID is a valid ItemRandomProperties row with a localized
|
||||
// suffix name (record +0x1c + locale*4), formats base + suffix via
|
||||
// ITEM_SUFFIX_TEMPLATE; otherwise the base name. Lets the by-STRING item
|
||||
// paths (GetItemNameByID, C_Item.GetItemInfo) apply an item link's random
|
||||
// suffix without a live item instance.
|
||||
FUN_ITEM_BUILD_NAME_FROM_ID = 0x005D8B00,
|
||||
|
||||
// Engine's inventory swap-and-send. Same primitive
|
||||
// `Script_EquipCursorItem` (0x00489660) uses after the cursor's
|
||||
// source location has been resolved. Sends opcode 0x10D
|
||||
@@ -544,6 +554,31 @@ enum Offsets {
|
||||
// input it's the right primitive.
|
||||
FUN_TOKEN_TO_GUID = 0x00515970,
|
||||
|
||||
// Mouseover-unit GUID globals — the `"mouseover"` token resolves to
|
||||
// these (see the token-dispatch list above). Written ONLY by
|
||||
// `FUN_SET_MOUSEOVER_UNIT`; zero when nothing is moused over. Used by
|
||||
// `Unit::Mouseover` to detect the loss transition.
|
||||
VAR_MOUSEOVER_GUID_LO = 0x00B4E2C8,
|
||||
VAR_MOUSEOVER_GUID_HI = 0x00B4E2CC,
|
||||
|
||||
// The engine's single mouseover set/clear chokepoint —
|
||||
// `__stdcall(guidLo, guidHi, arg3, arg4)` (early returns are all
|
||||
// `ret 0x10`, and the caller pushes 4 without cleaning). It is the
|
||||
// ONLY writer of `VAR_MOUSEOVER_GUID_*`, so every mouseover
|
||||
// gain/loss/change flows through it. On a unit gain it fires
|
||||
// `UPDATE_MOUSEOVER_UNIT` itself (via `FUN_FIRE_EVENT_NO_ARGS(0x150)`
|
||||
// in the friendly/hostile-unit tooltip case); on loss it clears the
|
||||
// GUID but fires nothing — the gap `Unit::Mouseover` closes.
|
||||
// Change-gated by its callers (if it ran per-frame the gain fire
|
||||
// would spam every frame while hovering, which vanilla doesn't), so
|
||||
// it's a cool hook target.
|
||||
FUN_SET_MOUSEOVER_UNIT = 0x00492890,
|
||||
|
||||
// Event ID of `UPDATE_MOUSEOVER_UNIT` (name-table slot
|
||||
// `0x00BE16D8` → `(0x16D8 - 0x1198) / 4`). Fired with no args via
|
||||
// `FUN_FIRE_EVENT_NO_ARGS`; Lua handlers read `UnitExists("mouseover")`.
|
||||
EVENT_UPDATE_MOUSEOVER_UNIT = 0x150,
|
||||
|
||||
// `SStrCmpI(a, b, n)` — Storm's case-insensitive memcmp-style
|
||||
// comparator. **`int __stdcall(const char *a, const char *b, int n)`**
|
||||
// — the function ends with `ret 0xc`, so the callee pops the
|
||||
@@ -4704,6 +4739,23 @@ enum Offsets {
|
||||
// `castGameObject != 0`).
|
||||
OFF_CGPLAYER_CAST_GAMEOBJECT_PTR = 0xB4,
|
||||
|
||||
// Indoor/outdoor query for a unit — backs `IsIndoors()` / `IsOutdoors()`
|
||||
// (`unit/State.cpp`). `__fastcall(unit) -> uint` where **nonzero =
|
||||
// outdoors**, 0 = indoors. A 2-instruction thunk: reads the unit's
|
||||
// world/collision-context pointer at `unit + 0xE0` and tail-jumps to the
|
||||
// WMO query `FUN_006706f0`. That query is fully null-safe — null context
|
||||
// or a unit not inside a WMO group returns 1 (outdoors); otherwise it
|
||||
// resolves the current WMO group via `FUN_0069d4f0` and returns bit 15
|
||||
// (`0x8000`) of the group's runtime flags word (the OUTDOOR bit).
|
||||
//
|
||||
// Derived from SuperWoW 2.2's `IsIndoors` handler (which does exactly
|
||||
// `unit = ObjMgr(0x10, PlayerGUID()); push (query(unit)==0)`), then
|
||||
// confirmed against the vanilla binary. Vanilla ships no Lua binding for
|
||||
// this; the 3.3.5 client's `IsIndoors` (`0x00612300`) uses the identical
|
||||
// shape (`unit + 0xB8` → WMO group flag test), so the semantics are
|
||||
// cross-checked across both clients.
|
||||
FUN_UNIT_IS_OUTDOORS = 0x00612cc0,
|
||||
|
||||
// No `MOVEFLAG_FLYING` constant intentionally. Vanilla 1.12
|
||||
// doesn't flip a flying bit during taxi flights (verified
|
||||
// empirically: `IsFlying()` checking 0x01000000 stayed false
|
||||
@@ -5417,6 +5469,7 @@ enum Offsets {
|
||||
// sum). AttributesEx2 (+0x24) bit 0x20000000 disables spell mods.
|
||||
OFF_SPELL_RECORD_FAMILY_NAME = 0x280, // u32
|
||||
OFF_SPELL_RECORD_FAMILY_FLAGS = 0x284, // u64
|
||||
OFF_SPELL_RECORD_ICON_ID = 0x1D4, // u32 SpellIconID (→ SpellIcon.dbc)
|
||||
OFF_SPELL_RECORD_ATTRIBUTES = 0x18, // u32 (base Attributes)
|
||||
SPELL_ATTR_PASSIVE = 0x40, // bit 6 — always-on aura
|
||||
OFF_SPELL_RECORD_ATTRIBUTES_EX2 = 0x24, // u32
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Aura::Source {
|
||||
|
||||
@@ -234,6 +235,263 @@ void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Server-side duration modifiers (trigger-driven inference) -----------
|
||||
//
|
||||
// Some server mechanics change a DoT's remaining time on another unit with
|
||||
// NO client-visible signal — verified in tortoise-wow: the change goes
|
||||
// through SetAuraDuration / RefreshHolder, and UpdateAuraDuration only
|
||||
// notifies the aura's target-if-a-player (self-scoped), never the observing
|
||||
// caster; on a mob the packet isn't even built. We can't hear the *change*,
|
||||
// but we DO see the TRIGGERING cast's SMSG_SPELL_GO, so we mirror the
|
||||
// server's edit on the cached entry. Rules come from Lua
|
||||
// (`C_UnitAuras.RegisterAuraDurationModifier`), so the server-specific set
|
||||
// lives in the addon, not the DLL. Turtle examples:
|
||||
// Conflagrate -> Immolate: reduce 3s (Immolate keeps ticking, -3s)
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder → reset to max)
|
||||
// A trigger is matched by exact spellID (from the server's script binding,
|
||||
// stable across ranks); the affected aura by SpellFamilyName + a family-flag
|
||||
// overlap (+ optional icon) — rank-proof, exactly how the server's scripts
|
||||
// find it. Conflagrate's *full*-consume path removes Immolate, which clears
|
||||
// the descriptor slot → OnAuraRemoved already handles it; the reduce rule
|
||||
// covers the keep-ticking case. Probabilistic refreshes (Carnage's roll) are
|
||||
// deliberately NOT shipped as defaults — the client can't know the server's
|
||||
// roll outcome, so inferring them would show wrong timers.
|
||||
|
||||
enum ModOp { MOD_REFRESH = 0, MOD_REDUCE = 1, MOD_SET = 2, MOD_REMOVE = 3 };
|
||||
|
||||
struct DurationMod {
|
||||
uint32_t triggerSpellId; // exact trigger; 0 = match by family+school below
|
||||
uint32_t triggerFamily; // (triggerSpellId==0) trigger's SpellFamilyName
|
||||
int32_t triggerSchool; // (triggerSpellId==0) school index; -1 = any school
|
||||
uint32_t affectedFamily; // SpellFamilyName of the affected aura
|
||||
uint64_t affectedMask; // must overlap the affected aura's SpellFamilyFlags
|
||||
uint32_t affectedIcon; // 0 = any; else affected aura's SpellIconID must equal
|
||||
int32_t op;
|
||||
int32_t valueMs; // REDUCE/SET amount; ignored by REFRESH/REMOVE
|
||||
};
|
||||
constexpr int kMaxMods = 128;
|
||||
DurationMod g_mods[kMaxMods];
|
||||
int g_modCount = 0;
|
||||
|
||||
bool AffectedMatches(const uint8_t *rec, const DurationMod &m) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != m.affectedFamily)
|
||||
return false;
|
||||
const uint64_t flags = *reinterpret_cast<const uint64_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_FLAGS);
|
||||
if ((flags & m.affectedMask) == 0)
|
||||
return false;
|
||||
if (m.affectedIcon != 0 &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_ICON_ID) != m.affectedIcon)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// A rule's trigger matches either by exact spellID, or (triggerSpellId == 0)
|
||||
// by the cast spell's SpellFamilyName + school index — one rule then covers
|
||||
// every rank / server-added spell of a class's school (e.g. any priest
|
||||
// shadow-school cast, for Shadow Weaving). triggerSchool < 0 = any school.
|
||||
bool TriggerMatches(const DurationMod &m, uint32_t triggerSpellId,
|
||||
const uint8_t *triggerRec) {
|
||||
if (m.triggerSpellId != 0)
|
||||
return m.triggerSpellId == triggerSpellId;
|
||||
if (triggerRec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
triggerRec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != m.triggerFamily)
|
||||
return false;
|
||||
if (m.triggerSchool >= 0 &&
|
||||
static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
|
||||
triggerRec + Offsets::OFF_SPELL_SCHOOL)) != m.triggerSchool)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
switch (m.op) {
|
||||
case MOD_REFRESH:
|
||||
if (e.durationMs > 0)
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → reset to max
|
||||
break;
|
||||
case MOD_SET:
|
||||
e.durationMs = static_cast<uint32_t>(m.valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(m.valueMs);
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(m.valueMs))
|
||||
e.expirationMs -= static_cast<uint32_t>(m.valueMs);
|
||||
else
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
}
|
||||
break;
|
||||
case MOD_REMOVE:
|
||||
e.used = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// On a trigger cast landing on its hit targets, mirror the server's duration
|
||||
// edit on the caster's own matching cached aura. Called from SpellGo_h before
|
||||
// the aura gate — triggers (Conflagrate, Molten Blast) apply no aura of their
|
||||
// own, so they'd otherwise be dropped.
|
||||
void ApplyDurationModifiers(uint32_t triggerSpellId, uint64_t caster,
|
||||
const uint64_t *targets, int numTargets) {
|
||||
if (triggerSpellId == 0 || caster == 0 || numTargets <= 0)
|
||||
return;
|
||||
const uint8_t *triggerRec =
|
||||
Spell::Lookup::RecordForID(static_cast<int>(triggerSpellId));
|
||||
const uint32_t now = NowMs();
|
||||
for (int r = 0; r < g_modCount; ++r) {
|
||||
const DurationMod &m = g_mods[r];
|
||||
if (!TriggerMatches(m, triggerSpellId, triggerRec))
|
||||
continue;
|
||||
for (int t = 0; t < numTargets; ++t) {
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.targetGuid != targets[t])
|
||||
continue;
|
||||
// The mechanic acts on the trigger-caster's own aura. Cast-applied
|
||||
// auras (Immolate, Flame Shock) carry caster == player from SpellGo;
|
||||
// but PROC-applied ones (Shadow Weaving) enter the cache via the
|
||||
// application hooks with casterGuid 0 (no SMSG_SPELL_GO ever named a
|
||||
// caster), so accept those too and attribute them to this trigger
|
||||
// caster (also gives them a sourceUnit).
|
||||
if (e.casterGuid != 0 && e.casterGuid != caster)
|
||||
continue;
|
||||
if (!AffectedMatches(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId)), m))
|
||||
continue;
|
||||
if (e.casterGuid == 0)
|
||||
e.casterGuid = caster;
|
||||
ApplyMod(e, m, now);
|
||||
break; // one matching aura per (rule, target), like the server
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RegisterDurationMod(uint32_t triggerSpellId, uint32_t triggerFamily,
|
||||
int32_t triggerSchool, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs) {
|
||||
// Trigger must be identified one way or the other.
|
||||
if ((triggerSpellId == 0 && triggerFamily == 0) || affectedMask == 0 ||
|
||||
op < MOD_REFRESH || op > MOD_REMOVE)
|
||||
return false;
|
||||
for (int i = 0; i < g_modCount; ++i) { // replace an identical rule
|
||||
DurationMod &m = g_mods[i];
|
||||
if (m.triggerSpellId == triggerSpellId && m.triggerFamily == triggerFamily &&
|
||||
m.triggerSchool == triggerSchool && m.affectedFamily == affectedFamily &&
|
||||
m.affectedMask == affectedMask && m.affectedIcon == affectedIcon) {
|
||||
m.op = op;
|
||||
m.valueMs = valueMs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (g_modCount >= kMaxMods)
|
||||
return false;
|
||||
g_mods[g_modCount++] = {triggerSpellId, triggerFamily, triggerSchool,
|
||||
affectedFamily, affectedMask, affectedIcon, op, valueMs};
|
||||
return true;
|
||||
}
|
||||
|
||||
int OpFromString(const char *s) {
|
||||
if (s == nullptr)
|
||||
return -1;
|
||||
if (_stricmp(s, "refresh") == 0)
|
||||
return MOD_REFRESH;
|
||||
if (_stricmp(s, "reduce") == 0)
|
||||
return MOD_REDUCE;
|
||||
if (_stricmp(s, "set") == 0)
|
||||
return MOD_SET;
|
||||
if (_stricmp(s, "remove") == 0)
|
||||
return MOD_REMOVE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, affectedFamily,
|
||||
// affectedFamilyFlags, affectedIcon, op [, valueSeconds])` -> bool.
|
||||
// op: "refresh" | "reduce" | "set" | "remove". valueSeconds used by
|
||||
// reduce/set. affectedIcon 0 = match any icon. Re-registering an identical
|
||||
// (trigger, family, flags, icon) tuple replaces its op/value.
|
||||
int __fastcall Script_RegisterAuraDurationModifier(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4) ||
|
||||
!Game::Lua::IsString(L, 5)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_UnitAuras.RegisterAuraDurationModifier(triggerSpellID, "
|
||||
"affectedFamily, affectedFamilyFlags, affectedIcon, op[, "
|
||||
"valueSeconds])");
|
||||
return 0;
|
||||
}
|
||||
const auto trigger = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const auto family = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
const auto mask = static_cast<uint64_t>(Game::Lua::ToNumber(L, 3));
|
||||
const auto icon = static_cast<uint32_t>(Game::Lua::ToNumber(L, 4));
|
||||
const int op = OpFromString(Game::Lua::ToString(L, 5));
|
||||
const int32_t valueMs =
|
||||
Game::Lua::IsNumber(L, 6)
|
||||
? static_cast<int32_t>(Game::Lua::ToNumber(L, 6) * 1000.0)
|
||||
: 0;
|
||||
if (op < 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L, RegisterDurationMod(trigger, /*triggerFamily*/ 0,
|
||||
/*triggerSchool*/ -1, family, mask,
|
||||
icon, op, valueMs));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterAuraDurationModifierByTrigger(triggerFamily,
|
||||
// triggerSchool, affectedFamily, affectedFamilyFlags, affectedIcon, op
|
||||
// [, valueSeconds])` -> bool. Like the above, but the trigger is matched
|
||||
// by SpellFamilyName + school index instead of an exact spellID — one rule
|
||||
// covers every rank / server-added spell of a class's school. triggerSchool
|
||||
// < 0 = any school. E.g. Shadow Weaving: priest (6) shadow-school (5) casts
|
||||
// refresh the target's Shadow Vulnerability.
|
||||
int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4) ||
|
||||
!Game::Lua::IsNumber(L, 5) || !Game::Lua::IsString(L, 6)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_UnitAuras.RegisterAuraDurationModifierByTrigger("
|
||||
"triggerFamily, triggerSchool, affectedFamily, affectedFamilyFlags, "
|
||||
"affectedIcon, op[, valueSeconds])");
|
||||
return 0;
|
||||
}
|
||||
const auto tfamily = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const auto tschool = static_cast<int32_t>(Game::Lua::ToNumber(L, 2));
|
||||
const auto family = static_cast<uint32_t>(Game::Lua::ToNumber(L, 3));
|
||||
const auto mask = static_cast<uint64_t>(Game::Lua::ToNumber(L, 4));
|
||||
const auto icon = static_cast<uint32_t>(Game::Lua::ToNumber(L, 5));
|
||||
const int op = OpFromString(Game::Lua::ToString(L, 6));
|
||||
const int32_t valueMs =
|
||||
Game::Lua::IsNumber(L, 7)
|
||||
? static_cast<int32_t>(Game::Lua::ToNumber(L, 7) * 1000.0)
|
||||
: 0;
|
||||
if (op < 0 || tfamily == 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L, RegisterDurationMod(/*triggerSpellId*/ 0, tfamily, tschool,
|
||||
family, mask, icon, op, valueMs));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterDurationModLua() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterAuraDurationModifier",
|
||||
&Script_RegisterAuraDurationModifier);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras",
|
||||
"RegisterAuraDurationModifierByTrigger",
|
||||
&Script_RegisterAuraDurationModifierByTrigger);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
|
||||
// Wipe the whole cache. Used on a map transition (see OnWorldTick).
|
||||
void FlushAll() {
|
||||
for (auto &e : g_cache)
|
||||
@@ -328,6 +586,11 @@ void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
if (caster == Unit::Identity::PlayerGuid())
|
||||
Totem::Tracker::OnPlayerSpellGo(spellId);
|
||||
|
||||
// Mirror server-side duration edits the client is never told about
|
||||
// (Conflagrate -3s Immolate, Molten Blast refresh Flame Shock, …). Runs
|
||||
// before the aura gate below: the trigger spell applies no aura itself.
|
||||
ApplyDurationModifiers(spellId, caster, targets, numTargets);
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
|
||||
+13
-2
@@ -21,7 +21,7 @@
|
||||
namespace Item::Arg {
|
||||
|
||||
Resolved Resolve(void *L, int idx) {
|
||||
Resolved out{0, nullptr};
|
||||
Resolved out{0, 0, nullptr};
|
||||
if (Game::Lua::IsNumber(L, idx)) {
|
||||
out.itemID = static_cast<int>(Game::Lua::ToNumber(L, idx));
|
||||
return out;
|
||||
@@ -34,7 +34,18 @@ Resolved Resolve(void *L, int idx) {
|
||||
return out;
|
||||
}
|
||||
if (const char *m = std::strstr(s, "item:")) {
|
||||
out.itemID = std::atoi(m + 5);
|
||||
m += 5;
|
||||
out.itemID = std::atoi(m);
|
||||
// Random suffix is the 3rd colon-separated field: itemID:enchant:suffix.
|
||||
// Walk to the 2nd ':' (stopping at the link boundary), read the number
|
||||
// after it. Bare "item:N" (no colons) leaves suffix 0.
|
||||
int colons = 0;
|
||||
for (const char *p = m; *p != '\0' && *p != '|'; ++p) {
|
||||
if (*p == ':' && ++colons == 2) {
|
||||
out.suffix = std::atoi(p + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Plain string — try numeric, otherwise expose as a name for
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Item::Arg {
|
||||
// can ignore `name` and treat `itemID == 0` as "no usable input".
|
||||
struct Resolved {
|
||||
int itemID;
|
||||
int suffix; // random-property / suffix ID (3rd `item:` field), 0 if none
|
||||
const char *name;
|
||||
};
|
||||
|
||||
|
||||
+15
-4
@@ -259,7 +259,8 @@ static int __fastcall Script_C_Item_GetItemFamily(void *L) {
|
||||
// the value lands on a retry after `GET_ITEM_INFO_RECEIVED`,
|
||||
// matching the modern async contract.
|
||||
static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
const Item::Arg::Resolved arg = Item::Arg::Resolve(L, 1);
|
||||
const int itemID = arg.itemID;
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
|
||||
@@ -288,14 +289,24 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
|
||||
// Apply the link's random suffix ("... of the Owl") to both the name and
|
||||
// the reconstructed link when the input carried one (arg.suffix); with no
|
||||
// suffix these reduce to the base name / plain `item:N:0:0:0` link.
|
||||
char suffixedName[128];
|
||||
const char *displayName =
|
||||
Item::Link::NameFromIDSuffix(static_cast<uint32_t>(itemID), arg.suffix,
|
||||
suffixedName, sizeof(suffixedName))
|
||||
? suffixedName
|
||||
: name;
|
||||
|
||||
char iconPath[260];
|
||||
if (!BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
iconPath[0] = 0;
|
||||
char link[256];
|
||||
const bool haveLink = Item::Link::BasicFromItemID(static_cast<uint32_t>(itemID),
|
||||
link, sizeof(link));
|
||||
const bool haveLink = Item::Link::BasicFromIDSuffix(static_cast<uint32_t>(itemID),
|
||||
arg.suffix, link, sizeof(link));
|
||||
|
||||
Game::Lua::PushString(L, PushedOrEmpty(name)); // 1 itemName
|
||||
Game::Lua::PushString(L, PushedOrEmpty(displayName)); // 1 itemName
|
||||
if (haveLink)
|
||||
Game::Lua::PushString(L, link); // 2 itemLink
|
||||
else
|
||||
|
||||
+31
-7
@@ -30,6 +30,10 @@ using BuildItemLink_t = const char *(__fastcall *)(const void *cgItem);
|
||||
using BuildInstanceName_t = void(__thiscall *)(const void *cgItem, char *out,
|
||||
unsigned outSize);
|
||||
|
||||
// Inner name builder, no CGItem needed: (out, outSize, itemID, suffixID).
|
||||
using BuildNameFromID_t = void(__fastcall *)(char *out, unsigned outSize,
|
||||
uint32_t itemID, int suffixID);
|
||||
|
||||
// Cached ItemStats record lookup. Same `_GetRecord` pattern the rest of
|
||||
// the codebase uses; passes a noop NULL callback so no SMSG fires for
|
||||
// uncached items — caller treats "not cached" as a clean failure.
|
||||
@@ -63,25 +67,45 @@ bool NameFromCGItem(const uint8_t *cgItem, char *out, size_t outSize) {
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize) {
|
||||
bool NameFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
auto fn = reinterpret_cast<BuildNameFromID_t>(
|
||||
Offsets::FUN_ITEM_BUILD_NAME_FROM_ID);
|
||||
fn(out, static_cast<unsigned>(outSize), itemID, suffixID);
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
bool BasicFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
const uint8_t *record = PeekItemRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
char name[128];
|
||||
if (!NameFromIDSuffix(itemID, suffixID, name, sizeof(name))) {
|
||||
// Suffix builder yielded nothing (e.g. suffix row missing) — fall back
|
||||
// to the base record name so we at least produce a valid link.
|
||||
const char *base = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (base == nullptr || *base == '\0')
|
||||
return false;
|
||||
std::snprintf(name, sizeof(name), "%s", base);
|
||||
}
|
||||
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
const int n = std::snprintf(out, outSize,
|
||||
"%s|Hitem:%u:0:0:0|h[%s]|h|r",
|
||||
"%s|Hitem:%u:0:%d:0|h[%s]|h|r",
|
||||
Item::QualityColor::Prefix(static_cast<int>(quality)),
|
||||
itemID, name);
|
||||
itemID, suffixID, name);
|
||||
return n > 0 && static_cast<size_t>(n) < outSize;
|
||||
}
|
||||
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize) {
|
||||
return BasicFromIDSuffix(itemID, 0, out, outSize);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// `C_Item.GetItemLink(itemLocation)` — returns the fully-decorated
|
||||
|
||||
@@ -51,4 +51,18 @@ bool NameFromCGItem(const uint8_t *cgItem, char *out, size_t outSize);
|
||||
// possible link (`item:` payload + colored prefix + bracketed name).
|
||||
bool BasicFromItemID(uint32_t itemID, char *out, size_t outSize);
|
||||
|
||||
// Writes the suffix-decorated display name for `(itemID, suffixID)` into
|
||||
// `out` WITHOUT a live item instance — the by-string analog of
|
||||
// NameFromCGItem. `suffixID` is the 3rd `item:` field (random property /
|
||||
// suffix); 0 = base name. Applies the "... of the Owl" suffix via the
|
||||
// engine's inner builder (FUN_ITEM_BUILD_NAME_FROM_ID) when the item is
|
||||
// cached and the suffix is a valid ItemRandomProperties row. Returns false
|
||||
// (out empty) if the item isn't cached or has no name — warm and retry.
|
||||
bool NameFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize);
|
||||
|
||||
// Like BasicFromItemID but preserves the random `suffixID` in both the
|
||||
// `item:` payload (`item:N:0:suffix:0`) and the bracketed, suffix-decorated
|
||||
// name. `suffixID` 0 reduces exactly to BasicFromItemID.
|
||||
bool BasicFromIDSuffix(uint32_t itemID, int suffixID, char *out, size_t outSize);
|
||||
|
||||
} // namespace Item::Link
|
||||
|
||||
+20
-1
@@ -94,8 +94,27 @@ static int __fastcall Script_C_Item_GetItemName(void *L) {
|
||||
// for `itemID`, firing a background cache fill on miss. Accepts the
|
||||
// same input shapes as `GetItemInfoInstant` (number, `"item:NNN..."`
|
||||
// string, or any string containing a parseable `item:NN` chunk).
|
||||
//
|
||||
// When the input is an item link carrying a random suffix
|
||||
// (`item:12022:0:769:0`), the returned name is suffix-decorated
|
||||
// ("Iridium Chain of the Owl") — the 3rd `item:` field is parsed and
|
||||
// applied via the engine's own name builder, so it matches the global
|
||||
// `GetItemInfo`'s name. A bare itemID (number or `item:N`) has no suffix
|
||||
// and yields the base name.
|
||||
static int __fastcall Script_C_Item_GetItemNameByID(void *L) {
|
||||
return PushNameForItemID(L, Item::Arg::ResolveItemID(L, 1));
|
||||
const Item::Arg::Resolved r = Item::Arg::Resolve(L, 1);
|
||||
if (r.itemID <= 0)
|
||||
return 0;
|
||||
char buf[128];
|
||||
if (Item::Link::NameFromIDSuffix(static_cast<uint32_t>(r.itemID), r.suffix,
|
||||
buf, sizeof(buf))) {
|
||||
Game::Lua::PushString(L, buf);
|
||||
return 1;
|
||||
}
|
||||
// Not cached yet — fire a background fill and return nil (same miss
|
||||
// behavior as the base-name path).
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(r.itemID));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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/>.
|
||||
|
||||
// UPDATE_MOUSEOVER_UNIT loss-fire polyfill.
|
||||
//
|
||||
// Vanilla fires `UPDATE_MOUSEOVER_UNIT` only when a mouseover is *gained*
|
||||
// (the engine's `FUN_SET_MOUSEOVER_UNIT` fires it inline in the
|
||||
// friendly/hostile-unit tooltip case). When the cursor leaves the unit,
|
||||
// the engine clears the mouseover GUID but fires nothing — so an addon
|
||||
// that reacted to `UnitExists("mouseover")` becoming true never gets told
|
||||
// when it becomes false. Retail fires the event for both gain and loss.
|
||||
//
|
||||
// We close the gap by co-hooking the single set/clear chokepoint (it is
|
||||
// the ONLY writer of the mouseover GUID globals, so every gain / loss /
|
||||
// change flows through it): resolve whether the mouseover was a *unit*
|
||||
// before the original, then after, and fire `UPDATE_MOUSEOVER_UNIT` when
|
||||
// it went from a unit to a non-unit.
|
||||
//
|
||||
// The unit-vs-non-unit test matters: the mouseover GUID also holds
|
||||
// gameobjects (and items), but the engine only fires the *gain* event for
|
||||
// units — the gameobject/item cases never fire it. So a naive
|
||||
// "GUID present -> absent" test spuriously fires when you move off a
|
||||
// gameobject that never fired a gain. Gating on "old was a unit, new
|
||||
// isn't" fixes that while still matching retail's other cases:
|
||||
//
|
||||
// * unit -> nothing : fire (retail fires)
|
||||
// * unit -> gameobject : fire (retail fires — mouseover unit lost)
|
||||
// * gameobject -> nothing: no (bug fixed; no gain ever fired)
|
||||
// * X -> unit : engine already fired inline; we don't (new is
|
||||
// a unit, so our `!newIsUnit` guard is false —
|
||||
// no double-fire on gain or unit->unit change)
|
||||
//
|
||||
// The event carries no arguments in vanilla (same as retail); handlers
|
||||
// read `UnitExists("mouseover")` — which is false at fire time on the loss
|
||||
// path, exactly matching retail semantics.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Mouseover {
|
||||
|
||||
namespace {
|
||||
|
||||
// `FUN_SET_MOUSEOVER_UNIT` is `__stdcall(guidLo, guidHi, arg3, arg4)` —
|
||||
// see Offsets.h. We don't use the args; they pass straight through.
|
||||
using SetMouseover_t = void(__stdcall *)(uint32_t guidLo, uint32_t guidHi,
|
||||
uint32_t arg3, uint32_t arg4);
|
||||
|
||||
using FireEventNoArgs_t = void(__fastcall *)(int eventID);
|
||||
|
||||
// `FUN_OBJECT_RESOLVE_BY_GUID(typeMask, debugName, guidLo, guidHi, prio)`
|
||||
// — keeps the object only if its type flags intersect `typeMask`, so the
|
||||
// unit|player mask returns non-null only for a live unit / player / pet /
|
||||
// MC'd creature (never a gameobject or item). Same primitive
|
||||
// `Unit::Flags::ResolveUnitOrPlayerByGuid` uses.
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int typeMask,
|
||||
const char *debugName,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
// True iff the current mouseover GUID resolves to a unit (not a
|
||||
// gameobject / item / nothing).
|
||||
bool MouseoverIsUnit() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MOUSEOVER_GUID_LO));
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MOUSEOVER_GUID_HI));
|
||||
if ((lo | hi) == 0)
|
||||
return false;
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
return fn(kUnitOrPlayerMask, "UPDATE_MOUSEOVER_UNIT", lo, hi, 0) != nullptr;
|
||||
}
|
||||
|
||||
SetMouseover_t SetMouseover_o = nullptr;
|
||||
|
||||
void __stdcall SetMouseover_h(uint32_t guidLo, uint32_t guidHi,
|
||||
uint32_t arg3, uint32_t arg4) {
|
||||
const bool hadUnit = MouseoverIsUnit();
|
||||
SetMouseover_o(guidLo, guidHi, arg3, arg4);
|
||||
const bool hasUnit = MouseoverIsUnit();
|
||||
|
||||
// Only the unit-loss transition is missing from the engine. Any
|
||||
// ->unit transition (gain / unit->unit change) already fired inline
|
||||
// in the original, so `!hasUnit` prevents a double-fire there.
|
||||
if (hadUnit && !hasUnit) {
|
||||
auto fire = reinterpret_cast<FireEventNoArgs_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS));
|
||||
fire(Offsets::EVENT_UPDATE_MOUSEOVER_UNIT);
|
||||
}
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_SET_MOUSEOVER_UNIT,
|
||||
reinterpret_cast<void *>(&SetMouseover_h),
|
||||
reinterpret_cast<void **>(&SetMouseover_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::Mouseover
|
||||
@@ -31,6 +31,22 @@ namespace {
|
||||
|
||||
using CancelAuraSend_t = void(__fastcall *)(int spellID);
|
||||
|
||||
// `FUN_UNIT_IS_OUTDOORS(unit) -> uint`, nonzero = outdoors (see Offsets.h).
|
||||
// The thunk tail-jumps into the WMO query, so eax carries the result even
|
||||
// though a decompiler renders the thunk itself as `void`.
|
||||
using IsOutdoorsQuery_t = uint32_t(__fastcall *)(const void *unit);
|
||||
|
||||
// Tri-state indoor/outdoor read for the local player: 1 = outdoors,
|
||||
// 0 = indoors, -1 = unknown (no resolvable player object, e.g. pre-world).
|
||||
int PlayerOutdoorState() {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return -1;
|
||||
auto fn = reinterpret_cast<IsOutdoorsQuery_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_UNIT_IS_OUTDOORS));
|
||||
return fn(player) != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t PlayerMovementFlags() {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
@@ -200,10 +216,37 @@ int __fastcall Script_IsAssistingRitual(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `IsIndoors()` — 1 iff the local player is under a WMO roof (inside a
|
||||
// building / cave / instance interior), else nil. Recomputed live from the
|
||||
// engine's WMO geometry query, so it flips as you cross an interior
|
||||
// threshold — not tied to the zone/area. nil before the player object
|
||||
// exists (pre-world). Vanilla 1.12 ships no binding for this.
|
||||
//
|
||||
int __fastcall Script_IsIndoors(void *L) {
|
||||
if (PlayerOutdoorState() == 0)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `IsOutdoors()` — 1 iff the local player is outdoors (open sky, or not
|
||||
// inside a WMO interior), else nil. Exact complement of `IsIndoors` for a
|
||||
// resolvable player; both return nil pre-world. See `Script_IsIndoors`.
|
||||
int __fastcall Script_IsOutdoors(void *L) {
|
||||
if (PlayerOutdoorState() == 1)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsMounted", &Script_IsMounted);
|
||||
Game::Lua::RegisterGlobalFunction("IsIndoors", &Script_IsIndoors);
|
||||
Game::Lua::RegisterGlobalFunction("IsOutdoors", &Script_IsOutdoors);
|
||||
Game::Lua::RegisterGlobalFunction("Dismount", &Script_Dismount);
|
||||
Game::Lua::RegisterGlobalFunction("IsStealthed", &Script_IsStealthed);
|
||||
Game::Lua::RegisterGlobalFunction("IsFalling", &Script_IsFalling);
|
||||
|
||||
Reference in New Issue
Block a user