mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 07:30:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
983cc3bdff | ||
|
|
18ac033381 | ||
|
|
81434f7014 | ||
|
|
22605dad64 |
@@ -54,7 +54,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.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.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
@@ -62,7 +62,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
|
||||
| [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) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitDistanceSquared`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `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
@@ -359,6 +359,8 @@ build instructions.
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
- [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
|
||||
- [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
|
||||
- [`C_Spell.GetSpellLevelInfo(spellID)`](#c_spellgetspelllevelinfospellid)
|
||||
- [`GetSpellRequiredTargetLevel(spellID)`](#getspellrequiredtargetlevelspellid)
|
||||
|
||||
- [SpellBook](#spellbook)
|
||||
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
|
||||
@@ -426,6 +428,7 @@ build instructions.
|
||||
- [`UnitIsOtherPlayersPet(unit)`](#unitisotherplayerspetunit)
|
||||
- [`UnitStandState(unit)`](#unitstandstateunit)
|
||||
- [`UnitInRange(unit)`](#unitinrangeunit)
|
||||
- [`UnitDistanceSquared(unit)`](#unitdistancesquaredunit)
|
||||
- [`UnitPower(unit [, powerType])` / `UnitPowerMax(unit [, powerType])`](#unitpowerunit--powertype--unitpowermaxunit--powertype)
|
||||
- [`UnitPowerType(unit)`](#unitpowertypeunit)
|
||||
|
||||
@@ -8924,6 +8927,55 @@ otherwise it falls back to `name`/`displayName`/`textureID`/`spellID` with
|
||||
**`nil` times**. The player path is unchanged (full timing). Same
|
||||
placeholder fields as `C_Spell.UnitCastingInfo`.
|
||||
|
||||
### `C_Spell.GetSpellLevelInfo(spellID)`
|
||||
|
||||
Returns the raw `Spell.dbc` level fields for a spell:
|
||||
|
||||
```
|
||||
spellLevel, baseLevel, maxLevel = C_Spell.GetSpellLevelInfo(spellID)
|
||||
```
|
||||
|
||||
- `spellLevel` — the rank's effective level (drives the target-level rule
|
||||
below and per-level scaling).
|
||||
- `baseLevel` — the spell's base level.
|
||||
- `maxLevel` — the level at which per-level scaling caps (`0` = no cap).
|
||||
|
||||
Returns `nil` for an invalid / unpopulated spellID.
|
||||
|
||||
```lua
|
||||
C_Spell.GetSpellLevelInfo(14752) -- 30, 30, 40 (Divine Spirit Rank 1)
|
||||
C_Spell.GetSpellLevelInfo(14818) -- 40, 40, 50 (Divine Spirit Rank 2)
|
||||
```
|
||||
|
||||
### `GetSpellRequiredTargetLevel(spellID)`
|
||||
|
||||
Also `C_Spell.GetSpellRequiredTargetLevel(spellID)`. Returns the minimum
|
||||
level a **target** must be for this exact spell (rank) to apply — e.g.
|
||||
Divine Spirit Rank 1 can only be cast on a target level **20 or higher**.
|
||||
Returns `0` when the spell has no such restriction, or `nil` for an
|
||||
invalid spellID.
|
||||
|
||||
```lua
|
||||
GetSpellRequiredTargetLevel(14752) -- 20 (Divine Spirit Rank 1)
|
||||
GetSpellRequiredTargetLevel(14818) -- 30 (Divine Spirit Rank 2)
|
||||
GetSpellRequiredTargetLevel(1243) -- 0 (Fortitude Rank 1)
|
||||
GetSpellRequiredTargetLevel(10060) -- 0 (Power Infusion — single rank)
|
||||
GetSpellRequiredTargetLevel(133) -- 0 (Fireball — damage, not a buff)
|
||||
```
|
||||
|
||||
Mirrors the server's `SelectAuraRankForLevel` rule: a **ranked beneficial
|
||||
aura** is castable when `targetLevel + 10 >= spellLevel`, so the minimum
|
||||
target level is `spellLevel - 10`. The restriction is applied only when the
|
||||
spell is a ranked positive aura — `spellLevel > 10`, not passive, part of a
|
||||
rank chain, and applying an aura to a friendly/party/raid target (or an
|
||||
`APPLY_AREA_AURA_PARTY` effect). Below the minimum, the lowest rank fails
|
||||
with `SPELL_FAILED_LOWLEVEL`; higher ranks auto-downrank.
|
||||
|
||||
> The explicit `MinTargetLevel` / `MaxTargetLevel` spell fields the server
|
||||
> also enforces are **server-only** columns — they aren't present in the
|
||||
> 1.12 client's `Spell.dbc`, so the `spellLevel` rule is the only
|
||||
> client-readable target-level mechanism.
|
||||
|
||||
## SpellBook
|
||||
|
||||
Functions keyed on the player or pet **spellbook** (slot indices,
|
||||
@@ -10160,6 +10212,44 @@ mouseover, etc.). For raid members in a different zone or otherwise
|
||||
not in the engine's sync window, the position virtual returns null
|
||||
and the function reports `(false, false)`.
|
||||
|
||||
### `UnitDistanceSquared(unit)`
|
||||
|
||||
Returns `(distanceSquared, checkedPosition)` — the **squared** world
|
||||
distance (yards²) from the player to `unit`, and a flag indicating
|
||||
whether both positions were available.
|
||||
|
||||
```lua
|
||||
local distSq, checked = UnitDistanceSquared("target")
|
||||
if checked and distSq <= 30 * 30 then
|
||||
-- target is within 30 yards
|
||||
end
|
||||
```
|
||||
|
||||
| Return | Meaning |
|
||||
|--------|---------|
|
||||
| `distanceSquared` | Squared distance player→unit. `0` when `checkedPosition` is `false` (a placeholder — matches retail's "always a number" shape). |
|
||||
| `checkedPosition` | `true` when both positions were read. `false` when `unit`'s position isn't available (empty `partyN` slot, no target, raid member outside the sync window, etc.). |
|
||||
|
||||
The value is **squared** on purpose: nearly all distance logic is a
|
||||
threshold compare (`distSq <= range * range`) or a nearest-unit sort,
|
||||
neither of which needs the square root — so retail exposes only the
|
||||
squared form (added 5.0.4) and never a plain `UnitDistance`. Take
|
||||
`math.sqrt(distanceSquared)` only when you need a yard number to show a
|
||||
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).
|
||||
|
||||
> 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`.
|
||||
|
||||
### `UnitClassBase(unit)`
|
||||
|
||||
Returns `(classFile, classID)` — the locale-independent class
|
||||
|
||||
@@ -492,6 +492,31 @@ void PushFromCache(void *L, const uint8_t *unit,
|
||||
c.casterGuid);
|
||||
}
|
||||
|
||||
// Reconciles the `Aura::Source` cache against `unit`'s descriptor: when the
|
||||
// descriptor holds at least one raw aura the unit is in view and fully synced,
|
||||
// so it's authoritative — any cached entry for the unit whose spell isn't in
|
||||
// the array was removed while we couldn't see it (e.g. cancelled out of range,
|
||||
// so no OnAuraRemoved reached us) and must be dropped before the fallback
|
||||
// resurfaces it as a phantom (an enumerated aura whose SetUnitAura tooltip is
|
||||
// empty). Reads the RAW slots (not IsSlotPopulated): a spell that's present
|
||||
// but flag-hidden is still legitimately on the unit. Skips when the array is
|
||||
// empty — that can't be told apart from an out-of-range descriptor drop, and
|
||||
// reconciling then would wipe still-valid out-of-range entries.
|
||||
void ReconcileCache(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
uint32_t present[Offsets::UNIT_AURA_TOTAL];
|
||||
int n = 0;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
const uint32_t id = ReadSpellID(unit, slot);
|
||||
if (id != 0)
|
||||
present[n++] = id;
|
||||
}
|
||||
if (n == 0)
|
||||
return;
|
||||
Aura::Source::EvictAbsent(UnitGuid(unit), present, n);
|
||||
}
|
||||
|
||||
// True if `unit`'s descriptor currently exposes any visible aura in either
|
||||
// range. The cache fallback is only trustworthy when the descriptor has been
|
||||
// wholesale-cleared: the two states it exists for — rogue stealth and party
|
||||
@@ -559,6 +584,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return false; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
// The descriptor held `descCount` matches; the caller already found fewer
|
||||
@@ -589,6 +615,7 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
|
||||
+29
-4
@@ -325,10 +325,18 @@ const Game::HookAutoRegister _hookAuraStacks{
|
||||
reinterpret_cast<void **>(&g_origOnAuraStacksChanged)};
|
||||
|
||||
// OnAuraRemoved — a descriptor aura slot went empty: the aura fell off, was
|
||||
// dispelled, or was replaced by a higher rank (the diff dispatcher fires
|
||||
// Removed(old) + Added(new)). Evict the cache entry so the fallback stops
|
||||
// surfacing it. Same ABI as OnAuraAdded (unit in ecx, slot + spellId on the
|
||||
// stack); `slot` is unused here — we evict by spellId.
|
||||
// dispelled, was cancelled by its owner, or was replaced by a higher rank
|
||||
// (the diff dispatcher fires Removed(old) + Added(new)). Evict the cache
|
||||
// entry so the descriptor-drop fallback stops surfacing it. Same ABI as
|
||||
// OnAuraAdded (unit in ecx, slot + spellId on the stack); `slot` is unused
|
||||
// here — we evict by spellId.
|
||||
//
|
||||
// Always evict, even for a death-persistent aura (flask, world buff) on a
|
||||
// dead unit: the server keeps those through death WITHOUT changing any
|
||||
// field, so death itself fires no OnAuraRemoved. Any removal we do receive
|
||||
// for one is therefore genuine — e.g. the owner cancelled the flask while
|
||||
// dead — and must clear the cache, or the fallback keeps surfacing a phantom
|
||||
// whose SetUnitAura tooltip is empty.
|
||||
using OnAuraRemoved_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId);
|
||||
OnAuraRemoved_t g_origOnAuraRemoved = nullptr;
|
||||
@@ -361,6 +369,23 @@ bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
return false;
|
||||
}
|
||||
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count) {
|
||||
if (unitGuid == 0 || presentSpellIds == nullptr || count <= 0)
|
||||
return;
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
bool present = false;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (presentSpellIds[i] == e.spellId) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
if (!present)
|
||||
e.used = false;
|
||||
}
|
||||
}
|
||||
|
||||
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
|
||||
if (unitGuid == 0 || out == nullptr || maxOut <= 0)
|
||||
return 0;
|
||||
|
||||
@@ -50,6 +50,18 @@ struct CachedAura {
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
};
|
||||
|
||||
// Evicts every cached entry for `unitGuid` whose spellId is NOT present in
|
||||
// `presentSpellIds[0..count)`. Used to reconcile the cache against a unit's
|
||||
// authoritative descriptor when it is back in view (a populated aura array
|
||||
// means the unit is fully synced): an entry the descriptor no longer lists
|
||||
// was removed while we couldn't observe it — e.g. a buff the owner cancelled
|
||||
// while out of our range, whose `OnAuraRemoved` we never received — so drop
|
||||
// it before the descriptor-drop fallback resurfaces it as a phantom. The
|
||||
// caller must only invoke this when the descriptor is populated (count > 0);
|
||||
// an empty array can't distinguish "out of range" from "genuinely buffless",
|
||||
// so reconciling then would wrongly wipe still-valid out-of-range entries.
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count);
|
||||
|
||||
// Fills `out` with up to `maxOut` cached, non-expired auras on `unitGuid`
|
||||
// whose helpful/harmful classification matches `harmful`. Returns the count
|
||||
// written. The classification is recorded from the aura's descriptor slot at
|
||||
|
||||
@@ -117,6 +117,139 @@ bool SpellHiddenFromAutoLearn(int spellID) {
|
||||
return (attrEx2 & SPELL_ATTR_EX2_HIDE_FROM_AUTOLEARN) != 0;
|
||||
}
|
||||
|
||||
// ---- Target-level gate for ranked beneficial auras ----------------------
|
||||
//
|
||||
// A ranked beneficial aura can't be applied to a target too low for the
|
||||
// rank — e.g. Divine Spirit Rank 1 (14752) requires the target to be level
|
||||
// >= 20. Server-enforced via `SpellMgr::SelectAuraRankForLevel`, whose rule
|
||||
// is: a ranked positive aura is castable when `targetLevel + 10 >=
|
||||
// spellLevel`, i.e. the minimum target level is `spellLevel - 10`. Below
|
||||
// it the lowest rank fails (SPELL_FAILED_LOWLEVEL); higher ranks auto-
|
||||
// downrank. The gate only applies to *ranked* *positive* *auras* — see
|
||||
// RequiredTargetLevel. Fully derivable from the client's Spell.dbc.
|
||||
//
|
||||
// (The explicit MinTargetLevel / MaxTargetLevel fields the server also
|
||||
// checks are NOT in the 1.12 client's Spell.dbc — server-only columns — so
|
||||
// the spellLevel rule is the only client-readable target-level mechanism.)
|
||||
|
||||
constexpr int OFF_SPELL_MAX_LEVEL = 0x6C; // uint32 (scaling cap level)
|
||||
constexpr int OFF_SPELL_SPELL_LEVEL = 0x74; // uint32 (rank's effective level)
|
||||
constexpr int OFF_SPELL_EFFECT = 0xF4; // uint32[3] effect type
|
||||
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148; // uint32[3] implicit target A
|
||||
constexpr int OFF_SPELL_RANK = 0x204; // char*[9] localized rank text
|
||||
constexpr uint32_t SPELL_ATTR_PASSIVE = 0x40; // Attributes bit
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AURA = 6;
|
||||
constexpr uint32_t SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 35;
|
||||
|
||||
int LocaleIndex() {
|
||||
return *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
}
|
||||
|
||||
// The friendly-target IDs the server's SelectAuraRankForLevel treats as
|
||||
// "positive" for rank selection: IsExplicitPositiveTarget (21/35/45/57/61)
|
||||
// plus IsAreaEffectPossitiveTarget (20/30/31/33/34/37/56/61). Narrower than
|
||||
// a general "is helpful" set — this is the exact gate the rank rule uses
|
||||
// (self-cast / pet targets don't count).
|
||||
bool IsPositiveRankTarget(uint32_t t) {
|
||||
switch (t) {
|
||||
case 21: // TARGET_UNIT_FRIEND
|
||||
case 35: // TARGET_UNIT_PARTY
|
||||
case 45: // TARGET_UNIT_FRIEND_CHAIN_HEAL
|
||||
case 57: // TARGET_UNIT_RAID
|
||||
case 61: // TARGET_UNIT_RAID_AND_CLASS
|
||||
case 20: // TARGET_ENUM_UNITS_PARTY_WITHIN_CASTER_RANGE
|
||||
case 30: // TARGET_ENUM_UNITS_FRIEND_AOE_AT_SRC_LOC
|
||||
case 31: // TARGET_ENUM_UNITS_FRIEND_AOE_AT_DEST_LOC
|
||||
case 33: // TARGET_ENUM_UNITS_PARTY_AOE_AT_SRC_LOC
|
||||
case 34: // TARGET_ENUM_UNITS_PARTY_AOE_AT_DEST_LOC
|
||||
case 37: // TARGET_UNIT_FRIEND_AND_PARTY
|
||||
case 56: // TARGET_ENUM_UNITS_RAID_WITHIN_CASTER_RANGE
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// True if the record carries a localized "Rank N" string — the client's
|
||||
// marker for a spell in a ranked line (single-rank spells leave it empty).
|
||||
// Proxy for the server's `GetSpellRank(id) != 0`, verified against
|
||||
// single-rank buffs (Power Infusion / Innervate / Blessing of Kings, all
|
||||
// empty Rank and un-gated) vs ranked ones (Divine Spirit / Fortitude).
|
||||
bool HasRankString(const uint8_t *record) {
|
||||
const char *rank = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_RANK + LocaleIndex() * 4);
|
||||
return rank != nullptr && *rank != '\0';
|
||||
}
|
||||
|
||||
// Minimum target level required to apply this exact spell (rank), or 0 if
|
||||
// the spell isn't subject to the ranked-positive-aura target-level rule.
|
||||
int RequiredTargetLevel(const uint8_t *record) {
|
||||
const int spellLevel =
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL);
|
||||
if (spellLevel <= 10) // targetLevel + 10 >= spellLevel holds for any level
|
||||
return 0;
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(record + OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & SPELL_ATTR_PASSIVE)
|
||||
return 0;
|
||||
if (!HasRankString(record)) // single-rank spell → not gated
|
||||
return 0;
|
||||
|
||||
auto *effect = reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT);
|
||||
auto *targetA =
|
||||
reinterpret_cast<const uint32_t *>(record + OFF_SPELL_EFFECT_TARGET_A);
|
||||
bool positiveAura = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if ((effect[i] == SPELL_EFFECT_APPLY_AURA &&
|
||||
IsPositiveRankTarget(targetA[i])) ||
|
||||
effect[i] == SPELL_EFFECT_APPLY_AREA_AURA_PARTY) {
|
||||
positiveAura = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!positiveAura)
|
||||
return 0;
|
||||
return spellLevel - 10;
|
||||
}
|
||||
|
||||
int ArgSpellID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
return static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
}
|
||||
|
||||
// `C_Spell.GetSpellLevelInfo(spellID)` → spellLevel, baseLevel, maxLevel.
|
||||
// Raw Spell.dbc level fields: spellLevel (this rank's effective level),
|
||||
// baseLevel, maxLevel (level at which scaling caps). nil for an unknown
|
||||
// spellID.
|
||||
int __fastcall Script_GetSpellLevelInfo(void *L) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(ArgSpellID(L));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_SPELL_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + Offsets::OFF_SPELL_RECORD_BASE_LEVEL)));
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(
|
||||
*reinterpret_cast<const int *>(record + OFF_SPELL_MAX_LEVEL)));
|
||||
return 3;
|
||||
}
|
||||
|
||||
// `GetSpellRequiredTargetLevel(spellID)` → the minimum level a target must
|
||||
// be for this spell (rank) to apply (0 = no restriction). nil for an
|
||||
// unknown spellID. Ranked beneficial auras only; see RequiredTargetLevel.
|
||||
int __fastcall Script_GetSpellRequiredTargetLevel(void *L) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(ArgSpellID(L));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(RequiredTargetLevel(record)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_SpellBook.GetSpellLevelLearned(spellID)` — returns the level
|
||||
// at which a spell becomes available (the `BaseLevel` field in
|
||||
// Spell.dbc). Direct read off the record — no class/race filtering,
|
||||
@@ -315,6 +448,12 @@ void RegisterLuaFunctions() {
|
||||
&Script_GetSpellLevelLearned);
|
||||
Game::Lua::RegisterTableFunction("C_SpellBook", "GetCurrentLevelSpells",
|
||||
&Script_GetCurrentLevelSpells);
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellRequiredTargetLevel",
|
||||
&Script_GetSpellRequiredTargetLevel);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellLevelInfo",
|
||||
&Script_GetSpellLevelInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellRequiredTargetLevel",
|
||||
&Script_GetSpellRequiredTargetLevel);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+86
-16
@@ -126,8 +126,9 @@ int SlotsForInvType(int invType, int *out) {
|
||||
// English fallback labels, used when the client doesn't define the
|
||||
// modern `_G[key]` global-string (vanilla 1.12 largely doesn't). Phrasing
|
||||
// mirrors pfUI's eqcompare so the deltas read consistently on this
|
||||
// client. Percent stats keep a "%" suffix since the values are native
|
||||
// vanilla percentages.
|
||||
// client. Percent stats carry NO "%" here — `IsPercentStat` appends the
|
||||
// "%" to the value ("-5% Block"), so the unit is correct regardless of
|
||||
// which label (Blizzard `_SHORT` or this fallback) is used.
|
||||
struct KeyLabel {
|
||||
const char *key;
|
||||
const char *label;
|
||||
@@ -149,25 +150,46 @@ constexpr KeyLabel kFallbackLabels[] = {
|
||||
{"RESISTANCE6_NAME", "Arcane Resistance"},
|
||||
{"ITEM_MOD_ATTACK_POWER_SHORT", "Attack Power"},
|
||||
{"ITEM_MOD_RANGED_ATTACK_POWER_SHORT", "Ranged Attack Power"},
|
||||
{"ITEM_MOD_CRIT_MELEE_RATING", "Melee Crit %"},
|
||||
{"ITEM_MOD_CRIT_RANGED_RATING", "Ranged Crit %"},
|
||||
{"ITEM_MOD_HIT_MELEE_RATING", "Melee Hit %"},
|
||||
{"ITEM_MOD_HIT_RANGED_RATING", "Ranged Hit %"},
|
||||
{"ITEM_MOD_HIT_SPELL_RATING", "Spell Hit %"},
|
||||
{"ITEM_MOD_CRIT_SPELL_RATING", "Spell Crit %"},
|
||||
{"ITEM_MOD_CRIT_MELEE_RATING", "Melee Crit"},
|
||||
{"ITEM_MOD_CRIT_RANGED_RATING", "Ranged Crit"},
|
||||
{"ITEM_MOD_HIT_MELEE_RATING", "Melee Hit"},
|
||||
{"ITEM_MOD_HIT_RANGED_RATING", "Ranged Hit"},
|
||||
{"ITEM_MOD_HIT_SPELL_RATING", "Spell Hit"},
|
||||
{"ITEM_MOD_CRIT_SPELL_RATING", "Spell Crit"},
|
||||
{"ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", "Spell Damage"},
|
||||
{"ITEM_MOD_SPELL_HEALING_DONE_SHORT", "Spell Healing"},
|
||||
{"ITEM_MOD_MANA_REGENERATION", "Mana Regen"},
|
||||
{"ITEM_MOD_DEFENSE_SKILL_RATING", "Defense"},
|
||||
{"ITEM_MOD_DODGE_RATING", "Dodge %"},
|
||||
{"ITEM_MOD_PARRY_RATING", "Parry %"},
|
||||
{"ITEM_MOD_BLOCK_RATING", "Block %"},
|
||||
{"ITEM_MOD_DODGE_RATING", "Dodge"},
|
||||
{"ITEM_MOD_PARRY_RATING", "Parry"},
|
||||
{"ITEM_MOD_BLOCK_RATING", "Block"},
|
||||
{"ITEM_MOD_BLOCK_VALUE", "Block Value"},
|
||||
{"ITEM_MOD_DAMAGE_PER_SECOND_SHORT", "Damage Per Second"},
|
||||
{"ITEM_DELTA_DESCRIPTION",
|
||||
"If you replace this item, the following stat changes will occur:"},
|
||||
};
|
||||
|
||||
// Percent-valued stat keys: the block/dodge/parry *chances* and the
|
||||
// crit/hit percentages. Vanilla stores these as native percentages (a
|
||||
// value of 5 means 5%), so the delta must read "-5% Block", not "-5
|
||||
// Block". The "%" is attached to the NUMBER rather than the label, so it
|
||||
// stays correct in every locale and requires no `_SHORT` change.
|
||||
// NB: ITEM_MOD_DEFENSE_SKILL_RATING (skill points) and
|
||||
// ITEM_MOD_BLOCK_VALUE (flat block amount) are NOT percentages.
|
||||
bool IsPercentStat(const char *key) {
|
||||
static const char *const kPercent[] = {
|
||||
"ITEM_MOD_CRIT_MELEE_RATING", "ITEM_MOD_CRIT_RANGED_RATING",
|
||||
"ITEM_MOD_CRIT_SPELL_RATING", "ITEM_MOD_HIT_MELEE_RATING",
|
||||
"ITEM_MOD_HIT_RANGED_RATING", "ITEM_MOD_HIT_SPELL_RATING",
|
||||
"ITEM_MOD_DODGE_RATING", "ITEM_MOD_PARRY_RATING",
|
||||
"ITEM_MOD_BLOCK_RATING",
|
||||
};
|
||||
for (const char *k : kPercent)
|
||||
if (std::strcmp(k, key) == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *FallbackLabel(const char *key) {
|
||||
for (const auto &e : kFallbackLabels)
|
||||
if (std::strcmp(e.key, key) == 0)
|
||||
@@ -199,22 +221,63 @@ bool EndsWithShort(const char *key) {
|
||||
return n >= 6 && std::strcmp(key + n - 6, "_SHORT") == 0;
|
||||
}
|
||||
|
||||
// Copies `_G[key]` into `out` iff it's a plain, non-empty, non-format
|
||||
// string. Direct globals-table access, no pcall. Returns whether it took.
|
||||
bool TryGlobalLabel(void *L, const char *key, char *out, int outSize) {
|
||||
// Raw `_G[key]` string fetch — no format-specifier filtering. Direct
|
||||
// globals-table access, no pcall. Returns whether a non-empty string was
|
||||
// copied into `out`.
|
||||
bool TryGlobalRaw(void *L, const char *key, char *out, int outSize) {
|
||||
const int saved = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
const char *g = (Game::Lua::Type(L, -1) == Game::Lua::TYPE_STRING)
|
||||
? Game::Lua::ToString(L, -1)
|
||||
: nullptr;
|
||||
const bool ok = (g != nullptr && *g != '\0' && !HasFormatSpecifier(g));
|
||||
const bool ok = (g != nullptr && *g != '\0');
|
||||
if (ok)
|
||||
std::snprintf(out, outSize, "%s", g);
|
||||
Game::Lua::SetTop(L, saved);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Copies `_G[key]` into `out` iff it's a plain, non-empty, non-format
|
||||
// string. Returns whether it took.
|
||||
bool TryGlobalLabel(void *L, const char *key, char *out, int outSize) {
|
||||
char tmp[192];
|
||||
if (!TryGlobalRaw(L, key, tmp, sizeof(tmp)) || HasFormatSpecifier(tmp))
|
||||
return false;
|
||||
std::snprintf(out, outSize, "%s", tmp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Formats a signed value token (e.g. "-5") as a localized percentage via
|
||||
// the PERCENTAGE_STRING global (typically "%s%%" → "-5%"). The locale
|
||||
// format is NEVER passed to snprintf — we substitute `num` into its first
|
||||
// conversion slot (`%s`/`%d`) by hand and collapse `%%` → `%`, so a
|
||||
// hostile or unexpected format can't misread the argument list. Falls back
|
||||
// to a plain "%" suffix when the global is absent.
|
||||
void FormatPercent(void *L, const char *num, char *out, int outSize) {
|
||||
char fmt[64];
|
||||
if (!TryGlobalRaw(L, "PERCENTAGE_STRING", fmt, sizeof(fmt))) {
|
||||
std::snprintf(out, outSize, "%s%%", num);
|
||||
return;
|
||||
}
|
||||
int o = 0;
|
||||
bool inserted = false;
|
||||
for (const char *p = fmt; *p != '\0' && o < outSize - 1; ++p) {
|
||||
if (p[0] == '%' && p[1] == '%') { // literal percent
|
||||
out[o++] = '%';
|
||||
++p;
|
||||
} else if (p[0] == '%' && p[1] != '\0' && !inserted) { // value slot
|
||||
for (const char *q = num; *q != '\0' && o < outSize - 1; ++q)
|
||||
out[o++] = *q;
|
||||
++p; // consume the conversion letter (s / d)
|
||||
inserted = true;
|
||||
} else {
|
||||
out[o++] = *p;
|
||||
}
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// Resolve a display label for a stat `key` into `out`, respecting
|
||||
// localization. The rating/regen tokens GetItemStats keys on
|
||||
// (`ITEM_MOD_CRIT_MELEE_RATING`, …) carry a "%s" *description* as their
|
||||
@@ -285,7 +348,14 @@ void RenderDeltas(void *self, void *L, const Accum *acc, double dpsDelta) {
|
||||
if (v == 0)
|
||||
continue;
|
||||
LabelFor(L, acc[i].key, label, sizeof(label));
|
||||
std::snprintf(line, sizeof(line), "%s%ld %s", v > 0 ? "+" : "", v, label);
|
||||
char num[48];
|
||||
std::snprintf(num, sizeof(num), "%s%ld", v > 0 ? "+" : "", v);
|
||||
if (IsPercentStat(acc[i].key)) {
|
||||
char pct[64];
|
||||
FormatPercent(L, num, pct, sizeof(pct));
|
||||
std::snprintf(num, sizeof(num), "%s", pct);
|
||||
}
|
||||
std::snprintf(line, sizeof(line), "%s %s", num, label);
|
||||
AddColoredLine(self, line, v > 0 ? kGain : kLoss);
|
||||
}
|
||||
}
|
||||
|
||||
+62
-14
@@ -8,11 +8,21 @@
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
|
||||
// range check, modern's `UnitInRange` shape. Vanilla 1.12 doesn't
|
||||
// ship the function but has all the underlying machinery — the
|
||||
// same `GetPosition` virtual (vtable slot 5) `CheckInteractDistance`
|
||||
// uses, applied with a fixed 40-yard threshold.
|
||||
// Player↔unit proximity, modern's shape. Vanilla 1.12 ships neither
|
||||
// function but has all the underlying machinery — the object's
|
||||
// `GetPosition` virtual (vtable slot 5) `CheckInteractDistance` uses:
|
||||
//
|
||||
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
|
||||
// range check (fixed threshold on the squared distance).
|
||||
// `UnitDistanceSquared(unit) → (distanceSquared, checkedPosition)` —
|
||||
// the raw squared world distance player→unit. Squared because
|
||||
// nearly every consumer compares against a threshold (`distSq <=
|
||||
// range*range`) or ranks by nearest, neither of which needs the
|
||||
// sqrt; that's why retail exposes only the squared form (added
|
||||
// 5.0.4). Center-to-center — NOT reach-aware (that's UnitXP_SP3's
|
||||
// niche); equivalent to computing it from SuperWoW `UnitPosition`,
|
||||
// but self-contained (reads the position in C, no sibling-DLL
|
||||
// dependency, no clash with SuperWoW's own `UnitPosition`).
|
||||
|
||||
#include "Game.h"
|
||||
#include "unit/Position.h"
|
||||
@@ -26,6 +36,24 @@ namespace {
|
||||
constexpr float kRangeYards = 40.0f;
|
||||
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
|
||||
|
||||
// Squared world distance from the player to `token`'s unit. Returns true
|
||||
// and writes *outSq on success; false when either position is unavailable
|
||||
// (unresolved / absent token, or an object with no known position yet —
|
||||
// e.g. a party member outside the client's sync range). This false is
|
||||
// exactly the `checkedPosition = false` case retail flags.
|
||||
bool PlayerToUnitDistSq(const char *token, float *outSq) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos))
|
||||
return false;
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
const float dz = unitPos[2] - playerPos[2];
|
||||
*outSq = dx * dx + dy * dy + dz * dz;
|
||||
return true;
|
||||
}
|
||||
|
||||
int __fastcall Script_UnitInRange(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitInRange(\"unit\")");
|
||||
@@ -44,27 +72,47 @@ int __fastcall Script_UnitInRange(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos)) {
|
||||
float distSq = 0.0f;
|
||||
if (!PlayerToUnitDistSq(token, &distSq)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
const float dz = unitPos[2] - playerPos[2];
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
Game::Lua::PushBool(L, distSq <= kRangeYardsSq);
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `UnitDistanceSquared("unit") → (distanceSquared, checkedPosition)`.
|
||||
// Unlike UnitInRange there's no self-quirk: `UnitDistanceSquared("player")`
|
||||
// is a legitimate `(0, true)`. On a position miss returns `(0, false)` —
|
||||
// matching retail's "always a number" shape; consumers must branch on
|
||||
// `checkedPosition`, since a real `0` (self, or exactly co-located units)
|
||||
// is indistinguishable from the miss placeholder by value alone.
|
||||
int __fastcall Script_UnitDistanceSquared(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitDistanceSquared(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
|
||||
float distSq = 0.0f;
|
||||
if (!PlayerToUnitDistSq(token, &distSq)) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(distSq));
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitInRange", &Script_UnitInRange);
|
||||
Game::Lua::RegisterGlobalFunction("UnitDistanceSquared",
|
||||
&Script_UnitDistanceSquared);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
Reference in New Issue
Block a user