mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 23:20:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b7d03c28 | ||
|
|
15cae8a474 |
@@ -131,6 +131,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_STOPPED_TURNING` | *(none)* |
|
||||
| `QUEST_ACCEPTED` | `questLogIndex, questID` |
|
||||
| `QUEST_DATA_LOAD_RESULT` | `questID, success` |
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_STARTED` | `numConsumers, utteranceID, durationMS, destination` |
|
||||
|
||||
+36
@@ -102,6 +102,7 @@ build instructions.
|
||||
- [`NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events)
|
||||
- [`PLAYER_FOCUS_CHANGED` event](#player_focus_changed-event)
|
||||
- [`QUEST_ACCEPTED` event](#quest_accepted-event)
|
||||
- [`QUEST_REMOVED` event](#quest_removed-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
@@ -2387,6 +2388,41 @@ quests within the same engine tick, so single-add is always a real
|
||||
user accept. A brand-new character's very first quest accept
|
||||
(`0 → 1` entries) fires correctly.
|
||||
|
||||
### `QUEST_REMOVED` event
|
||||
|
||||
Fires once per quest leaving the local quest log, with the questID as
|
||||
the single payload arg. Covers **both turn-ins and abandons** — the
|
||||
two are distinguishable by whether a `QUEST_TURNED_IN` accompanies
|
||||
the removal. Polyfills modern WoW's event of the same name (added in
|
||||
8.0.1; the Classic-era signature is the bare questID, which is what
|
||||
we match).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("QUEST_REMOVED")
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "QUEST_REMOVED" then
|
||||
local questID = arg1
|
||||
-- ...
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
Synthesized from the same `FUN_QUEST_LOG_REBUILD` pre-/post-snapshot
|
||||
diff as `QUEST_ACCEPTED` — the removal side of the delta. For
|
||||
turn-ins, `QUEST_REMOVED` fires **after** `QUEST_TURNED_IN`, matching
|
||||
retail ordering: the SMSG_QUESTGIVER_QUEST_COMPLETE packet doesn't
|
||||
touch the log itself; the removal arrives in the follow-up quest-log
|
||||
update packets, whose rebuild triggers the diff. The observed turn-in
|
||||
sequence is `QUEST_TURNED_IN` → `UNIT_QUEST_LOG_CHANGED` →
|
||||
`QUEST_LOG_UPDATE` (fired inside the rebuild) → `QUEST_REMOVED`.
|
||||
|
||||
**Does not fire on login / character-switch resyncs.** Same
|
||||
suppression rule as `QUEST_ACCEPTED` (a user action removes at most
|
||||
one quest per rebuild), plus one extra gate: a rebuild that removes
|
||||
one quest while adding several is a cross-character resync shape, not
|
||||
gameplay, and stays silent.
|
||||
|
||||
### `QUEST_TURNED_IN` event
|
||||
|
||||
Fires when the server confirms a quest turn-in
|
||||
|
||||
+78
-4
@@ -2440,10 +2440,10 @@ enum Offsets {
|
||||
// walks the player's slot data, repopulates `VAR_QUEST_LOG_ENTRIES`
|
||||
// / `VAR_QUEST_LOG_ENTRY_COUNT`, then fires QUEST_LOG_UPDATE.
|
||||
//
|
||||
// Hooked by `Quest::Accepted` to snapshot the log pre-/post-rebuild
|
||||
// and fire `QUEST_ACCEPTED(logIndex, questID)` for the diff. Single
|
||||
// quiet target in the `0x004DExxx` quest region — no known DLL
|
||||
// collisions.
|
||||
// Hooked by `Quest::LogEvents` to snapshot the log pre-/post-rebuild
|
||||
// and fire `QUEST_ACCEPTED(logIndex, questID)` / `QUEST_REMOVED(questID)`
|
||||
// for the diff. Single quiet target in the `0x004DExxx` quest region —
|
||||
// no known DLL collisions.
|
||||
FUN_QUEST_LOG_REBUILD = 0x004DE510,
|
||||
|
||||
// Player and pet spellbooks — flat int32 arrays indexed by 0-based slot.
|
||||
@@ -4359,6 +4359,80 @@ enum Offsets {
|
||||
FUN_GOSSIP_SELECT_ACTIVE_QUEST = 0x004E2600,
|
||||
FUN_SCRIPT_CLOSE_GOSSIP = 0x004E2B20,
|
||||
|
||||
// Current gossip NPC GUID (u64). Set by the SMSG_GOSSIP_MESSAGE
|
||||
// handler, cleared by the gossip-close worker at 0x004E1FA0 (which
|
||||
// then fires GOSSIP_CLOSED, event 0x12B). Non-zero = a gossip
|
||||
// session is live — the gate that keeps `C_GossipInfo` from serving
|
||||
// the previous NPC's stale arrays/text after close.
|
||||
VAR_GOSSIP_NPC_GUID = 0x00BC3F58,
|
||||
|
||||
// --- Quest-greeting state (SMSG_QUESTGIVER_QUEST_LIST) -------------
|
||||
// Vanilla's SECOND questgiver path: NPCs without a gossip menu send
|
||||
// SMSG_QUESTGIVER_QUEST_LIST, whose handler (FUN_005dbb10) fills a
|
||||
// storage entirely separate from the gossip arrays and fires
|
||||
// QUEST_GREETING (event 300 = 0x12C) — the QuestFrameGreetingPanel
|
||||
// ("Current Quests" / "Available Quests"). Vanilla Lua reads it via
|
||||
// GetGreetingText / Get{Available,Active}Title / etc.; C_GossipInfo
|
||||
// merges it in so modern-addon ports work on greeting NPCs too.
|
||||
//
|
||||
// Shared quest-session state, written by the panel-begin helper
|
||||
// FUN_00500bd0(panelType, textID, guidLo, guidHi, ...):
|
||||
// VAR_QUESTGIVER_GUID — u64, the current questgiver. Cleared by
|
||||
// the session-close worker FUN_00501130 (fires QUEST_FINISHED,
|
||||
// event 0x130). Non-zero = quest session live.
|
||||
// VAR_QUEST_PANEL_TYPE — i32 panel: 0 greeting, 1 detail,
|
||||
// 2 progress, 3 reward. begin(0) also zeroes both greeting
|
||||
// arrays + counts + the text buffers.
|
||||
VAR_QUESTGIVER_GUID = 0x00BE0810,
|
||||
VAR_QUEST_PANEL_TYPE = 0x00BE0818,
|
||||
|
||||
// GUID the greeting arrays were filled for — written ONLY by the
|
||||
// SMSG_QUESTGIVER_QUEST_LIST handler. The greeting arrays are valid
|
||||
// iff this equals VAR_QUESTGIVER_GUID (and that is non-zero);
|
||||
// otherwise they're leftovers from an older NPC (e.g. the current
|
||||
// quest session came from a gossip-menu quest click, which begins
|
||||
// at panel 1 without ever clearing the greeting arrays).
|
||||
VAR_GREETING_NPC_GUID = 0x00C4D740,
|
||||
|
||||
// Greeting text (0x200 inline, $N/$C-expanded) — what vanilla's
|
||||
// Script_GetGreetingText pushes.
|
||||
VAR_QUEST_GREETING_TEXT = 0x00BDE068,
|
||||
|
||||
// The two greeting quest arrays: 32 entries, stride 0x4C =
|
||||
// { u32 questID @ +0, i32 level @ +4, char title[0x40] @ +8,
|
||||
// u32 extra @ +0x48 (available: status==0 flag; active: always
|
||||
// 0 — the appender FUN_00500e90 never stores the status, see
|
||||
// the scratch arrays below for isComplete) }.
|
||||
// Appenders: FUN_00500e40 (available), FUN_00500e90 (active).
|
||||
// Readers: Script_Get{Available,Active}{Title,Level} @ 0x00501AC0/
|
||||
// 0x00501B30/0x00501BA0/0x00501C20.
|
||||
VAR_GREETING_AVAILABLE_ENTRIES = 0x00BDFE60,
|
||||
VAR_GREETING_ACTIVE_ENTRIES = 0x00BDE690,
|
||||
GREETING_QUESTS_STRIDE = 0x4C,
|
||||
GREETING_QUESTS_MAX = 32,
|
||||
OFF_GREETING_QUEST_ID = 0x000,
|
||||
OFF_GREETING_QUEST_LEVEL = 0x004,
|
||||
OFF_GREETING_QUEST_TITLE = 0x008,
|
||||
VAR_GREETING_AVAILABLE_COUNT = 0x00BE0834,
|
||||
VAR_GREETING_ACTIVE_COUNT = 0x00BE0838,
|
||||
|
||||
// Scratch arrays the SMSG_QUESTGIVER_QUEST_LIST handler fills in
|
||||
// packet order (32 × u32 each, zeroed per packet): questIDs and the
|
||||
// per-quest dialog status. Status 3|4 = active (same sentinel as
|
||||
// the gossip quest rows), 4 = complete/turn-in-ready. The canonical
|
||||
// greeting arrays drop the status, so `isComplete` for an active
|
||||
// greeting quest is recovered by questID lookup here.
|
||||
VAR_GREETING_SCRATCH_IDS = 0x00C4D2B0,
|
||||
VAR_GREETING_SCRATCH_STATUS = 0x00C4D340,
|
||||
|
||||
// Engine greeting-quest selectors — the workers vanilla's
|
||||
// Script_Select{Available,Active}Quest call: `__fastcall(int
|
||||
// idx0Based)`, index straight into the corresponding greeting
|
||||
// array. They validate VAR_QUESTGIVER_GUID internally and send
|
||||
// CMSG_QUESTGIVER_{QUERY_QUEST,COMPLETE_QUEST}.
|
||||
FUN_GREETING_SELECT_AVAILABLE_QUEST = 0x005012A0,
|
||||
FUN_GREETING_SELECT_ACTIVE_QUEST = 0x00501320,
|
||||
|
||||
// CDataStore primitive readers — `__thiscall void(this, void *out)`.
|
||||
// Read at the current cursor (`+0x14`) and advance it by the value
|
||||
// size. We call these directly from `Unit::MirrorTimer` to peek
|
||||
|
||||
+159
-11
@@ -22,6 +22,24 @@
|
||||
// SMSG_GOSSIP_MESSAGE handler at `0x004E26E0` and cleared each open.
|
||||
// We read the same shape the engine's own `Script_GetGossip*` reads.
|
||||
//
|
||||
// Vanilla has a SECOND questgiver path this namespace also covers:
|
||||
// NPCs without a gossip menu send SMSG_QUESTGIVER_QUEST_LIST instead —
|
||||
// the QUEST_GREETING event and the QuestFrameGreetingPanel ("Current
|
||||
// Quests" / "Available Quests") — whose data lands in a storage
|
||||
// entirely separate from the gossip arrays (see the VAR_GREETING_*
|
||||
// block in Offsets.h). Retail keeps the two APIs disjoint, but for
|
||||
// backporting it's far more useful for `C_GossipInfo` to serve
|
||||
// whichever questgiver session is actually live:
|
||||
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0) → gossip arrays
|
||||
// - greeting session live (VAR_QUESTGIVER_GUID != 0
|
||||
// and == VAR_GREETING_NPC_GUID) → greeting arrays
|
||||
// - neither → empty / ""
|
||||
// The GUID gates double as staleness protection: both storages persist
|
||||
// after their frames close (the engine never clears them until the
|
||||
// next open), so ungated reads would report the previous NPC.
|
||||
// `GetOptions`/`GetNumOptions` stay gossip-only — greetings have no
|
||||
// options.
|
||||
//
|
||||
// Missing modern fields — vanilla 1.12's server simply never sends
|
||||
// them, so there's nothing to surface:
|
||||
// - `rewards`, `spellID` (post-vanilla; merged in with the
|
||||
@@ -46,6 +64,57 @@ namespace {
|
||||
|
||||
using EngineScriptFn = int(__fastcall *)(void *L);
|
||||
|
||||
uint64_t ReadU64(uintptr_t va) {
|
||||
return *reinterpret_cast<const uint64_t *>(va);
|
||||
}
|
||||
|
||||
// A gossip session is live from SMSG_GOSSIP_MESSAGE until the close
|
||||
// worker (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED.
|
||||
bool GossipSessionActive() {
|
||||
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0;
|
||||
}
|
||||
|
||||
// A quest-greeting session is live while the quest-session GUID is set
|
||||
// AND the greeting arrays were filled for that same NPC. The second
|
||||
// check matters because a gossip-menu quest click opens a quest session
|
||||
// at panel 1 without ever touching the greeting arrays — without it,
|
||||
// leftovers from an older greeting NPC would leak through.
|
||||
bool GreetingSessionActive() {
|
||||
const uint64_t giver = ReadU64(Offsets::VAR_QUESTGIVER_GUID);
|
||||
return giver != 0 && giver == ReadU64(Offsets::VAR_GREETING_NPC_GUID);
|
||||
}
|
||||
|
||||
const uint8_t *GreetingEntry(bool active, int slot) {
|
||||
const uintptr_t base = active ? Offsets::VAR_GREETING_ACTIVE_ENTRIES
|
||||
: Offsets::VAR_GREETING_AVAILABLE_ENTRIES;
|
||||
return reinterpret_cast<const uint8_t *>(
|
||||
base + static_cast<uintptr_t>(slot) * Offsets::GREETING_QUESTS_STRIDE);
|
||||
}
|
||||
|
||||
int GreetingCount(bool active) {
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
active ? Offsets::VAR_GREETING_ACTIVE_COUNT
|
||||
: Offsets::VAR_GREETING_AVAILABLE_COUNT);
|
||||
return (count < 0) ? 0
|
||||
: (count > Offsets::GREETING_QUESTS_MAX) ? Offsets::GREETING_QUESTS_MAX
|
||||
: count;
|
||||
}
|
||||
|
||||
// The greeting arrays drop the per-quest dialog status, but the packet
|
||||
// handler's scratch arrays (packet order, zeroed per packet) keep it —
|
||||
// status 4 = complete/turn-in-ready, same sentinel as the gossip rows.
|
||||
bool GreetingQuestIsComplete(uint32_t questID) {
|
||||
const auto *ids = reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_GREETING_SCRATCH_IDS);
|
||||
const auto *status = reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_GREETING_SCRATCH_STATUS);
|
||||
for (int i = 0; i < Offsets::GREETING_QUESTS_MAX; ++i) {
|
||||
if (ids[i] == questID)
|
||||
return status[i] == 4;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *OptionEntry(int slot) {
|
||||
return reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_GOSSIP_OPTIONS) +
|
||||
@@ -79,14 +148,21 @@ bool IsActiveQuest(uint32_t status) {
|
||||
return status == 3 || status == 4;
|
||||
}
|
||||
|
||||
// `C_GossipInfo.GetText()` — pushes the greeting string the engine
|
||||
// resolved for the gossip-giver's NPC_TEXT.dbc entry. Mirrors what
|
||||
// `Script_GetGossipText` does (one inline `lua_pushstring` of the
|
||||
// fixed buffer at `VAR_GOSSIP_GREETING_TEXT`).
|
||||
// `C_GossipInfo.GetText()` — the greeting string of whichever
|
||||
// questgiver session is live: the gossip buffer (what
|
||||
// `Script_GetGossipText` pushes) for a gossip session, the quest
|
||||
// greeting buffer (what `Script_GetGreetingText` pushes) for a
|
||||
// greeting session, "" when neither is open. Both buffers persist
|
||||
// after close, so the gates keep stale text from the previous NPC
|
||||
// out.
|
||||
int __fastcall Script_C_GossipInfo_GetText(void *L) {
|
||||
const char *text = reinterpret_cast<const char *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_GOSSIP_GREETING_TEXT));
|
||||
Game::Lua::PushString(L, text);
|
||||
uintptr_t buf = 0;
|
||||
if (GossipSessionActive())
|
||||
buf = Offsets::VAR_GOSSIP_GREETING_TEXT;
|
||||
else if (GreetingSessionActive())
|
||||
buf = Offsets::VAR_QUEST_GREETING_TEXT;
|
||||
Game::Lua::PushString(
|
||||
L, (buf != 0) ? reinterpret_cast<const char *>(buf) : "");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -108,6 +184,8 @@ int __fastcall Script_C_GossipInfo_GetText(void *L) {
|
||||
int __fastcall Script_C_GossipInfo_GetOptions(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
if (!GossipSessionActive())
|
||||
return 1; // greeting sessions have no options; stale data stays hidden
|
||||
|
||||
int outIdx = 0;
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
|
||||
@@ -138,6 +216,36 @@ int __fastcall Script_C_GossipInfo_GetOptions(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Greeting-session variant: walks the pre-split greeting array for the
|
||||
// wanted side. Same table shape as the gossip rows (questID / title /
|
||||
// questLevel / isComplete) so consumers can't tell the sources apart.
|
||||
int PushGreetingQuestList(void *L, bool wantActive) {
|
||||
const int count = GreetingCount(wantActive);
|
||||
for (int slot = 0; slot < count; ++slot) {
|
||||
const uint8_t *entry = GreetingEntry(wantActive, slot);
|
||||
const uint32_t questID = *reinterpret_cast<const uint32_t *>(
|
||||
entry + Offsets::OFF_GREETING_QUEST_ID);
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
Game::Lua::SetFieldNumber(L, "questID",
|
||||
static_cast<double>(questID));
|
||||
Game::Lua::SetFieldString(L, "title", reinterpret_cast<const char *>(
|
||||
entry + Offsets::OFF_GREETING_QUEST_TITLE));
|
||||
const int32_t level = *reinterpret_cast<const int32_t *>(
|
||||
entry + Offsets::OFF_GREETING_QUEST_LEVEL);
|
||||
Game::Lua::SetFieldNumber(L, "questLevel",
|
||||
static_cast<double>(level));
|
||||
if (wantActive)
|
||||
Game::Lua::SetFieldBool(L, "isComplete",
|
||||
GreetingQuestIsComplete(questID));
|
||||
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Shared body for `GetAvailableQuests` / `GetActiveQuests`. Both walk
|
||||
// the same backing array and split by the `status` field — `wantActive`
|
||||
// inverts the filter.
|
||||
@@ -145,6 +253,12 @@ int PushQuestList(void *L, bool wantActive) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
if (!GossipSessionActive()) {
|
||||
if (GreetingSessionActive())
|
||||
return PushGreetingQuestList(L, wantActive);
|
||||
return 1; // no questgiver session — empty table, never stale data
|
||||
}
|
||||
|
||||
int outIdx = 0;
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_QUESTS_MAX; ++slot) {
|
||||
const uint8_t *entry = QuestEntry(slot);
|
||||
@@ -189,15 +303,19 @@ int __fastcall Script_C_GossipInfo_GetActiveQuests(void *L) {
|
||||
|
||||
int __fastcall Script_C_GossipInfo_GetNumOptions(void *L) {
|
||||
int count = 0;
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
|
||||
if (OptionIndex(OptionEntry(slot)) >= 0)
|
||||
count += 1;
|
||||
if (GossipSessionActive()) {
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
|
||||
if (OptionIndex(OptionEntry(slot)) >= 0)
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(count));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int CountQuests(bool wantActive) {
|
||||
if (!GossipSessionActive())
|
||||
return GreetingSessionActive() ? GreetingCount(wantActive) : 0;
|
||||
int count = 0;
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_QUESTS_MAX; ++slot) {
|
||||
const uint8_t *entry = QuestEntry(slot);
|
||||
@@ -247,6 +365,17 @@ void EngineSelectActiveQuest(int idx0Based) {
|
||||
fn(idx0Based);
|
||||
}
|
||||
|
||||
// Greeting-session selectors — the workers vanilla's
|
||||
// Script_Select{Available,Active}Quest call. Index straight into the
|
||||
// corresponding greeting array (they validate the questgiver GUID
|
||||
// internally before sending the CMSG).
|
||||
void EngineSelectGreetingQuest(bool wantActive, int idx0Based) {
|
||||
auto fn = reinterpret_cast<SelectQuest_t>(
|
||||
wantActive ? Offsets::FUN_GREETING_SELECT_ACTIVE_QUEST
|
||||
: Offsets::FUN_GREETING_SELECT_AVAILABLE_QUEST);
|
||||
fn(idx0Based);
|
||||
}
|
||||
|
||||
// `C_GossipInfo.SelectOption(gossipOptionID[, text])` — wraps vanilla's
|
||||
// `SelectGossipOption(slotIndex)`. `text` is the boxCoded password,
|
||||
// passed through to the engine helper which gates on the
|
||||
@@ -260,6 +389,8 @@ int __fastcall Script_C_GossipInfo_SelectOption(void *L) {
|
||||
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *password =
|
||||
Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
if (!GossipSessionActive())
|
||||
return 0; // stale option slots must not send a CMSG
|
||||
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
|
||||
const uint8_t *entry = OptionEntry(slot);
|
||||
@@ -284,7 +415,8 @@ int __fastcall Script_C_GossipInfo_SelectOptionByIndex(void *L) {
|
||||
return 0;
|
||||
}
|
||||
const int slot0Based = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0Based < 0 || slot0Based >= Offsets::GOSSIP_OPTIONS_MAX)
|
||||
if (slot0Based < 0 || slot0Based >= Offsets::GOSSIP_OPTIONS_MAX ||
|
||||
!GossipSessionActive())
|
||||
return 0;
|
||||
EngineSelectOption(slot0Based, nullptr);
|
||||
return 0;
|
||||
@@ -301,6 +433,22 @@ int SelectQuestByID(void *L, bool wantActive, const char *errUsage) {
|
||||
}
|
||||
const uint32_t target = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
if (!GossipSessionActive()) {
|
||||
if (!GreetingSessionActive())
|
||||
return 0;
|
||||
const int count = GreetingCount(wantActive);
|
||||
for (int slot = 0; slot < count; ++slot) {
|
||||
const uint32_t questID = *reinterpret_cast<const uint32_t *>(
|
||||
GreetingEntry(wantActive, slot) +
|
||||
Offsets::OFF_GREETING_QUEST_ID);
|
||||
if (questID == target) {
|
||||
EngineSelectGreetingQuest(wantActive, slot);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_QUESTS_MAX; ++slot) {
|
||||
const uint8_t *entry = QuestEntry(slot);
|
||||
|
||||
@@ -11,18 +11,29 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `QUEST_ACCEPTED(questLogIndex, questID)` event — fires once per
|
||||
// new quest entering the local quest log. Polyfills modern WoW's
|
||||
// event of the same name (added in 3.1.0). Matches the Cata/WotLK
|
||||
// signature: `(questLogIndex, questID)`, 1-based log index.
|
||||
// Quest log delta events — polyfills two modern WoW events from one
|
||||
// pre-/post-rebuild diff:
|
||||
//
|
||||
// `QUEST_ACCEPTED(questLogIndex, questID)` — fires once per new quest
|
||||
// entering the local quest log. Matches the Cata/WotLK signature,
|
||||
// 1-based log index (event added in 3.1.0).
|
||||
//
|
||||
// `QUEST_REMOVED(questID)` — fires once per quest leaving the local
|
||||
// quest log, for both turn-ins and abandons (event added in 8.0.1;
|
||||
// Classic-era signature is the bare questID). For turn-ins this fires
|
||||
// *after* `QUEST_TURNED_IN`, matching retail ordering: the 0x191
|
||||
// complete packet doesn't touch the log; the removal arrives in the
|
||||
// follow-up quest-log update packets, whose rebuild we diff here.
|
||||
// Observed turn-in sequence: QUEST_TURNED_IN → UNIT_QUEST_LOG_CHANGED
|
||||
// → QUEST_LOG_UPDATE (fired inside the rebuild) → QUEST_REMOVED.
|
||||
//
|
||||
// Hook target: `FUN_QUEST_LOG_REBUILD` (`0x004DE510`) — the single
|
||||
// chokepoint the engine uses to refresh the Lua-visible quest log
|
||||
// from the player's authoritative slot data at `[CGPlayer + 0xE68 +
|
||||
// 0x28]`. Every state change that adds, removes, or updates a quest
|
||||
// flows through here. By snapshotting the log before and after the
|
||||
// original runs, we can compute the delta and fire QUEST_ACCEPTED
|
||||
// only for genuine additions.
|
||||
// original runs, we can compute the delta and fire only for genuine
|
||||
// additions / removals.
|
||||
//
|
||||
// Login suppression: the same function handles the initial bulk-sync
|
||||
// at character entry, which appears as a single rebuild call adding
|
||||
@@ -30,7 +41,10 @@
|
||||
// added in a single rebuild (which can only happen on login / reload
|
||||
// resync — human input speed can't accept multiple quests in the
|
||||
// same engine tick). A brand-new character's first accept is
|
||||
// `0 → 1` and fires correctly.
|
||||
// `0 → 1` and fires correctly. Removals apply the same rule (a user
|
||||
// action removes at most one quest per rebuild) and are additionally
|
||||
// suppressed when the rebuild also added multiple quests — a
|
||||
// mixed-delta rebuild is a cross-character resync, not gameplay.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -39,13 +53,15 @@
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Quest::Accepted {
|
||||
namespace Quest::LogEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEventName = "QUEST_ACCEPTED";
|
||||
constexpr const char *kAcceptedEventName = "QUEST_ACCEPTED";
|
||||
constexpr const char *kRemovedEventName = "QUEST_REMOVED";
|
||||
|
||||
const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
const Event::Custom::AutoReserve _reserveAccepted{kAcceptedEventName};
|
||||
const Event::Custom::AutoReserve _reserveRemoved{kRemovedEventName};
|
||||
|
||||
// Walk the live quest log and collect questIDs of real (non-header)
|
||||
// entries. Headers are 16-byte rows with a non-NULL pointer at +0x8;
|
||||
@@ -80,7 +96,7 @@ QuestLogRebuild_t QuestLogRebuild_o = nullptr;
|
||||
void __fastcall QuestLogRebuild_h(int param_1) {
|
||||
// `param_1 == 0` is the engine's "no-op rebuild" signal — just
|
||||
// refires QUEST_LOG_UPDATE without touching the entry array.
|
||||
// No diff possible, no accept happening, pass through.
|
||||
// No diff possible, no accept/remove happening, pass through.
|
||||
if (param_1 == 0) {
|
||||
QuestLogRebuild_o(param_1);
|
||||
return;
|
||||
@@ -93,14 +109,17 @@ void __fastcall QuestLogRebuild_h(int param_1) {
|
||||
|
||||
// Walk the post-state in index order so the fired event's
|
||||
// `questLogIndex` matches the 1-based slot the engine just
|
||||
// assigned. Skip questIDs already in `pre`.
|
||||
// assigned. Collect the full post-set on the way for the
|
||||
// removal diff.
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRY_COUNT));
|
||||
if (count <= 0)
|
||||
return;
|
||||
const auto *entries = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_QUEST_LOG_ENTRIES));
|
||||
|
||||
std::unordered_set<int> post;
|
||||
if (count > 0)
|
||||
post.reserve(static_cast<size_t>(count));
|
||||
|
||||
int newCount = 0;
|
||||
int firstNewIdx = 0;
|
||||
int firstNewID = 0;
|
||||
@@ -114,6 +133,7 @@ void __fastcall QuestLogRebuild_h(int param_1) {
|
||||
entry + Offsets::OFF_QUEST_LOG_ENTRY_QUEST_ID);
|
||||
if (questID <= 0)
|
||||
continue;
|
||||
post.insert(questID);
|
||||
if (pre.count(questID) != 0)
|
||||
continue;
|
||||
++newCount;
|
||||
@@ -123,14 +143,35 @@ void __fastcall QuestLogRebuild_h(int param_1) {
|
||||
}
|
||||
}
|
||||
|
||||
int removedCount = 0;
|
||||
int firstRemovedID = 0;
|
||||
for (const int questID : pre) {
|
||||
if (post.count(questID) != 0)
|
||||
continue;
|
||||
++removedCount;
|
||||
if (removedCount == 1)
|
||||
firstRemovedID = questID;
|
||||
}
|
||||
|
||||
// Single addition → user-driven accept. Multiple → bulk resync
|
||||
// (login or post-reload). Modern WoW's QUEST_ACCEPTED doesn't
|
||||
// fire on initial sync either; we match that.
|
||||
if (newCount == 1) {
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
const int slot = Event::Custom::Lookup(kAcceptedEventName);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", firstNewIdx, firstNewID);
|
||||
}
|
||||
|
||||
// Single removal → user-driven turn-in or abandon. The extra
|
||||
// `newCount <= 1` gate rejects the one resync shape the removal
|
||||
// count alone can't: switching to a character whose log shares
|
||||
// all but one of the previous character's quests (1 removal, N
|
||||
// additions in the same rebuild).
|
||||
if (removedCount == 1 && newCount <= 1) {
|
||||
const int slot = Event::Custom::Lookup(kRemovedEventName);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d", firstRemovedID);
|
||||
}
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
@@ -138,4 +179,4 @@ static const Game::HookAutoRegister _hookreg{
|
||||
reinterpret_cast<void *>(&QuestLogRebuild_h),
|
||||
reinterpret_cast<void **>(&QuestLogRebuild_o)};
|
||||
|
||||
} // namespace Quest::Accepted
|
||||
} // namespace Quest::LogEvents
|
||||
Reference in New Issue
Block a user