Compare commits

...
1 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
15 changed files with 432 additions and 50 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` |
+47
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)
@@ -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
@@ -23168,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;
+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