mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 15:10:39 +00:00
Every custom-event fire resolved its name to a slot via Event::Custom::Lookup - a linear strcmp scan over the ~46-entry reservation table, on every single fire. The slot itself was already cached per table build; only the name->reservation hop was repeated. On a client that is purely CPU-bound, that is wasted work on paths that burst exactly when frames are scarce (nameplate churn, cast spam, loot rolls, mouse/modifier input). AutoReserve now remembers its reservation index (the ctor's dedup resolves duplicate names to the same entry) and exposes Slot() - two loads. Reload safety is unchanged and stays centralized: Slot() reads through the shared table PrepareForReload/RetryClaims maintain, so the value can never go stale. Lookup is deleted; LookupByName remains for engine-defined names. All fire sites converted to the instance handle: - CastEvents: the k* name constants become the AutoReserve instances, so all 19 fire sites stay textually identical; the Fire/FireReticle/ FireRemote helpers take the reservation by reference. - equipmentset: reservations move into Set.h as C++17 inline consts so the Data and Api TUs share one handle. - Cache::QueryLoad::Register takes the AutoReserve pointer instead of an event-name string (creature/gameobject callers updated). - Tts keeps its lazy VanillaTTS-stand-down reservations via null-guarded pointers set at registration. - GlobalMouse/InputEvents/lossofcontrol instances moved above their fire sites (static-init list order is irrelevant).
225 lines
7.9 KiB
C++
225 lines
7.9 KiB
C++
// 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 "cache/QueryLoad.h"
|
|
|
|
#include "Game.h"
|
|
#include "Offsets.h"
|
|
#include "event/Custom.h"
|
|
#include "tick/WorldTick.h"
|
|
|
|
#include <cstdint>
|
|
|
|
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<GetCacheRecord_t>(getRecord);
|
|
const uint64_t zeroGuid = 0;
|
|
void *userData =
|
|
callback ? reinterpret_cast<void *>(static_cast<uintptr_t>(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 Event::Custom::AutoReserve *reserve; // completion event's reservation
|
|
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(r->reserve->Slot(), static_cast<int>(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<Trampoline_t>(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<Trampoline_t>(g_origGameObjectParser)(cache, nullptr, packetReader, flag);
|
|
DispatchSweep(cache);
|
|
}
|
|
|
|
const Game::HookAutoRegister _hookCreature{
|
|
Offsets::FUN_CREATURE_QUERY_RESPONSE, reinterpret_cast<void *>(&CreatureParser_h),
|
|
reinterpret_cast<void **>(&g_origCreatureParser)};
|
|
const Game::HookAutoRegister _hookGameObject{
|
|
Offsets::FUN_GAMEOBJECT_QUERY_RESPONSE, reinterpret_cast<void *>(&GameObjectParser_h),
|
|
reinterpret_cast<void **>(&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 Event::Custom::AutoReserve *reserve,
|
|
uintptr_t getRecordAddr) {
|
|
if (cacheInstance == nullptr || reserve == 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].reserve = reserve;
|
|
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<void *>(&NoOpCallback));
|
|
r->pending[r->pendingCount].id = id;
|
|
r->pending[r->pendingCount].ticksWaiting = 0;
|
|
++r->pendingCount;
|
|
return true;
|
|
}
|
|
|
|
} // namespace Cache::QueryLoad
|