// 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 .
#include "cache/QueryLoad.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
#include
namespace Cache::QueryLoad {
namespace {
// A cache class's `_GetRecord`. callback=nullptr → pure peek (returns the
// loaded data block or nullptr, no query). Non-null callback → also fires
// the network query on a miss and stores `(callback, userData)` on the
// entry's pending list. dedup=1 so repeat fetches for the same id before
// the response lands don't queue the callback twice.
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
const uint64_t *guid, void *callback,
void *userData, char dedup);
// A cache class's response parser, which we hook (e.g.
// FUN_CREATURE_QUERY_RESPONSE / FUN_GAMEOBJECT_QUERY_RESPONSE).
using CacheResponse_t = void(__thiscall *)(void *cache, void *packetReader, int flag);
// Engine callback shape (__stdcall, ret 8) — see Item::Data. We only pass
// it to make `_GetRecord` send the query (the trigger is callback != 0);
// completion is observed via the response hook + tick sweep.
void __stdcall NoOpCallback(void * /*userData*/, int /*success*/) {}
const uint8_t *CacheFetch(uintptr_t getRecord, void *cache, uint32_t id, void *callback) {
auto fn = reinterpret_cast(getRecord);
const uint64_t zeroGuid = 0;
void *userData =
callback ? reinterpret_cast(static_cast(id)) : nullptr;
return fn(cache, id, &zeroGuid, callback, userData, 1);
}
struct Pending {
uint32_t id;
int ticksWaiting;
};
constexpr int kMaxCaches = 8;
constexpr int kMaxPending = 256;
constexpr int kMaxWaitTicks = 1200; // give up and fire failure after this many WorldTicks
struct Registered {
void *cache;
const char *eventName;
uintptr_t getRecord; // this cache class's _GetRecord
Pending pending[kMaxPending];
int pendingCount;
};
Registered g_caches[kMaxCaches];
int g_cacheCount = 0;
Registered *Find(void *cache) {
for (int i = 0; i < g_cacheCount; ++i) {
if (g_caches[i].cache == cache)
return &g_caches[i];
}
return nullptr;
}
void Fire(const Registered *r, uint32_t id, bool success) {
Event::Custom::FireIdSuccess(Event::Custom::Lookup(r->eventName),
static_cast(id), success);
}
int FindPending(const Registered *r, uint32_t id) {
for (int i = 0; i < r->pendingCount; ++i) {
if (r->pending[i].id == id)
return i;
}
return -1;
}
void RemovePendingAt(Registered *r, int idx) {
--r->pendingCount;
if (idx != r->pendingCount)
r->pending[idx] = r->pending[r->pendingCount];
}
bool IsLoaded(const Registered *r, uint32_t id) {
return CacheFetch(r->getRecord, r->cache, id, nullptr) != nullptr;
}
// Fire + remove any pending id whose entry is now loaded.
void Sweep(Registered *r) {
for (int i = 0; i < r->pendingCount;) {
if (IsLoaded(r, r->pending[i].id)) {
const uint32_t id = r->pending[i].id;
RemovePendingAt(r, i);
Fire(r, id, true);
continue;
}
++i;
}
}
// Shared post-response dispatch: a parser for `cache` just ran, so sweep
// that cache's pending set (no-op for caches we don't track).
void DispatchSweep(void *cache) {
Registered *r = Find(cache);
if (r != nullptr)
Sweep(r);
}
// One response-handler hook per cache class. Each calls its own original
// trampoline (the parser fills the entry + walks the engine's own
// callback list), then sweeps via the shared dispatch. `__thiscall`
// mimicked as `__fastcall(ecx, edx_unused, …)`.
using Trampoline_t = void(__fastcall *)(void *cache, void *edx, void *packetReader, int flag);
CacheResponse_t g_origCreatureParser = nullptr;
void __fastcall CreatureParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
reinterpret_cast(g_origCreatureParser)(cache, nullptr, packetReader, flag);
DispatchSweep(cache);
}
CacheResponse_t g_origGameObjectParser = nullptr;
void __fastcall GameObjectParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
reinterpret_cast(g_origGameObjectParser)(cache, nullptr, packetReader, flag);
DispatchSweep(cache);
}
const Game::HookAutoRegister _hookCreature{
Offsets::FUN_CREATURE_QUERY_RESPONSE, reinterpret_cast(&CreatureParser_h),
reinterpret_cast(&g_origCreatureParser)};
const Game::HookAutoRegister _hookGameObject{
Offsets::FUN_GAMEOBJECT_QUERY_RESPONSE, reinterpret_cast(&GameObjectParser_h),
reinterpret_cast(&g_origGameObjectParser)};
// Timeout + safety net. The hooks cover the happy path; this fires
// failure for ids that never resolve after `kMaxWaitTicks`, and
// re-checks loaded as a backstop for fills observed off the hook path.
void OnWorldTick() {
for (int c = 0; c < g_cacheCount; ++c) {
Registered *r = &g_caches[c];
for (int i = 0; i < r->pendingCount;) {
if (IsLoaded(r, r->pending[i].id)) {
const uint32_t id = r->pending[i].id;
RemovePendingAt(r, i);
Fire(r, id, true);
continue;
}
if (r->pending[i].ticksWaiting >= kMaxWaitTicks) {
const uint32_t id = r->pending[i].id;
RemovePendingAt(r, i);
Fire(r, id, false);
continue;
}
++r->pending[i].ticksWaiting;
++i;
}
}
}
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr) {
if (cacheInstance == nullptr || eventName == nullptr || getRecordAddr == 0)
return;
if (Find(cacheInstance) != nullptr) // idempotent (safe across /reload)
return;
if (g_cacheCount >= kMaxCaches)
return;
g_caches[g_cacheCount].cache = cacheInstance;
g_caches[g_cacheCount].eventName = eventName;
g_caches[g_cacheCount].getRecord = getRecordAddr;
g_caches[g_cacheCount].pendingCount = 0;
++g_cacheCount;
}
bool IsCached(void *cacheInstance, uint32_t id) {
const Registered *r = Find(cacheInstance);
return r != nullptr && IsLoaded(r, id);
}
bool RequestLoad(void *cacheInstance, uint32_t id) {
Registered *r = Find(cacheInstance);
if (r == nullptr || id == 0)
return false;
// Already cached — fire success synchronously (modern RequestLoad
// contract: the completion event fires even on a cache hit).
if (IsLoaded(r, id)) {
Fire(r, id, true);
return true;
}
// Already in flight — one completion event per id.
if (FindPending(r, id) >= 0)
return true;
if (r->pendingCount >= kMaxPending)
return false;
// Send the query (non-null callback is the trigger), then track until
// the response hook / tick fires the event.
CacheFetch(r->getRecord, cacheInstance, id, reinterpret_cast(&NoOpCallback));
r->pending[r->pendingCount].id = id;
r->pending[r->pendingCount].ticksWaiting = 0;
++r->pendingCount;
return true;
}
} // namespace Cache::QueryLoad