Compare commits

...
3 Commits
Author SHA1 Message Date
Brues e7ccc11309 feat(unit): UnitHasIncomingResurrection, INCOMING_RESURRECT_CHANGED
4.3.4's CGIncomingResurrection. SMSG_SPELL_START adds a resurrect cast
under its object target, a unit or a corpse. SMSG_SPELL_GO, the failure
packets and ClearCastingSpell (through Spell::Cast, Turtle's only
interrupt signal) end it. A cast whose caster leaves view never reports
its end, so it ends on the next world tick. Each change fires the event
for every token naming the target, or the corpse's owner. 1.12's class
resurrections carry effect 113, not 4.3.4's 18, so both count.

Object::Corpse::ByOwner, lifted from spell/Range.cpp, now skips bones as
4.3.4's ClntObjMgrFindCorpseByOwner does. Turtle's bones keep their
owner, so old bones hid a ghost's live corpse, from this API and from
the range check's ghost redirect.

HasEffect moves from CombatLog to Spell::Lookup, and the SpellCastTargets
object mask 0x8A02 becomes one Offsets.h constant.
2026-10-29 19:21:47 -05:00
Brues bab31b5f7b docs(unit): bound the GetShapeshiftForm example to the bar
GetShapeshiftForm returns the bar count + 1 for a form that is not on
the stance bar, as 3.3.5 and 4.3.4 do. The example passed that index to
GetShapeshiftFormInfo, which returns nils for it.
2026-10-28 23:31:49 -05:00
Brues e0e24bab34 feat(macro): SetMacroDisplay range unit
C_Macro.SetMacroDisplay(macroSlot, value [, unit]) takes the unit the
button's range check uses, so a publisher's [@unit] reaches the range
check as SetMacroSpell's target does. The token stays a token: each
lookup resolves it again, so the range follows a new mouseover, focus or
target with no republish. A token that names no unit leaves the target
in charge.

The engine's resolver vets the unit at publish and raises its "Unknown
unit name" error for anything else, as UnitExists does. It tells a
token's family from its spelling alone, so a token that passes once
never raises at a later lookup. The unit is ignored for false and for an
empty value, and a publish that changes only the unit updates it.
2026-10-28 23:19:36 -05:00
18 changed files with 509 additions and 66 deletions
+2 -1
View File
@@ -133,7 +133,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByAuraInstanceID`, `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
| [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` |
@@ -279,6 +279,7 @@ when launching with `-console`), not as Lua functions. See the
| `GLOBAL_MOUSE_DOWN` | `button` |
| `GLOBAL_MOUSE_UP` | `button` |
| `HEARTHSTONE_BOUND` | *(none)* |
| `INCOMING_RESURRECT_CHANGED` | `unitTarget` |
| `INVENTORY_SEARCH_UPDATE` | *(none)* |
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
| `LEARNED_SPELL_IN_SKILL_LINE` | `spellID, skillLineIndex, isGuildPerkSpell` |
+82 -9
View File
@@ -223,6 +223,7 @@ build instructions.
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
- [`WEAR_EQUIPMENT_SET` event](#wear_equipment_set-event)
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
- [`INCOMING_RESURRECT_CHANGED` event](#incoming_resurrect_changed-event)
- [`INVENTORY_SEARCH_UPDATE` event](#inventory_search_update-event)
- [`LOOT_HISTORY_ROLL_CHANGED` / `LOOT_HISTORY_ROLL_COMPLETE` / `LOOT_HISTORY_FULL_UPDATE` events](#loot_history_roll_changed--loot_history_roll_complete--loot_history_full_update-events)
- [`LEARNED_SPELL_IN_SKILL_LINE` event](#learned_spell_in_skill_line-event)
@@ -644,7 +645,7 @@ build instructions.
- [`GetMacroIcons` / `GetMacroItemIcons` / `GetLooseMacroIcons` / `GetLooseMacroItemIcons`](#getmacroicons--getmacroitemicons--getloosemacroicons--getloosemacroitemicons)
- [`C_Macro.CreateMacro` / `C_Macro.EditMacro`](#c_macrocreatemacro--c_macroeditmacro)
- [`C_Macro.GetMacroIcon(macroSlot)`](#c_macrogetmacroiconmacroslot)
- [`C_Macro.SetMacroDisplay(macroSlot, value)`](#c_macrosetmacrodisplaymacroslot-value)
- [`C_Macro.SetMacroDisplay(macroSlot, value [, unit])`](#c_macrosetmacrodisplaymacroslot-value--unit)
- [Mail](#mail)
- [`GetSendMailItemLink([attachmentIndex])`](#getsendmailitemlinkattachmentindex)
@@ -1011,6 +1012,7 @@ build instructions.
- [`UnitPowerType(unit)`](#unitpowertypeunit)
- [`UnitSpellHaste(unit)`](#unitspellhasteunit)
- [`UnitSpellTargetName(unit)`](#unitspelltargetnameunit)
- [`UnitHasIncomingResurrection(unit)`](#unithasincomingresurrectionunit)
- [`CanBeRaidTarget(unit)`](#canberaidtargetunit)
- [UnitAuras](#unitauras)
@@ -5297,6 +5299,25 @@ forwarding, which [`C_Reputation.GetLastStandingChange`](#c_reputationgetlaststa
exposes — so addons can read the structured payload from inside the
chat event without re-parsing the localized string.
### `INCOMING_RESURRECT_CHANGED` event
Fires when a resurrection cast at a unit starts or stops. `arg1` is the unit
token. The event gives no value.
[`UnitHasIncomingResurrection`](#unithasincomingresurrectionunit) gives the
new value.
The event fires one time for each token that names the unit, for example
`"party1"` and `"target"`. If the cast is at the corpse of a released player,
`arg1` names that player.
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("INCOMING_RESURRECT_CHANGED")
f:SetScript("OnEvent", function()
print(arg1, UnitHasIncomingResurrection(arg1))
end)
```
### `INVENTORY_SEARCH_UPDATE` event
Fires with no payload when
@@ -14968,7 +14989,7 @@ to.
If SuperCleveRoidMacros is loaded, it controls macro display, and ClassicAPI
does not evaluate `#showtooltip`. A build that hands its own results over
through
[`C_Macro.SetMacroDisplay`](#c_macrosetmacrodisplaymacroslot-value) keeps
[`C_Macro.SetMacroDisplay`](#c_macrosetmacrodisplaymacroslot-value--unit) keeps
`#showtooltip` working for the macros it does not claim.
A macro that names a condition
@@ -15342,7 +15363,7 @@ mark, and the macro's own icon otherwise.
selector edits. Use this function when you want the icon a macro shows, and
`GetMacroInfo` when you want the one the player picked.
### `C_Macro.SetMacroDisplay(macroSlot, value)`
### `C_Macro.SetMacroDisplay(macroSlot, value [, unit])`
Tells the client what a macro is about, so the action button shows it. For
an addon that parses macros itself and wants the button to follow, instead
@@ -15350,9 +15371,10 @@ of replacing `GetActionTexture`, `GetActionCooldown`, `IsUsableAction` and
the rest in Lua.
```lua
C_Macro.SetMacroDisplay(1, "Frostbolt") -- show this
C_Macro.SetMacroDisplay(1, false) -- mine, nothing matched, show ?
C_Macro.SetMacroDisplay(1, nil) -- released, `#showtooltip` resumes
C_Macro.SetMacroDisplay(1, "Frostbolt") -- show this
C_Macro.SetMacroDisplay(1, "Flash Heal", "mouseover") -- show this, range to the mouseover
C_Macro.SetMacroDisplay(1, false) -- mine, nothing matched, show ?
C_Macro.SetMacroDisplay(1, nil) -- released, `#showtooltip` resumes
```
`macroSlot` is the macro index, the same one
@@ -15366,6 +15388,31 @@ it, the same as a directive that names its own value.
Returns `true` when the value named a spell or an item.
**The range unit.** `unit` is optional. It names the unit that the button's
range check uses in place of your target. Usually it is the `[@unit]` of the
clause that your parser matched. It takes any unit token or GUID string that
`UnitExists` takes, for example `"mouseover"`, `"focus"` or
`"party2target"`. If you give no `unit`, or `nil`, the range check uses your
target.
The client finds the unit again each time it checks the button's range. So
if `"mouseover"` moves to a different unit, the range follows it with no new
call. The same is true for `"focus"`, `"target"` and every other token. This
is different from the `target` of
[`SetMacroSpell`](#setmacrospellmacro-spell--target--setmacroitemmacro-item--target),
which is found once, when you call it.
If the token points at no unit at that time, for example `"mouseover"` with
nothing under the cursor, the range check uses your target. If the unit is
too far away for the client to see it, for example a party member in another
zone, the button has no range state.
The function ignores `unit` when `value` is `false`, `nil` or an empty
string, because the macro then aims at nothing. A call with the same `value`
and a different `unit` changes the unit. If `unit` is not a unit token, the
function raises the error `Unknown unit name: <unit>`, the same error that
`UnitExists` gives. Then it publishes nothing.
**What the button picks up.** The spell is written where the client keeps
each macro's spell, which is the field its own buttons read. So cooldown,
range, usable, out-of-mana greying, the current-cast highlight and
@@ -15379,8 +15426,8 @@ button, on the cursor while you drag the macro, and in the macro window.
The icon replaces the macro's own only when that is the question mark, which
is the same rule `#showtooltip` follows. A macro with a chosen icon keeps it.
**Call it whenever your answer changes.** Nothing is re-evaluated for you.
The value stands until you publish another, and it survives the client
**Call it whenever your answer changes.** Nothing is re-evaluated for you,
except the unit that a `unit` token points at. The value stands until you publish another, and it survives the client
re-reading the macro, so an edit elsewhere cannot quietly replace it.
Publishing takes the macro over: `#showtooltip` is not parsed for it while
@@ -20560,7 +20607,7 @@ is the number that `[stance:N]` and `[form:N]` test.
```lua
local index = GetShapeshiftForm()
if index > 0 then
if index > 0 and index <= GetNumShapeshiftForms() then
local icon, name = GetShapeshiftFormInfo(index)
print("active:", name)
end
@@ -23142,6 +23189,32 @@ The player's target lands with the confirming packet, about one round-trip
after the cast starts. So `UnitSpellTargetName("player")` can read `nil` for
the first part of your own cast, then return the name.
### `UnitHasIncomingResurrection(unit)`
```
hasIncomingResurrection = UnitHasIncomingResurrection(unit)
```
Returns true while somebody casts a resurrection spell at `unit`. Otherwise
it returns false. If `unit` is a released player, the function also includes
casts at the corpse of that player.
```lua
if UnitHasIncomingResurrection("party1") then
print(UnitName("party1"), "has an incoming resurrection")
end
```
The value changes to true when the cast starts. It changes to false when the
cast stops, for any reason. This includes a completed cast that the target did
not accept yet.
You see a cast only when its caster is near you. If the caster goes out of
range during the cast, the value changes to false.
[`INCOMING_RESURRECT_CHANGED`](#incoming_resurrect_changed-event) fires at
each change.
### `CanBeRaidTarget(unit)`
Returns true if you can put a raid target icon on the unit. Otherwise it
+7
View File
@@ -3396,6 +3396,13 @@ enum Offsets {
SMSG_SPELL_CHANNEL_UPDATE = 0x13A,
SMSG_SPELL_DELAYED = 0x1E2,
SMSG_SPELL_FAILED_OTHER = 0x2A6,
// SpellCastTargets mask bits that carry a packed object GUID right after
// the u16 mask in SMSG_SPELL_START / SMSG_SPELL_GO: unit 0x2, PvP corpse
// 0x200, game object 0x800, corpse 0x8000 — one GUID for whichever is
// set (the client's target reader 0x007E4ED0; tortoise-wow
// SpellCastTargets::write). A resurrect aimed at a released player
// carries the corpse.
SPELL_TARGET_FLAGS_OBJECT = 0x8A02,
// Melee swing result broadcast. Body (verified against the server's own
// writer, tortoise-wow Unit::SendAttackStateUpdate): u32 hitInfo,
// packedGuid attacker, packedGuid victim, u32 totalDamage, u8 subCount,
+8 -10
View File
@@ -42,6 +42,7 @@
#include "combatlog/RecentCasts.h"
#include "net/PacketDispatch.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -54,13 +55,9 @@ using Net::CDataStore;
using Net::Read;
using Net::ReadPackedGuid;
constexpr uint32_t SMSG_SPELL_START = 0x131;
constexpr uint32_t SMSG_SPELL_GO = 0x132;
// The cast flag that keeps a cast out of the combat log (both servers'
// CAST_FLAG_HIDDEN_COMBATLOG); 4.3.4 tests the same bit.
constexpr uint16_t CAST_FLAG_HIDDEN_COMBATLOG = 0x1;
constexpr uint16_t TARGET_FLAG_OBJECT = 0x8A02;
constexpr uint8_t SPELL_MISS_REFLECT = 11;
// 4.3.4's SpellHasOpenLockEffect tests 33 alone; 1.12's success line also
@@ -85,7 +82,7 @@ int CastingTime(void *unit, int spellId) {
// The rules both cast builders share.
bool Logged(const uint8_t *spell) {
return !Hidden(spell) && !HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
return !Hidden(spell) && !Spell::Lookup::HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX4) &
ATTR_EX4_NO_CAST_LOG) == 0;
}
@@ -127,7 +124,8 @@ void SpellGo(CDataStore *p) {
Read<uint8_t>(p);
}
const uint16_t targetMask = Read<uint16_t>(p);
const uint64_t target = (targetMask & TARGET_FLAG_OBJECT) ? ReadPackedGuid(p) : 0;
const uint64_t target =
(targetMask & Offsets::SPELL_TARGET_FLAGS_OBJECT) ? ReadPackedGuid(p) : 0;
if (Overrun(p))
return;
RecentCasts::Record(caster, static_cast<uint32_t>(spellId), hitList, hits);
@@ -135,17 +133,17 @@ void SpellGo(CDataStore *p) {
return;
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, caster);
const uint8_t *spell = SpellRecord(spellId);
if (unit == nullptr || !Logged(spell) || HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) ||
CastingTime(unit, spellId) != 0)
if (unit == nullptr || !Logged(spell) ||
Spell::Lookup::HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) || CastingTime(unit, spellId) != 0)
return;
Entry e = Begin(caster, target, SPELL_CAST_SUCCESS, spell);
Queue(e);
}
void OnPacket(uint32_t opcode, CDataStore *p) {
if (opcode == SMSG_SPELL_START)
if (opcode == Offsets::SMSG_SPELL_START)
SpellStart(p);
else if (opcode == SMSG_SPELL_GO)
else if (opcode == Offsets::SMSG_SPELL_GO)
SpellGo(p);
}
-10
View File
@@ -609,16 +609,6 @@ bool Ranged(const uint8_t *spell) {
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
bool HasEffect(const uint8_t *spell, int effect) {
if (spell == nullptr)
return false;
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT + i * 4) == effect)
return true;
}
return false;
}
bool HasAuraEffect(const uint8_t *spell, int auraName) {
if (spell == nullptr)
return false;
-3
View File
@@ -158,9 +158,6 @@ bool Ranged(const uint8_t *spell);
// A school index (Spell.dbc School, the packets' school) as a school mask.
uint32_t SchoolMask(uint32_t school);
// Whether one of the spell's three effects is `effect`.
bool HasEffect(const uint8_t *spell, int effect);
// Whether one of the spell's three effects applies the aura `auraName`
// (EffectApplyAuraName).
bool HasAuraEffect(const uint8_t *spell, int auraName);
+2 -1
View File
@@ -14,6 +14,7 @@
#include "combatlog/RecentCasts.h"
#include "combatlog/Entry.h"
#include "spell/Lookup.h"
#include "time/Clock.h"
#include <cstdint>
@@ -80,7 +81,7 @@ const uint8_t *SpellByEffect(uint64_t caster, uint64_t target, int effectA, int
if (c.caster != caster || !Hit(c, target))
return false;
const uint8_t *spell = SpellRecord(static_cast<int>(c.spellId));
if (!HasEffect(spell, effectA) && !HasEffect(spell, effectB))
if (!Spell::Lookup::HasEffect(spell, effectA) && !Spell::Lookup::HasEffect(spell, effectB))
return false;
found = spell;
return true;
+20 -3
View File
@@ -11,7 +11,7 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Macro.SetMacroDisplay(macroSlot, value)` — the Lua door onto
// `C_Macro.SetMacroDisplay(macroSlot, value [, unit])` — the Lua door onto
// `Macro::ShowTooltip::Publish`, for an addon that has its own macro parser
// and wants the display done properly rather than by replacing the action
// globals. See the ShowTooltip header for what publishing buys.
@@ -27,6 +27,12 @@
// item-before-spell precedence, `item:N`, links, inventory slots and
// `bag slot` for free.
//
// `unit` is the `[@unit]` the publisher's clause matched: the unit the
// button's range check uses. Unlike `SetMacroSpell`'s target it stays a
// token, resolved at every lookup (`PublishOptions::liveUnit`), so a
// publisher whose clause still matches need not republish when the
// mouseover, focus or target moves.
//
// `SetMacroSpell(macro, spell [, target])` and `SetMacroItem(macro, item
// [, target])` are 4.3.4's narrower doors onto the same publish: a spellbook
// spell only, or an item only, aimed at `target` (the unit the button's range
@@ -37,6 +43,7 @@
#include "Offsets.h"
#include "macro/Arg.h"
#include "macro/ShowTooltip.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -46,7 +53,7 @@ namespace {
int __fastcall Script_C_Macro_SetMacroDisplay(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: C_Macro.SetMacroDisplay(macroSlot, value)");
Game::Lua::Error(L, "Usage: C_Macro.SetMacroDisplay(macroSlot, value [, unit])");
return 0;
}
const int macroSlot = static_cast<int>(Game::Lua::ToNumber(L, 1));
@@ -63,7 +70,17 @@ int __fastcall Script_C_Macro_SetMacroDisplay(void *L) {
}
const char *value = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
Game::Lua::PushBoolean(L, Macro::ShowTooltip::Publish(macroSlot, value) ? 1 : 0);
Macro::ShowTooltip::PublishOptions options;
// A value that names nothing aims at nothing, so its unit is not read.
if (value != nullptr && value[0] != '\0' && Game::Lua::IsString(L, 3)) {
options.unit = Game::Lua::ToString(L, 3);
options.liveUnit = true;
// The engine's resolver vets the token: anything that is not one
// raises its "Unknown unit name" error, exactly as UnitExists does,
// before anything is published.
Unit::Identity::GuidForToken(options.unit);
}
Game::Lua::PushBoolean(L, Macro::ShowTooltip::Publish(macroSlot, value, options) ? 1 : 0);
return 1;
}
+12 -2
View File
@@ -160,6 +160,9 @@ struct Entry {
uint32_t isPet = 0;
int itemID = 0;
uint64_t unit = 0; // Info::unit
// A published `PublishOptions::liveUnit` token, resolved afresh by every
// lookup in place of `unit`. Empty for none.
char unitToken[kOptionsMax] = {};
uint32_t lastEvalMs = 0;
};
@@ -1233,7 +1236,8 @@ bool LookupEntries(uint32_t macroID, Info *out) {
out->spellID = e.spellID;
out->isPet = e.isPet;
out->itemID = e.itemID;
out->unit = e.unit;
out->unit = e.unitToken[0] != '\0' ? ::Unit::Identity::GuidForToken(e.unitToken)
: e.unit;
// A publisher re-publishes the moment its answer changes, so a claimed
// macro never needs the polling refresh a condition of ours does.
out->conditional = false;
@@ -1327,7 +1331,13 @@ bool Publish(int macroSlot, const char *value, const PublishOptions &options) {
ResolveValue(value, &r, /*explicitValue=*/true,
/*itemOnly=*/options.accept == Accept::Item);
}
r.unit = UnitGuid(options.unit);
// Set on every publish, so one that changes only the unit still lands:
// `ApplyResolution` stores the unit before its unchanged-value early out.
if (options.liveUnit && options.unit != nullptr)
std::snprintf(e.unitToken, sizeof(e.unitToken), "%s", options.unit);
else
e.unitToken[0] = '\0';
r.unit = options.liveUnit ? 0 : UnitGuid(options.unit);
BusyScope busy;
ApplyResolution(L, e, r, matched);
+10 -2
View File
@@ -72,8 +72,10 @@ struct Info {
// from it has to keep re-reading while the cursor rests on the button.
bool conditional;
// The unit the macro's spell is aimed at, 4.3.4's MACRONODE::m_target: the
// `[@unit]` of the clause that matched, or the target a `SetMacroSpell` /
// `SetMacroItem` named. 0 means the current target, as for any action.
// `[@unit]` of the clause that matched, the target a `SetMacroSpell` /
// `SetMacroItem` named, or whoever the token a `SetMacroDisplay` named
// points at as of this lookup. 0 means the current target, as for any
// action.
uint64_t unit;
};
@@ -86,6 +88,12 @@ struct PublishOptions {
Accept accept = Accept::Any;
// A unit token or a name the spell is aimed at; null for the current target.
const char *unit = nullptr;
// Resolve `unit` at every lookup rather than once, here, so the macro
// keeps following whoever the token names (a new mouseover, focus or
// target) with no republish. `unit` must then be a token the engine's
// resolver accepts: one that resolves once never raises later, since the
// resolver tells a token's family from its spelling alone.
bool liveUnit = false;
// The claim ends when the macro's body is edited, as 4.3.4's SetMacroSpell
// and SetMacroItem last only until the macro is parsed again. Otherwise it
// lasts until `Release` or a reload.
+73
View File
@@ -0,0 +1,73 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "object/Corpse.h"
#include "Game.h"
#include "Offsets.h"
#include "object/Resolve.h"
namespace Object::Corpse {
namespace {
// `ClntObjMgrEnumVisibleObjects(cb, ctx)`; the callback's dummy EDX slot
// puts the trailing GUID where the engine passes it. Return non-zero to keep
// iterating.
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
const uint8_t *Fields(const void *corpse) {
return Game::Read<const uint8_t *>(corpse, Offsets::OFF_UNIT_DESCRIPTOR);
}
uint64_t Owner(const void *corpse) {
const uint8_t *fields = Fields(corpse);
return fields != nullptr ? Game::Read<uint64_t>(fields, Offsets::OFF_CORPSE_FIELD_OWNER) : 0;
}
struct Scan {
uint64_t owner;
void *corpse;
};
// Bones keep their owner (tortoise-wow Map.cpp, the corpse-to-bones
// conversion), so every earlier death leaves one beside the live corpse;
// 4.3.4 skips them.
int __fastcall ScanCallback(Scan *scan, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid, nullptr, 0);
if (obj == nullptr || Owner(obj) != scan->owner ||
(Game::Read<uint32_t>(Fields(obj), Offsets::OFF_CORPSE_FIELD_FLAGS) &
Offsets::CORPSE_FLAG_BONES) != 0)
return 1;
scan->corpse = obj;
return 0;
}
} // namespace
void *ByOwner(uint64_t owner) {
if (owner == 0 || !Object::RegistryReady())
return nullptr;
Scan scan{owner, nullptr};
reinterpret_cast<EnumVisibleObjects_t>(Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS)(
reinterpret_cast<EnumCallback_t>(&ScanCallback), &scan);
return scan.corpse;
}
uint64_t OwnerOf(uint64_t guid) {
const void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid);
return obj != nullptr ? Owner(obj) : 0;
}
} // namespace Object::Corpse
+30
View File
@@ -0,0 +1,30 @@
// 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/>.
#pragma once
#include <cstdint>
// Player corpses (`TYPEMASK_CORPSE`) and the player each belongs to, read
// from the corpse's CORPSE_FIELD_OWNER.
namespace Object::Corpse {
// The visible corpse `owner` left, not counting bones, or nullptr — 4.3.4's
// ClntObjMgrFindCorpseByOwner, over the visible objects.
void *ByOwner(uint64_t owner);
// The player who left the corpse `guid`, or 0 when `guid` is not a visible
// corpse.
uint64_t OwnerOf(uint64_t guid);
} // namespace Object::Corpse
+2
View File
@@ -88,6 +88,7 @@
#include "time/Clock.h"
#include "unit/Focus.h"
#include "unit/Identity.h"
#include "unit/IncomingResurrection.h"
#include <cstdint>
@@ -691,6 +692,7 @@ void ParseChannelUpdate(Net::CDataStore *packet) {
void HandleCastAborted(uint64_t guid, int spellID, bool castSlotOnly = false) {
if (guid == 0 || spellID == 0)
return;
Unit::IncomingResurrection::OnCastEnded(guid, spellID);
// Remote units: invalidate the cached cast/channel so
// UnitCastingInfo/UnitChannelInfo stop reporting it this tick.
for (auto &e : g_remoteCasts) {
+10
View File
@@ -52,6 +52,16 @@ bool IsFitToFamily(const uint8_t *spellRecord, uint32_t family,
return (flags & flagMask) != 0;
}
bool HasEffect(const uint8_t *spellRecord, int effect) {
if (spellRecord == nullptr)
return false;
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
if (Game::Read<int>(spellRecord, Offsets::OFF_SPELL_RECORD_EFFECT + i * 4) == effect)
return true;
}
return false;
}
int SpellbookSlotToID(int slot1Based, int bookType) {
const int slot = slot1Based - 1;
if (slot < 0 || slot >= Offsets::SPELLBOOK_MAX_SLOTS)
+4
View File
@@ -31,6 +31,10 @@ const uint8_t *RecordForID(int spellID);
bool IsFitToFamily(const uint8_t *spellRecord, uint32_t family,
uint64_t flagMask);
// Whether one of the spell's three effects is `effect` — 4.3.4's
// SpellHasEffect. False for a null record.
bool HasEffect(const uint8_t *spellRecord, int effect);
// Resolves a 1-based Lua-facing spellbook slot to the spellID stored
// there. `bookType` is `0` for the player spellbook, `1` for the pet
// spellbook (matches the engine's encoding from `Script_GetSpellName`).
+2 -25
View File
@@ -15,6 +15,7 @@
#include "Game.h"
#include "Offsets.h"
#include "object/Corpse.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "unit/Position.h"
@@ -114,8 +115,6 @@ constexpr int32_t TARGET_PET = 5;
using Relation_t = bool(__thiscall *)(void *caster, void *other);
using CreatureType_t = int(__thiscall *)(void *unit);
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
bool Relation(uintptr_t fn, void *caster, void *other) {
return reinterpret_cast<Relation_t>(fn)(caster, other);
@@ -181,28 +180,6 @@ uint32_t TargetWord(const uint8_t *rec, bool &aimsAtPet) {
return word;
}
struct CorpseScan {
uint64_t owner;
void *corpse;
};
int __fastcall CorpseScanCallback(CorpseScan *scan, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid, nullptr, 0);
if (obj == nullptr ||
Game::Read<uint64_t>(Fields(obj), Offsets::OFF_CORPSE_FIELD_OWNER) != scan->owner)
return 1;
scan->corpse = obj;
return 0;
}
// 4.3.4's ClntObjMgrFindCorpseByOwner, over the visible objects.
void *CorpseOf(uint64_t owner) {
CorpseScan scan{owner, nullptr};
reinterpret_cast<EnumVisibleObjects_t>(Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS)(
reinterpret_cast<EnumCallback_t>(&CorpseScanCallback), &scan);
return scan.corpse;
}
// A unit target (0x006E4511..0x006E469A).
bool AdmitsUnit(void *caster, const uint8_t *rec, uint32_t word, void *unit) {
const uint8_t *fields = Fields(unit);
@@ -281,7 +258,7 @@ void *AdmittedTarget(void *caster, const uint8_t *rec, void *target) {
if (info != nullptr &&
(Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_FLAGS) &
Offsets::PLAYER_FLAG_GHOST) != 0) {
if (void *corpse = CorpseOf(Game::Read<uint64_t>(Head(target), 0)))
if (void *corpse = Object::Corpse::ByOwner(Game::Read<uint64_t>(Head(target), 0)))
return corpse;
}
}
+217
View File
@@ -0,0 +1,217 @@
// 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/>.
// `UnitHasIncomingResurrection(unit)` and `INCOMING_RESURRECT_CHANGED` —
// 4.3.4's CGIncomingResurrection (Ui/IncomingResurrection.cpp) and the two
// calls its spell code makes into it:
// - SMSG_SPELL_START: a spell with a resurrect effect and an object target
// adds {caster, spell} under that target
// (PendingSpellCast::OnSpellStartPacket, 0x008ACD80);
// - the cast's end removes it (FinishPendingSpellCast, 0x008AF840, which
// SPELL_GO and every failure run).
// Each change signals INCOMING_RESURRECT_CHANGED to every token naming the
// target, or the corpse's owner when the target is a corpse
// (Script_SendUnitSignal). The query answers for the unit itself, then for
// its corpse (HasIncomingResurrection, 0x009FCF80).
//
// Where 1.12 differs:
// - Resurrect effects. 4.3.4 tests effect 18 alone. 1.12's class
// resurrections (Resurrection, Redemption, Ancestral Spirit, Rebirth,
// every rank) carry 113, RESURRECT_NEW, and 18 only item and NPC spells
// (Spell.dbc), so both count.
// - The cast's end. 1.12 keeps no pending cast. Its end is the caster's
// SMSG_SPELL_GO, or CGUnit_C::ClearCastingSpell and the failure packets,
// which `Spell::Cast` owns and reports through OnCastEnded. Turtle sends
// observers no failure packet on an interrupt; SuperWoW calls
// ClearCastingSpell instead (docs/notes/cast-channel.md).
// - Lifetime. 4.3.4 empties the table on entering and leaving the world. A
// cast whose caster leaves view never reports its end, so an entry whose
// caster is no longer visible ends on the next world tick, which covers
// a world change as well.
// - One cast per caster, as the unit's single precast slot (+0xC8C) holds:
// a caster's new resurrection replaces its last.
#include "unit/IncomingResurrection.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "net/PacketDispatch.h"
#include "object/Corpse.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "tick/WorldTick.h"
#include "unit/Identity.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Unit::IncomingResurrection {
namespace {
constexpr int SPELL_EFFECT_RESURRECT = 18;
constexpr int SPELL_EFFECT_RESURRECT_NEW = 113;
const Game::Doc::Field kChangedPayload[] = {
Game::Doc::Req("unitTarget", "UnitToken", "The unit, or the player whose corpse it is."),
};
const Game::Doc::Event kChangedDoc{
"Unit", "A resurrection cast at the unit began or ended. Read UnitHasIncomingResurrection.",
kChangedPayload};
const Event::Custom::AutoReserve kChanged{"INCOMING_RESURRECT_CHANGED", &kChangedDoc};
struct Entry {
uint64_t target; // the cast's object target: a unit or a corpse
uint64_t unit; // who hears the change: the corpse's owner, else target
uint64_t caster;
int spellID;
};
std::vector<Entry> g_entries;
void Signal(uint64_t unit) {
const int slot = kChanged.Slot();
if (!Event::Custom::HasListeners(slot))
return;
char tokens[16][32];
const int n = Unit::Identity::TokensForGUID(unit, tokens, 16);
for (int i = 0; i < n; ++i)
Event::Custom::Fire(slot, "%s", tokens[i]);
}
// Ends `caster`'s resurrection: of `spellID`, or any for 0.
void End(uint64_t caster, int spellID) {
for (auto it = g_entries.begin(); it != g_entries.end(); ++it) {
if (it->caster == caster && (spellID == 0 || it->spellID == spellID)) {
const uint64_t unit = it->unit;
g_entries.erase(it);
Signal(unit);
return;
}
}
}
void Begin(uint64_t target, uint64_t caster, int spellID) {
for (const Entry &e : g_entries) {
if (e.caster == caster && e.target == target && e.spellID == spellID)
return;
}
End(caster, 0);
const uint64_t owner = Object::Corpse::OwnerOf(target);
const uint64_t unit = owner != 0 ? owner : target;
g_entries.push_back({target, unit, caster, spellID});
Signal(unit);
}
bool Targeted(uint64_t target) {
for (const Entry &e : g_entries) {
if (e.target == target)
return true;
}
return false;
}
bool HasIncomingResurrection(uint64_t guid) {
if (guid == 0 || g_entries.empty())
return false;
if (Targeted(guid))
return true;
const void *corpse = Object::Corpse::ByOwner(guid);
return corpse != nullptr && Targeted(Unit::Identity::GuidForObject(corpse));
}
bool IsResurrect(int spellID) {
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
return Spell::Lookup::HasEffect(rec, SPELL_EFFECT_RESURRECT) ||
Spell::Lookup::HasEffect(rec, SPELL_EFFECT_RESURRECT_NEW);
}
// SMSG_SPELL_START / SMSG_SPELL_GO: packed item or caster, packed caster,
// spell, then for SPELL_START u16 flags, u32 cast time and the targets.
void OnPacket(uint32_t opcode, Net::CDataStore *p) {
if (opcode != Offsets::SMSG_SPELL_START && opcode != Offsets::SMSG_SPELL_GO)
return;
if (opcode == Offsets::SMSG_SPELL_GO && g_entries.empty())
return;
Net::ReadPackedGuid(p);
const uint64_t caster = Net::ReadPackedGuid(p);
const int spellID = static_cast<int>(Net::Read<uint32_t>(p));
if (opcode == Offsets::SMSG_SPELL_GO) {
if (p->m_read <= p->m_size)
End(caster, spellID);
return;
}
Net::Read<uint16_t>(p);
Net::Read<uint32_t>(p);
const uint16_t targetMask = Net::Read<uint16_t>(p);
const uint64_t target =
(targetMask & Offsets::SPELL_TARGET_FLAGS_OBJECT) ? Net::ReadPackedGuid(p) : 0;
if (p->m_read > p->m_size || caster == 0 || target == 0 || !IsResurrect(spellID))
return;
Begin(target, caster, spellID);
}
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
void OnWorldTick() {
for (size_t i = 0; i < g_entries.size();) {
if (Object::ByGuid(Offsets::TYPEMASK_UNIT, g_entries[i].caster) != nullptr) {
++i;
continue;
}
const uint64_t unit = g_entries[i].unit;
g_entries.erase(g_entries.begin() + static_cast<std::ptrdiff_t>(i));
Signal(unit);
}
}
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
int __fastcall Script_UnitHasIncomingResurrection(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: UnitHasIncomingResurrection(\"unit\")");
return 0;
}
Game::Lua::PushBool(L, HasIncomingResurrection(
Unit::Identity::GuidForToken(Game::Lua::ToString(L, 1))));
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("hasIncomingResurrection", "bool"),
};
const Game::Doc::Function kUnitHasIncomingResurrection{
"Whether a resurrection is being cast at the unit, or at the corpse of a released player.",
kArgs, kRets, "UnitGlobals"};
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("UnitHasIncomingResurrection",
&Script_UnitHasIncomingResurrection,
&kUnitHasIncomingResurrection);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
void OnCastEnded(uint64_t caster, int spellID) {
if (caster != 0 && spellID != 0 && !g_entries.empty())
End(caster, spellID);
}
} // namespace Unit::IncomingResurrection
+28
View File
@@ -0,0 +1,28 @@
// 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/>.
#pragma once
#include <cstdint>
// 4.3.4's CGIncomingResurrection: the resurrections being cast at each unit
// or corpse. Backs `UnitHasIncomingResurrection` and
// `INCOMING_RESURRECT_CHANGED`.
namespace Unit::IncomingResurrection {
// `caster` stopped casting `spellID`: CGUnit_C::ClearCastingSpell or a
// failure packet, both owned by `Spell::Cast`. Ends the resurrection it was
// casting, if that is one.
void OnCastEnded(uint64_t caster, int spellID);
} // namespace Unit::IncomingResurrection