entity_create_probe.cpp 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119
  1. #include "entity_create_probe.h"
  2. #include <Windows.h>
  3. #include <intrin.h>
  4. #include <array>
  5. #include <cstddef>
  6. #include <cstdint>
  7. #include <cstdio>
  8. #include <span>
  9. #include <string_view>
  10. #include "../../core/logging/log.h"
  11. #include "../hooking/detour.h"
  12. #include "../patterns/image_scan.h"
  13. #include "../patterns/signature_text.h"
  14. namespace sunrise::client::diagnostics {
  15. namespace {
  16. namespace patterns = client::patterns;
  17. namespace detour = client::hooking::detour;
  18. /**
  19. * The index allocator the entity creator calls first.
  20. * Recovered from the mapped-image dump. Its body is unmistakable: it stores -1 into the caller's
  21. * out-parameter, then asks a pool at `+0xC118` sized `0x2000` for a free index. The frame size is
  22. * wildcarded so the match carries no position-dependent byte.
  23. */
  24. constexpr std::string_view kIndexAllocatorText =
  25. "48 89 5C 24 08 48 89 74 24 10 57 48 83 EC ? 48 8B DA C7 02 FF FF FF FF 48 8B F9 "
  26. "BA 00 20 00 00";
  27. /** Compiled pattern bytes of the signature text above. */
  28. constexpr auto kIndexAllocator =
  29. patterns::signature<patterns::signature_length(kIndexAllocatorText)>(kIndexAllocatorText);
  30. /** The allocator answers this in its out-parameter when it has no index to give. */
  31. constexpr std::int32_t kNoIndex = -1;
  32. /**
  33. * Byte offset of the free-slot bitmap inside the manager the allocator is handed.
  34. * Read out of the allocator's body: it calls the bitmap search with `rcx = manager + 0xC118` and
  35. * a width of `0x2000`, then clears the bit it was given. A set bit is therefore a FREE slot, and
  36. * the search answers -1 only when every word is zero.
  37. */
  38. constexpr std::size_t kFreeBitmapOffset = 0xC118;
  39. /** Slots the bitmap covers, from the width the allocator passes. */
  40. constexpr std::size_t kFreeBitmapBits = 0x2000;
  41. /** Words in that bitmap. */
  42. constexpr std::size_t kFreeBitmapWords = kFreeBitmapBits / 32;
  43. /**
  44. * Counts the free slots the manager currently holds.
  45. * The exhaustion line alone cannot separate "the host never gave the client any slots" from
  46. * "the client used everything it was given", and those need opposite fixes.
  47. * @param pool Manager the allocator was handed.
  48. * @return Set bits in its free bitmap, or -1 when the bitmap cannot be read.
  49. */
  50. [[nodiscard]] std::int64_t free_slot_count(const void* pool) noexcept {
  51. if (pool == nullptr) {
  52. return -1;
  53. }
  54. std::int64_t free = 0;
  55. __try {
  56. const auto* words = reinterpret_cast<const std::uint32_t*>(
  57. static_cast<const std::byte*>(pool) + kFreeBitmapOffset);
  58. for (std::size_t word = 0; word < kFreeBitmapWords; ++word) {
  59. free += static_cast<std::int64_t>(__popcnt(words[word]));
  60. }
  61. } __except (EXCEPTION_EXECUTE_HANDLER) {
  62. return -1;
  63. }
  64. return free;
  65. }
  66. /** Outcomes reported per run, so a per-frame failure cannot fill the log. */
  67. constexpr LONG kMaxReports = 200;
  68. /**
  69. * Stack frames captured above this probe on each allocation.
  70. * The allocator itself is generic — one function serves every entity in the game — so its own
  71. * address says nothing about what is being built. The callers above it are what differ, and six
  72. * frames is enough to separate "the world is placing an object" from "a weapon spawned a
  73. * projectile" without unwinding the whole fiber stack.
  74. */
  75. constexpr ULONG kTraceFrames = 6;
  76. /**
  77. * Allocation traces per run.
  78. * A raid load builds a few hundred entities, so this holds several bubble loads while still
  79. * bounding what a long firefight can write.
  80. */
  81. constexpr LONG kMaxTraces = 4096;
  82. /** Traces already spent. */
  83. volatile LONG g_traces{};
  84. /**
  85. * Image offset of the pointer to the game's entity record table.
  86. * Recovered from the creation path itself, which indexes it as `base + (handle & 0x1FFF) * stride`
  87. * at `0x4D71F7`: `imul ebx, [rip -> 0x1F93430]` then `add rbx, [rip -> 0x1F93428]`. The mask is the
  88. * same 13 bits the allocator's bitmap covers, so a record addresses exactly one allocated index.
  89. */
  90. constexpr std::uintptr_t kEntityTableBaseRva = 0x1F93428;
  91. /** Image offset of the record stride that pairs with the table above. */
  92. constexpr std::uintptr_t kEntityTableStrideRva = 0x1F93430;
  93. /** Stride the dump reports. Checked at runtime, because a wrong one would read foreign memory. */
  94. constexpr std::uint32_t kExpectedRecordStride = 224;
  95. /** Bytes of each record dumped. The whole record, so the type field can be found by comparison. */
  96. constexpr std::size_t kRecordDumpBytes = kExpectedRecordStride;
  97. /** Records dumped per run, bounded so a long session cannot fill the sink. */
  98. constexpr LONG kMaxRecords = 512;
  99. /** Records already dumped. */
  100. volatile LONG g_records{};
  101. /**
  102. * Record class every live entity carries at `+0x64`.
  103. * Constant across all 57 records of a run, so it marks a slot the game has actually built rather
  104. * than one holding whatever the last entity left behind.
  105. */
  106. constexpr std::uint32_t kRecordClass = 0x80809783;
  107. /** Offset of the record class within a record. */
  108. constexpr std::size_t kRecordClassOffset = 0x64;
  109. /** Offset of the object's definition hash. Varies per object kind; `0xFFFFFFFF` where absent. */
  110. constexpr std::size_t kRecordDefinitionOffset = 0x88;
  111. /** Offset of the instance ordinal that counts copies of one definition. */
  112. constexpr std::size_t kRecordOrdinalOffset = 0x8C;
  113. /** Offset of the transform block, which is still unset when a record is first dumped. */
  114. constexpr std::size_t kRecordTransformOffset = 0xA0;
  115. /** Dwords of the transform block reported, covering the orientation and position quads. */
  116. constexpr std::size_t kRecordTransformDwords = 8;
  117. /** Seconds between censuses. Short enough to catch a bubble soon after it settles. */
  118. constexpr DWORD kCensusIntervalMs = 15'000;
  119. /**
  120. * Most recent manager the allocator was handed.
  121. * The census needs the free bitmap to tell a live record from one an entity left behind, and the
  122. * allocator is the only place the manager pointer is known.
  123. */
  124. void* volatile g_lastPool{};
  125. /** Entries one census reports, so a fully populated table cannot fill the sink. */
  126. constexpr LONG kCensusEntryBudget = 2'048;
  127. /**
  128. * Distinct record classes counted per census.
  129. * The census filtered on one class, `kRecordClass`, and so never reported an index above ~1019.
  130. * An interaction incident then named entity **3539** as its target while the player stood on the
  131. * Wall of Wishes activation plate -- an object that works -- and the twenty panels that do not
  132. * work sit at 749..768. Whatever separates them is not visible while the walk only ever admits
  133. * one class, so every class is counted and sampled now.
  134. */
  135. constexpr std::size_t kClassCapacity = 24;
  136. /**
  137. * Records dumped per distinct class, so a large class cannot crowd out a small one.
  138. * Set at 48 this hid the very thing it was built to find: one class holds every real record, so
  139. * only indices 0..47 were ever dumped and the Wall of Wishes panels at 749..768 fell outside the
  140. * log entirely. That absence then read as "the player never reached the wall", which was wrong.
  141. * The share only needs to stop one class starving another, so it sits at the whole budget.
  142. */
  143. constexpr LONG kPerClassDump = 2'048;
  144. /** Cleared to stop the census thread. */
  145. volatile LONG g_censusRunning{};
  146. /** Census thread handle. */
  147. HANDLE g_censusThread{};
  148. /**
  149. * Index whose record has not been dumped yet.
  150. * The record is empty when the allocator hands the index out — the creator fills it afterwards — so
  151. * each index is read one allocation late, when whatever built it has finished.
  152. */
  153. volatile LONG g_pendingIndex{-1};
  154. /**
  155. * Dumps one entity record so the entity can be named rather than counted.
  156. * Counting proved the pool works and says nothing about what is in it. The record is the only place
  157. * the client keeps an entity's identity, and every entity in the run shares one creation path, so
  158. * the bytes here are what separate a wall panel from a projectile.
  159. * @param index Index whose record to read.
  160. */
  161. void report_record(std::int32_t index) noexcept {
  162. if (index < 0 || static_cast<std::size_t>(index) >= kFreeBitmapBits
  163. || !core::log::accepts(core::log::Channel::client, core::log::Level::debug)
  164. || InterlockedIncrement(&g_records) > kMaxRecords) {
  165. return;
  166. }
  167. const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
  168. if (base == 0) {
  169. return;
  170. }
  171. std::array<char, core::log::kLineCapacity> line{};
  172. int written = 0;
  173. __try {
  174. const auto table = *reinterpret_cast<const std::byte* const*>(base + kEntityTableBaseRva);
  175. const auto stride = *reinterpret_cast<const std::uint32_t*>(base + kEntityTableStrideRva);
  176. // A stride that has moved means this offset no longer names the table, and reading through
  177. // it would dump unrelated memory as if it were an entity.
  178. if (table == nullptr || stride != kExpectedRecordStride) {
  179. return;
  180. }
  181. const auto* const record = table + static_cast<std::size_t>(index) * stride;
  182. written = std::snprintf(line.data(),
  183. line.size(),
  184. "ev=entity_create stage=record idx=%d hex=",
  185. static_cast<int>(index));
  186. for (std::size_t offset = 0; offset < kRecordDumpBytes && written > 0
  187. && static_cast<std::size_t>(written) + 3 < line.size();
  188. ++offset) {
  189. const int more = std::snprintf(line.data() + written,
  190. line.size() - static_cast<std::size_t>(written),
  191. "%02X",
  192. std::to_integer<unsigned char>(record[offset]));
  193. if (more <= 0) {
  194. break;
  195. }
  196. written += more;
  197. }
  198. } __except (EXCEPTION_EXECUTE_HANDLER) {
  199. return;
  200. }
  201. if (written <= 0) {
  202. return;
  203. }
  204. const auto length = static_cast<std::size_t>(written) < line.size()
  205. ? static_cast<std::size_t>(written)
  206. : line.size() - 1;
  207. core::log::write(core::log::Channel::client, core::log::Level::debug, {line.data(), length});
  208. }
  209. /** Resolved `RtlCaptureStackBackTrace`, or null when ntdll would not give it up. */
  210. USHORT(NTAPI* g_captureBacktrace)(ULONG, ULONG, PVOID*, PULONG){};
  211. /** Allocations between pool samples. Frequent enough to shape the drain, rare enough to be free. */
  212. constexpr LONG kSampleInterval = 16;
  213. /** Bytes in the bitmap, from the width the client's own stocking path passes to its fill. */
  214. constexpr std::size_t kFreeBitmapBytes = kFreeBitmapBits / 8;
  215. /**
  216. * High slots the host keeps for its own entities and never leases to the client.
  217. * The join grant is `kSlotCount - kDefaultServerReserve` = 7936, so the top 256 indices are the
  218. * host's. The client's own initialiser frees the whole bitmap because in its intended world it
  219. * owns every slot; here it does not, and handing it the reserve would let it allocate an index
  220. * the host also considers its own.
  221. */
  222. constexpr std::size_t kServerReserveSlots = 256;
  223. /** Bytes of the bitmap that stay clear, covering the reserve at the top of the index space. */
  224. constexpr std::size_t kReserveBytes = kServerReserveSlots / 8;
  225. /** Bytes of the bitmap that are freed to the client. */
  226. constexpr std::size_t kClientBytes = kFreeBitmapBytes - kReserveBytes;
  227. /** Words of the bitmap covering the client's half. The split lands on a word boundary. */
  228. constexpr std::size_t kClientWords = kClientBytes / sizeof(std::uint32_t);
  229. static_assert(kClientBytes % sizeof(std::uint32_t) == 0,
  230. "the client half must end on a word so a refill never touches the reserve");
  231. /** Bits per bitmap word. */
  232. constexpr std::size_t kBitsPerWord = 32;
  233. /**
  234. * Address span treated as belonging to the game's image.
  235. * The dump reports an image size of 0x8A5EA00, so this clears it with room for a larger build while
  236. * still rejecting a frame that landed in Sunrise's own module or on a foreign allocation.
  237. */
  238. constexpr std::uintptr_t kImageSpan = 0x10000000;
  239. /**
  240. * The allocator's real shape, read from its body rather than guessed.
  241. * It uses exactly two arguments: `rcx` is the manager whose free-slot bitmap sits at `+0xC118`,
  242. * and `rdx` is the out-parameter it fills with the allocated index. It returns `rdx` unchanged.
  243. */
  244. using IndexAllocator = void*(__fastcall*)(void*, std::int32_t*) noexcept;
  245. detour::Handle g_allocator{};
  246. volatile LONG g_reports{};
  247. /** Successful allocations seen, used only to space the samples. */
  248. volatile LONG g_allocations{};
  249. /**
  250. * One manager's record of the indices this probe has watched the allocator hand out.
  251. *
  252. * A blanket `memset(bitmap, 0xFF, ...)` is what made the very first stocking work and what made
  253. * every later one lethal. It frees index 0 upward, and by the time a pool has drained, index 0
  254. * belongs to a live entity. The allocator picks the lowest set bit, so the next creation lands on
  255. * top of a live entity and the world stops being a consistent list of them. That is the crash on
  256. * respawn, the crash on Worldline Zero's ability, and the mainloop stall that ends a Shuro Chi run
  257. * a few seconds after the room loads.
  258. *
  259. * Keeping the set of indices already handed out turns the refill from "free everything" into
  260. * "free what was never taken", which is the only form of it that is safe to run on a live pool.
  261. */
  262. struct PoolRecord {
  263. /** Manager this record belongs to, or null while the slot is unused. */
  264. void* pool;
  265. /** Set bit per index the allocator gave out and the client has not since handed back. */
  266. std::array<volatile LONG, kFreeBitmapWords> live;
  267. /** Whether this pool has been refilled at least once. */
  268. volatile LONG stocked;
  269. };
  270. /** Managers tracked at once. A world change builds a new one, so several are live per run. */
  271. constexpr std::size_t kTrackedPoolCapacity = 16;
  272. /** Per-manager occupancy records, claimed on first sight. */
  273. std::array<PoolRecord, kTrackedPoolCapacity> g_pools{};
  274. /**
  275. * Finds the record for one manager, claiming a free slot on first sight.
  276. * @param pool Manager the allocator was handed.
  277. * @return Its record, or null when the table is full.
  278. */
  279. [[nodiscard]] PoolRecord* find_pool(void* pool) noexcept {
  280. for (auto& record : g_pools) {
  281. if (record.pool == pool) {
  282. return &record;
  283. }
  284. }
  285. for (auto& record : g_pools) {
  286. auto* const slot = reinterpret_cast<void* volatile*>(&record.pool);
  287. if (InterlockedCompareExchangePointer(slot, pool, nullptr) == nullptr
  288. || record.pool == pool) {
  289. return &record;
  290. }
  291. }
  292. // Past capacity nothing is tracked, so nothing is refilled either. A missed refill costs this
  293. // world's entities; an untracked one corrupts a live pool.
  294. return nullptr;
  295. }
  296. /**
  297. * Records that one index is now owned by an entity.
  298. * @param record Manager record, or null when the manager is untracked.
  299. * @param index Index the allocator produced.
  300. */
  301. void mark_live(PoolRecord* record, std::int32_t index) noexcept {
  302. if (record == nullptr || index < 0 || static_cast<std::size_t>(index) >= kFreeBitmapBits) {
  303. return;
  304. }
  305. const auto slot = static_cast<std::size_t>(index);
  306. (void)InterlockedOr(&record->live[slot / kBitsPerWord],
  307. static_cast<LONG>(1u << (slot % kBitsPerWord)));
  308. }
  309. /** Off leaves the probe reporting only, which is what it did before it could write. */
  310. bool g_stockUnstockedPool{};
  311. /** Refill a drained pool as well as an unstocked one. Safe now that the refill spares live slots. */
  312. bool g_restockAlways{};
  313. /**
  314. * Reports one probe outcome, up to the per-run budget.
  315. * @param stage Which half answered.
  316. * @param outcome What it answered.
  317. * @param detail Free slots left in the pool, or -1 when the bitmap could not be read.
  318. */
  319. void report_pair(const char* stage,
  320. const char* outcome,
  321. std::int64_t detail,
  322. std::int64_t allocations) noexcept {
  323. if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
  324. || InterlockedIncrement(&g_reports) > kMaxReports) {
  325. return;
  326. }
  327. std::array<char, core::log::kLineCapacity> line{};
  328. const int written = std::snprintf(line.data(),
  329. line.size(),
  330. "ev=entity_create stage=%s result=%s free=%lld allocs=%lld",
  331. stage,
  332. outcome,
  333. static_cast<long long>(detail),
  334. static_cast<long long>(allocations));
  335. if (written > 0) {
  336. core::log::write(core::log::Channel::client,
  337. core::log::Level::debug,
  338. {line.data(), static_cast<std::size_t>(written)});
  339. }
  340. }
  341. /**
  342. * Reports one refill, naming how many slots it actually handed back.
  343. * @param outcome Whether the pool answered after the refill.
  344. * @param free Free slots the bitmap holds now.
  345. * @param freed Slots this refill put back.
  346. * @param allocations Successful allocations seen so far.
  347. */
  348. void report_stock(const char* outcome,
  349. std::int64_t free,
  350. std::int64_t freed,
  351. std::int64_t allocations) noexcept {
  352. if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
  353. || InterlockedIncrement(&g_reports) > kMaxReports) {
  354. return;
  355. }
  356. std::array<char, core::log::kLineCapacity> line{};
  357. const int written =
  358. std::snprintf(line.data(),
  359. line.size(),
  360. "ev=entity_create stage=allocate result=%s free=%lld freed=%lld allocs=%lld",
  361. outcome,
  362. static_cast<long long>(free),
  363. static_cast<long long>(freed),
  364. static_cast<long long>(allocations));
  365. if (written > 0) {
  366. core::log::write(core::log::Channel::client,
  367. core::log::Level::debug,
  368. {line.data(), static_cast<std::size_t>(written)});
  369. }
  370. }
  371. /**
  372. * Names one allocation and the call sites that asked for it.
  373. *
  374. * Counting allocations proved the pool works; it cannot say what is being built, and that is the
  375. * question a missing Wall of Wishes actually poses. Its panels are one repeated object, so a burst
  376. * of identical traces landing on consecutive indices as a bubble loads is the wall being created,
  377. * and the absence of such a burst is the wall never being asked for. Those two need opposite fixes.
  378. *
  379. * Addresses are image-relative because the game is rebased every run; an RVA maps straight into the
  380. * mapped-image dump, where file offset equals RVA.
  381. * @param pool Manager the index came from, so per-type managers would show as distinct pointers.
  382. * @param index Index the allocator produced.
  383. * @param sequence Allocation ordinal within the run.
  384. */
  385. void report_allocation(const void* pool, std::int32_t index, LONG sequence) noexcept {
  386. if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
  387. || InterlockedIncrement(&g_traces) > kMaxTraces) {
  388. return;
  389. }
  390. const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
  391. std::array<char, core::log::kLineCapacity> line{};
  392. int written = std::snprintf(line.data(),
  393. line.size(),
  394. "ev=entity_create stage=alloc n=%ld idx=%d pool=0x%llX sites=",
  395. static_cast<long>(sequence),
  396. static_cast<int>(index),
  397. static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(pool)));
  398. if (written <= 0) {
  399. return;
  400. }
  401. std::array<void*, kTraceFrames> frames{};
  402. // Frame 0 is this probe, which is never interesting, so the capture starts one above it.
  403. const USHORT captured = g_captureBacktrace == nullptr
  404. ? 0
  405. : g_captureBacktrace(1, kTraceFrames, frames.data(), nullptr);
  406. for (USHORT frame = 0; frame < captured && written > 0
  407. && static_cast<std::size_t>(written) < line.size();
  408. ++frame) {
  409. const auto site = reinterpret_cast<std::uintptr_t>(frames[frame]);
  410. // A frame inside Sunrise's own module is noise here; only the game's code is addressable
  411. // in the dump, so anything outside it is printed as a gap rather than a misleading offset.
  412. const bool inImage = base != 0 && site >= base && (site - base) < kImageSpan;
  413. const int more =
  414. std::snprintf(line.data() + written,
  415. line.size() - static_cast<std::size_t>(written),
  416. inImage ? "%s0x%llX" : "%s-",
  417. frame == 0 ? "" : ",",
  418. static_cast<unsigned long long>(inImage ? site - base : 0));
  419. if (more <= 0) {
  420. break;
  421. }
  422. written += more;
  423. }
  424. const auto length = static_cast<std::size_t>(written) < line.size()
  425. ? static_cast<std::size_t>(written)
  426. : line.size() - 1;
  427. core::log::write(core::log::Channel::client, core::log::Level::debug, {line.data(), length});
  428. }
  429. void report(const char* stage, const char* outcome, std::int64_t detail) noexcept {
  430. if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
  431. || InterlockedIncrement(&g_reports) > kMaxReports) {
  432. return;
  433. }
  434. std::array<char, core::log::kLineCapacity> line{};
  435. const int written = std::snprintf(line.data(),
  436. line.size(),
  437. "ev=entity_create stage=%s result=%s free=%lld",
  438. stage,
  439. outcome,
  440. static_cast<long long>(detail));
  441. if (written > 0) {
  442. core::log::write(core::log::Channel::client,
  443. core::log::Level::debug,
  444. {line.data(), static_cast<std::size_t>(written)});
  445. }
  446. }
  447. /**
  448. * Mirrors the index allocator and reports whether it produced an index.
  449. * The out-parameter is the answer: the original writes -1 into it before doing anything, and
  450. * overwrites it only on success.
  451. */
  452. /**
  453. * Reads the game's entity record table, or reports that it cannot be addressed.
  454. * @param table Receives the table base.
  455. * @param stride Receives the record stride.
  456. * @return True when both were read and the stride still matches this build.
  457. */
  458. [[nodiscard]] bool entity_table(const std::byte*& table, std::uint32_t& stride) noexcept {
  459. const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
  460. if (base == 0) {
  461. return false;
  462. }
  463. __try {
  464. table = *reinterpret_cast<const std::byte* const*>(base + kEntityTableBaseRva);
  465. stride = *reinterpret_cast<const std::uint32_t*>(base + kEntityTableStrideRva);
  466. } __except (EXCEPTION_EXECUTE_HANDLER) {
  467. return false;
  468. }
  469. return table != nullptr && stride == kExpectedRecordStride;
  470. }
  471. /**
  472. * Reports which of one word's 32 indices already hold an entity.
  473. *
  474. * The probe's own record of handed-out indices covers only what came through the hooked allocator,
  475. * and a census measured that as 58 of 830 — the world's placed objects reach the table by some
  476. * other path entirely. Trusting that record alone therefore freed 7936 slots while 42 entities
  477. * were sitting in them, and the client then allocated straight over the top. The game's own record
  478. * table is the authority on which slots are taken, so occupancy is read from there instead.
  479. * @param table Entity record table base.
  480. * @param stride Record stride.
  481. * @param word Word of the free bitmap being refilled.
  482. * @return Set bit per index in that word whose record is live.
  483. */
  484. [[nodiscard]] LONG occupied_mask(const std::byte* table, std::uint32_t stride, std::size_t word) noexcept {
  485. std::uint32_t mask = 0;
  486. for (std::size_t bit = 0; bit < kBitsPerWord; ++bit) {
  487. const std::size_t index = word * kBitsPerWord + bit;
  488. __try {
  489. if (*reinterpret_cast<const std::uint32_t*>(table + index * stride
  490. + kRecordClassOffset)
  491. == kRecordClass) {
  492. mask |= 1u << bit;
  493. }
  494. } __except (EXCEPTION_EXECUTE_HANDLER) {
  495. // An unreadable record is treated as taken, which costs a slot rather than an entity.
  496. mask |= 1u << bit;
  497. }
  498. }
  499. return static_cast<LONG>(mask);
  500. }
  501. /**
  502. * Frees every client slot that no entity holds, leaving the ones that do alone.
  503. *
  504. * The client's own initialiser at `0x7FF71DDADB20` fills this bitmap with `0xFF` — every slot free
  505. * — but only when a role global reads zero; here it reads 3, so the fill never runs and the bitmap
  506. * is all-zero from the first frame. Every entity creation then fails, which is why no enemy, plate,
  507. * door or banner ever appeared and why an encounter bubble kicked to orbit. Writing those bytes
  508. * ourselves is right exactly once, on a pool that is still empty. On a pool that has drained it is
  509. * catastrophic, because the slots the client is using read as clear too and become free again.
  510. *
  511. * So the refill is driven by `record->live` instead of by a constant. A slot is freed only when the
  512. * bitmap says it is taken AND this probe never watched the allocator hand it out. Two passes,
  513. * because another thread may claim a slot while the first one runs: the second re-clears anything
  514. * that became live in between, so no index is ever offered twice.
  515. * @param pool Manager the allocator was handed.
  516. * @param record Occupancy record for that manager.
  517. * @param failure Receives a Windows error, or 1 for a null pool and 2 for a faulting write.
  518. * @return Slots freed, or -1 when the bitmap could not be written.
  519. */
  520. [[nodiscard]] std::int64_t stock_pool(void* pool,
  521. PoolRecord* record,
  522. std::uint32_t& failure) noexcept {
  523. failure = 0;
  524. if (pool == nullptr || record == nullptr) {
  525. failure = 1;
  526. return -1;
  527. }
  528. auto* const bitmap = static_cast<std::byte*>(pool) + kFreeBitmapOffset;
  529. // The bitmap sits in the game's own allocation, so it carries whatever protection that
  530. // allocation was given. Reading it worked, which does not prove it is writable.
  531. DWORD previous = 0;
  532. if (VirtualProtect(bitmap, kFreeBitmapBytes, PAGE_READWRITE, &previous) == FALSE) {
  533. failure = GetLastError();
  534. return -1;
  535. }
  536. const std::byte* table = nullptr;
  537. std::uint32_t stride = 0;
  538. const bool hasTable = entity_table(table, stride);
  539. std::int64_t freed = 0;
  540. __try {
  541. auto* const words = reinterpret_cast<volatile LONG*>(bitmap);
  542. for (std::size_t word = 0; word < kClientWords; ++word) {
  543. const LONG available = words[word];
  544. // A slot the client has put back is no longer live, so it returns to the pool with the
  545. // rest. Without this the record would only ever grow and the refill would fade to a
  546. // no-op over a long session.
  547. const LONG live = InterlockedAnd(&record->live[word], ~available) & ~available;
  548. // The record table is the authority; the probe's own list is kept as a second opinion
  549. // for anything created in the window before its record is filled in.
  550. const LONG occupied = hasTable ? occupied_mask(table, stride, word) : 0;
  551. const LONG missing =
  552. static_cast<LONG>(~static_cast<std::uint32_t>(live | available | occupied));
  553. if (missing != 0) {
  554. (void)InterlockedOr(&words[word], missing);
  555. freed += __popcnt(static_cast<unsigned int>(missing));
  556. }
  557. }
  558. for (std::size_t word = 0; word < kClientWords; ++word) {
  559. const LONG live = record->live[word];
  560. if (live != 0) {
  561. (void)InterlockedAnd(&words[word], ~live);
  562. }
  563. }
  564. // The host's reserve at the top of the space stays clear so the client cannot allocate an
  565. // index the host also considers its own.
  566. for (std::size_t word = kClientWords; word < kFreeBitmapWords; ++word) {
  567. (void)InterlockedAnd(&words[word], 0);
  568. }
  569. } __except (EXCEPTION_EXECUTE_HANDLER) {
  570. failure = 2;
  571. freed = -1;
  572. }
  573. DWORD restored = 0;
  574. (void)VirtualProtect(bitmap, kFreeBitmapBytes, previous, &restored);
  575. if (freed >= 0) {
  576. (void)InterlockedExchange(&record->stocked, 1);
  577. if (!hasTable) {
  578. // Worth saying out loud: without the table the refill is back to trusting a list that
  579. // has been measured as 7% complete, which is how live entities got overwritten.
  580. report("allocate", "stock_without_table", freed);
  581. }
  582. }
  583. return freed;
  584. }
  585. void* __fastcall allocator_body(void* pool, std::int32_t* index) noexcept {
  586. const auto call = reinterpret_cast<IndexAllocator>(g_allocator.original);
  587. if (call == nullptr) {
  588. return nullptr;
  589. }
  590. void* result = call(pool, index);
  591. InterlockedExchangePointer(&g_lastPool, pool);
  592. PoolRecord* const record = find_pool(pool);
  593. if (index == nullptr || *index != kNoIndex) {
  594. // Every index the client takes is recorded before anything else can act on it, because a
  595. // refill that does not know about it would offer the same index to a second entity.
  596. if (index != nullptr) {
  597. mark_live(record, *index);
  598. }
  599. // Sample the pool as it is spent. A steadily falling count means indices are allocated and
  600. // never returned; a count that rises again means the client's own free path does work and
  601. // the drain is simply the world being large. Those need opposite fixes, and the exhaustion
  602. // line alone cannot tell them apart because it only ever fires at zero.
  603. const LONG seen = InterlockedIncrement(&g_allocations);
  604. report_allocation(pool, index == nullptr ? kNoIndex : *index, seen);
  605. // One allocation behind, so the creator has had time to fill the record being read.
  606. report_record(InterlockedExchange(&g_pendingIndex, index == nullptr ? -1 : *index));
  607. if ((seen % kSampleInterval) == 0) {
  608. // The count is reported beside the free total: if the pool empties while this barely
  609. // moves, the bitmap is being cleared by something other than allocation.
  610. report_pair("allocate", "sample", free_slot_count(pool), seen);
  611. }
  612. return result;
  613. }
  614. const std::int64_t free = free_slot_count(pool);
  615. // A pool is refilled the first time it is seen empty, and again on every later drain when the
  616. // knob is on. Both are safe now: the refill spares the indices already handed out, so it can
  617. // no longer hand one index to two entities the way the old blanket fill did.
  618. const bool allowed = g_stockUnstockedPool && record != nullptr
  619. && (g_restockAlways || record->stocked == 0);
  620. if (free != 0 || !allowed) {
  621. report_pair("allocate", "exhausted", free, g_allocations);
  622. return result;
  623. }
  624. std::uint32_t failure = 0;
  625. const std::int64_t freed = stock_pool(pool, record, failure);
  626. if (freed < 0) {
  627. // Naming the reason matters: a refused write and a faulting page need different fixes.
  628. report("allocate", "stock_failed", static_cast<std::int64_t>(failure));
  629. return result;
  630. }
  631. result = call(pool, index);
  632. // `freed` is the number that matters. It should fall well short of the whole client half: the
  633. // gap is the live entities the old fill used to trample.
  634. report_stock(*index == kNoIndex ? "stocked_still_empty" : "stocked",
  635. free_slot_count(pool),
  636. freed,
  637. g_allocations);
  638. if (index != nullptr) {
  639. mark_live(record, *index);
  640. }
  641. return result;
  642. }
  643. /**
  644. * Image offset of the pointer that reaches the game's entity pool descriptors.
  645. * From the creation path at `0x4D71B5`: `mov rcx, [rip -> 0x2439C70]` then `add rdx, [rcx]` with
  646. * the pool ordinal already shifted left by six, so descriptors are 64 bytes apart and their array
  647. * base is one further dereference in. Within a descriptor, `+0x08` is the pool base and `+0x30`
  648. * its element size -- `imul eax, [rdx + 0x30]` then `add rcx, [rdx + 8]`.
  649. */
  650. constexpr std::uintptr_t kPoolDirectoryRva = 0x2439C70;
  651. /** Bytes between pool descriptors. */
  652. constexpr std::size_t kPoolDescriptorStride = 64;
  653. /** Descriptors probed. The ordinal comes from a handle's high bits, which are six wide. */
  654. constexpr std::size_t kPoolDescriptorCount = 64;
  655. /** Offset of a pool's base pointer within its descriptor. */
  656. constexpr std::size_t kPoolBaseOffset = 0x08;
  657. /** Offset of a pool's element size within its descriptor. */
  658. constexpr std::size_t kPoolElementSizeOffset = 0x30;
  659. /** An element size outside this is not a record, so the descriptor is not one either. */
  660. constexpr std::uint32_t kMaximumElementSize = 4096;
  661. /**
  662. * Pools whose elements match the entity record stride, walked by the census.
  663. * The directory holds TWO 224-byte pools, ordinals 33 and 35, at stable and distinct bases. The
  664. * census has only ever read whichever one `kEntityTableBaseRva` points at, so half the records of
  665. * this shape were never looked at -- and the activation plate that works, entity 3539, is not in
  666. * the half that was.
  667. */
  668. constexpr std::size_t kRecordPoolCapacity = 4;
  669. /** Bases of the record-shaped pools found in the directory. */
  670. std::array<const std::byte*, kRecordPoolCapacity> g_recordPools{};
  671. /** Ordinals of those pools, in the same order. */
  672. std::array<std::size_t, kRecordPoolCapacity> g_recordPoolOrdinals{};
  673. /** Record-shaped pools found. */
  674. std::size_t g_recordPoolCount{};
  675. /**
  676. * Reports every entity pool the game keeps, not just the one the census walks.
  677. *
  678. * The class tally proved the 224-byte table holds exactly one class and 830 records, and that
  679. * everything read above them is out-of-bounds noise. So the Wall of Wishes activation plate, which
  680. * an interaction incident named as entity 3539 and which visibly works, cannot be in that table at
  681. * all -- while the twenty panels that do not work are. Handles carry a pool ordinal in their high
  682. * bits, which is why one table was never the whole picture.
  683. */
  684. void report_pools() noexcept {
  685. if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)) {
  686. return;
  687. }
  688. const auto image = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
  689. if (image == 0) {
  690. return;
  691. }
  692. for (std::size_t ordinal = 0; ordinal < kPoolDescriptorCount; ++ordinal) {
  693. const std::byte* poolBase = nullptr;
  694. std::uint32_t elementSize = 0;
  695. __try {
  696. const auto* const directory =
  697. *reinterpret_cast<const std::byte* const*>(image + kPoolDirectoryRva);
  698. if (directory == nullptr) {
  699. return;
  700. }
  701. const auto* const descriptors = *reinterpret_cast<const std::byte* const*>(directory);
  702. if (descriptors == nullptr) {
  703. return;
  704. }
  705. const auto* const descriptor = descriptors + ordinal * kPoolDescriptorStride;
  706. poolBase = *reinterpret_cast<const std::byte* const*>(descriptor + kPoolBaseOffset);
  707. elementSize =
  708. *reinterpret_cast<const std::uint32_t*>(descriptor + kPoolElementSizeOffset);
  709. } __except (EXCEPTION_EXECUTE_HANDLER) {
  710. continue;
  711. }
  712. if (poolBase == nullptr || elementSize == 0 || elementSize > kMaximumElementSize) {
  713. continue;
  714. }
  715. if (elementSize == kExpectedRecordStride && g_recordPoolCount < kRecordPoolCapacity) {
  716. g_recordPoolOrdinals[g_recordPoolCount] = ordinal;
  717. g_recordPools[g_recordPoolCount++] = poolBase;
  718. }
  719. std::array<char, core::log::kLineCapacity> line{};
  720. const int written =
  721. std::snprintf(line.data(),
  722. line.size(),
  723. "ev=entity_census stage=pool ordinal=%zu base=0x%llX element=%u",
  724. ordinal,
  725. static_cast<unsigned long long>(
  726. reinterpret_cast<std::uintptr_t>(poolBase)),
  727. elementSize);
  728. if (written > 0) {
  729. core::log::write(core::log::Channel::client,
  730. core::log::Level::debug,
  731. {line.data(), static_cast<std::size_t>(written)});
  732. }
  733. }
  734. }
  735. /**
  736. * Reports the built records of one record-shaped pool other than the cached one.
  737. *
  738. * The cached pointer at `kEntityTableBaseRva` names a single pool, and the directory shows two of
  739. * this shape. An object that works and an object that does not may simply live in different pools,
  740. * and that is not visible while only one is read.
  741. * @param poolBase Base of the pool to walk.
  742. * @param stride Record stride, the same for every pool of this shape.
  743. * @param ordinal Directory ordinal, reported so the two can be told apart.
  744. */
  745. void walk_pool(const std::byte* poolBase, std::uint32_t stride, std::size_t ordinal) noexcept {
  746. LONG reported = 0;
  747. for (std::size_t index = 0; index < kFreeBitmapBits && reported < kCensusEntryBudget; ++index) {
  748. std::array<char, core::log::kLineCapacity> line{};
  749. int written = 0;
  750. __try {
  751. const auto* const record = poolBase + index * stride;
  752. const auto recordClass =
  753. *reinterpret_cast<const std::uint32_t*>(record + kRecordClassOffset);
  754. if (recordClass != kRecordClass) {
  755. continue;
  756. }
  757. written = std::snprintf(
  758. line.data(),
  759. line.size(),
  760. "ev=entity_census stage=entry pool=%zu idx=%zu cls=0x%08X def=0x%08X ord=%u rec=",
  761. ordinal,
  762. index,
  763. recordClass,
  764. *reinterpret_cast<const std::uint32_t*>(record + kRecordDefinitionOffset),
  765. *reinterpret_cast<const std::uint32_t*>(record + kRecordOrdinalOffset));
  766. for (std::size_t offset = 0; offset < kRecordDumpBytes && written > 0
  767. && static_cast<std::size_t>(written) + 3 < line.size();
  768. ++offset) {
  769. const int more = std::snprintf(line.data() + written,
  770. line.size() - static_cast<std::size_t>(written),
  771. "%02X",
  772. std::to_integer<unsigned char>(record[offset]));
  773. if (more <= 0) {
  774. break;
  775. }
  776. written += more;
  777. }
  778. } __except (EXCEPTION_EXECUTE_HANDLER) {
  779. continue;
  780. }
  781. if (written <= 0) {
  782. continue;
  783. }
  784. ++reported;
  785. core::log::write(core::log::Channel::client,
  786. core::log::Level::debug,
  787. {line.data(), static_cast<std::size_t>(written)});
  788. }
  789. std::array<char, core::log::kLineCapacity> tail{};
  790. const int written = std::snprintf(tail.data(),
  791. tail.size(),
  792. "ev=entity_census stage=pool_end ordinal=%zu records=%ld",
  793. ordinal,
  794. static_cast<long>(reported));
  795. if (written > 0) {
  796. core::log::write(core::log::Channel::client,
  797. core::log::Level::debug,
  798. {tail.data(), static_cast<std::size_t>(written)});
  799. }
  800. }
  801. /**
  802. * Walks the whole entity table and reports every slot the game has built.
  803. *
  804. * The per-allocation dump reads a record one allocation after it is handed out, which is early
  805. * enough that the transform is still its default — every instance of one definition reported the
  806. * same placement, which cannot be true. A census taken well after a bubble has settled reads the
  807. * finished records instead, and placement is the field that matters here: a grid of identical
  808. * co-planar objects is a wall of shootable panels and nothing else is, so this can identify the
  809. * Wall of Wishes without knowing the game's own name for it.
  810. */
  811. void run_census() noexcept {
  812. if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)) {
  813. return;
  814. }
  815. const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
  816. if (base == 0) {
  817. return;
  818. }
  819. const std::byte* table = nullptr;
  820. std::uint32_t stride = 0;
  821. __try {
  822. table = *reinterpret_cast<const std::byte* const*>(base + kEntityTableBaseRva);
  823. stride = *reinterpret_cast<const std::uint32_t*>(base + kEntityTableStrideRva);
  824. } __except (EXCEPTION_EXECUTE_HANDLER) {
  825. return;
  826. }
  827. if (table == nullptr || stride != kExpectedRecordStride) {
  828. return;
  829. }
  830. // A record keeps its class marker after the entity is gone, so the marker alone cannot
  831. // distinguish a live entity from a slot one left behind. The free bitmap can: a slot the
  832. // allocator would hand out is not holding anything, whatever its record still says.
  833. const auto* freeWords = static_cast<const std::uint32_t*>(nullptr);
  834. if (void* const pool = g_lastPool; pool != nullptr) {
  835. freeWords = reinterpret_cast<const std::uint32_t*>(static_cast<std::byte*>(pool)
  836. + kFreeBitmapOffset);
  837. }
  838. // First pass counts every class present. A record whose class word is zero or all ones has
  839. // never been built, so those are the only two values treated as empty.
  840. std::array<std::uint32_t, kClassCapacity> classes{};
  841. std::array<LONG, kClassCapacity> classCounts{};
  842. std::array<LONG, kClassCapacity> classDumped{};
  843. std::size_t classCount = 0;
  844. for (std::size_t index = 0; index < kFreeBitmapBits; ++index) {
  845. std::uint32_t value = 0;
  846. __try {
  847. value = *reinterpret_cast<const std::uint32_t*>(table + index * stride
  848. + kRecordClassOffset);
  849. } __except (EXCEPTION_EXECUTE_HANDLER) {
  850. continue;
  851. }
  852. if (value == 0 || value == 0xFFFFFFFFU) {
  853. continue;
  854. }
  855. std::size_t slot = 0;
  856. while (slot < classCount && classes[slot] != value) {
  857. ++slot;
  858. }
  859. if (slot == classCount) {
  860. if (classCount == kClassCapacity) {
  861. continue;
  862. }
  863. classes[classCount++] = value;
  864. }
  865. ++classCounts[slot];
  866. }
  867. for (std::size_t slot = 0; slot < classCount; ++slot) {
  868. std::array<char, core::log::kLineCapacity> head{};
  869. const int headWritten = std::snprintf(head.data(),
  870. head.size(),
  871. "ev=entity_census stage=class value=0x%08X count=%ld",
  872. classes[slot],
  873. static_cast<long>(classCounts[slot]));
  874. if (headWritten > 0) {
  875. core::log::write(core::log::Channel::client,
  876. core::log::Level::debug,
  877. {head.data(), static_cast<std::size_t>(headWritten)});
  878. }
  879. }
  880. g_recordPoolCount = 0;
  881. report_pools();
  882. // Name the pool the census has been reading all along, so its ordinal can be matched against
  883. // the directory rather than assumed.
  884. {
  885. std::array<char, core::log::kLineCapacity> line{};
  886. const int written = std::snprintf(
  887. line.data(),
  888. line.size(),
  889. "ev=entity_census stage=table base=0x%llX stride=%u pools=%zu",
  890. static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(table)),
  891. stride,
  892. g_recordPoolCount);
  893. if (written > 0) {
  894. core::log::write(core::log::Channel::client,
  895. core::log::Level::debug,
  896. {line.data(), static_cast<std::size_t>(written)});
  897. }
  898. }
  899. // Every record-shaped pool, not just the cached one. A pool the cached pointer already names
  900. // is not walked twice.
  901. for (std::size_t slot = 0; slot < g_recordPoolCount; ++slot) {
  902. if (g_recordPools[slot] == table) {
  903. continue;
  904. }
  905. walk_pool(g_recordPools[slot], stride, g_recordPoolOrdinals[slot]);
  906. }
  907. LONG live = 0;
  908. LONG stale = 0;
  909. // Per pass, not per run: a shared budget truncated the one census that mattered.
  910. LONG entries = 0;
  911. for (std::size_t index = 0; index < kFreeBitmapBits; ++index) {
  912. std::array<char, core::log::kLineCapacity> line{};
  913. int written = 0;
  914. __try {
  915. const auto* const record = table + index * stride;
  916. const auto recordClass =
  917. *reinterpret_cast<const std::uint32_t*>(record + kRecordClassOffset);
  918. if (recordClass == 0 || recordClass == 0xFFFFFFFFU) {
  919. continue;
  920. }
  921. std::size_t slot = 0;
  922. while (slot < classCount && classes[slot] != recordClass) {
  923. ++slot;
  924. }
  925. const bool spent = slot == classCount || classDumped[slot] >= kPerClassDump;
  926. if (!spent) {
  927. ++classDumped[slot];
  928. }
  929. unsigned slotFree = 0;
  930. if (freeWords != nullptr
  931. && (freeWords[index / kBitsPerWord] & (1u << (index % kBitsPerWord))) != 0) {
  932. slotFree = 1;
  933. ++stale;
  934. } else {
  935. ++live;
  936. }
  937. // The tally above counts every record; only the dump is rationed.
  938. if (spent || entries >= kCensusEntryBudget) {
  939. continue;
  940. }
  941. written = std::snprintf(
  942. line.data(),
  943. line.size(),
  944. "ev=entity_census stage=entry idx=%zu cls=0x%08X def=0x%08X ord=%u free=%u rec=",
  945. index,
  946. recordClass,
  947. *reinterpret_cast<const std::uint32_t*>(record + kRecordDefinitionOffset),
  948. *reinterpret_cast<const std::uint32_t*>(record + kRecordOrdinalOffset),
  949. slotFree);
  950. // The whole record, not just the transform block. The block at `+0xA0` decodes as a
  951. // clean quaternion but the four dwords after it are not the position — as floats they
  952. // are denormals and values in the trillions. Somewhere in these 224 bytes there are
  953. // three coordinates, and the way to find them is to scan every aligned offset across a
  954. // group for one that varies plausibly. A 5x5 grid of co-planar panels is the Wall of
  955. // Wishes and nothing else in the room is shaped like that, so placement identifies it
  956. // where counting has not.
  957. for (std::size_t offset = 0; offset < kRecordDumpBytes && written > 0
  958. && static_cast<std::size_t>(written) + 3 < line.size();
  959. ++offset) {
  960. const int more = std::snprintf(line.data() + written,
  961. line.size() - static_cast<std::size_t>(written),
  962. "%02X",
  963. std::to_integer<unsigned char>(record[offset]));
  964. if (more <= 0) {
  965. break;
  966. }
  967. written += more;
  968. }
  969. } __except (EXCEPTION_EXECUTE_HANDLER) {
  970. continue;
  971. }
  972. if (written <= 0) {
  973. continue;
  974. }
  975. ++entries;
  976. core::log::write(core::log::Channel::client,
  977. core::log::Level::debug,
  978. {line.data(), static_cast<std::size_t>(written)});
  979. }
  980. std::array<char, core::log::kLineCapacity> tail{};
  981. const int written = std::snprintf(tail.data(),
  982. tail.size(),
  983. "ev=entity_census stage=end live=%ld stale=%ld allocs=%ld",
  984. static_cast<long>(live),
  985. static_cast<long>(stale),
  986. static_cast<long>(g_allocations));
  987. if (written > 0) {
  988. core::log::write(core::log::Channel::client,
  989. core::log::Level::debug,
  990. {tail.data(), static_cast<std::size_t>(written)});
  991. }
  992. }
  993. /**
  994. * Runs a census on its own thread so it does not sit inside the game's allocation path.
  995. * @param unused Thread parameter, unused.
  996. * @return Always zero.
  997. */
  998. DWORD WINAPI census_thread(LPVOID unused) noexcept {
  999. (void)unused;
  1000. while (g_censusRunning != 0) {
  1001. Sleep(kCensusIntervalMs);
  1002. if (g_censusRunning == 0) {
  1003. break;
  1004. }
  1005. run_census();
  1006. }
  1007. return 0;
  1008. }
  1009. /**
  1010. * Attaches one probe, reporting its own outcome.
  1011. * @param signature Pattern to find.
  1012. * @param name Reported name.
  1013. * @param replacement Probe body.
  1014. * @param handle Receives the trampoline.
  1015. * @return True when the target was found and the detour attached.
  1016. */
  1017. [[nodiscard]] bool attach(std::span<const patterns::PatternByte> signature,
  1018. const char* name,
  1019. void* replacement,
  1020. detour::Handle& handle) noexcept {
  1021. std::byte* const target = patterns::scan_main_image_unique(signature, name);
  1022. std::array<char, core::log::kLineCapacity> line{};
  1023. if (target == nullptr) {
  1024. const int written = std::snprintf(line.data(),
  1025. line.size(),
  1026. "ev=entity_create stage=attach name=%s result=fail",
  1027. name);
  1028. if (written > 0) {
  1029. core::log::write(core::log::Channel::client,
  1030. core::log::Level::warn,
  1031. {line.data(), static_cast<std::size_t>(written)});
  1032. }
  1033. return false;
  1034. }
  1035. const detour::Spec spec{target, replacement};
  1036. const bool attached = detour::install(spec, handle);
  1037. const int written = std::snprintf(line.data(),
  1038. line.size(),
  1039. "ev=entity_create stage=attach name=%s result=%s",
  1040. name,
  1041. attached ? "ok" : "fail");
  1042. if (written > 0) {
  1043. core::log::write(core::log::Channel::client,
  1044. attached ? core::log::Level::info : core::log::Level::warn,
  1045. {line.data(), static_cast<std::size_t>(written)});
  1046. }
  1047. return attached;
  1048. }
  1049. } // namespace
  1050. /** Reports which half of the client's entity creation refuses. */
  1051. bool install_entity_create_probe(bool stockUnstockedPool, bool restockAlways) noexcept {
  1052. g_stockUnstockedPool = stockUnstockedPool;
  1053. g_restockAlways = restockAlways;
  1054. // Resolved rather than linked: the trace is a diagnostic, and a missing export should cost the
  1055. // call sites in the log, not the probe that stocks the pool.
  1056. if (HMODULE const ntdll = GetModuleHandleW(L"ntdll.dll"); ntdll != nullptr) {
  1057. g_captureBacktrace = reinterpret_cast<decltype(g_captureBacktrace)>(
  1058. reinterpret_cast<void*>(GetProcAddress(ntdll, "RtlCaptureStackBackTrace")));
  1059. }
  1060. const bool allocator = attach(kIndexAllocator,
  1061. "entity_index_allocator",
  1062. reinterpret_cast<void*>(&allocator_body),
  1063. g_allocator);
  1064. if (allocator) {
  1065. InterlockedExchange(&g_censusRunning, 1);
  1066. g_censusThread = CreateThread(nullptr, 0, &census_thread, nullptr, 0, nullptr);
  1067. }
  1068. // The initialiser is deliberately NOT hooked. Its fifth argument is passed on the stack
  1069. // (`mov dword [var_20h], eax` before the call), and a four-argument replacement got that
  1070. // wrong and black-screened the load. It does not need hooking anyway: the allocator alone
  1071. // answers the question, because the initialiser only runs when the allocator succeeded.
  1072. return allocator;
  1073. }
  1074. /** Detaches the entity-creation probes. */
  1075. void uninstall_entity_create_probe() noexcept {
  1076. InterlockedExchange(&g_censusRunning, 0);
  1077. if (g_censusThread != nullptr) {
  1078. // The census only reads, so a shutdown that beats it costs a census, never the process.
  1079. (void)CloseHandle(g_censusThread);
  1080. g_censusThread = nullptr;
  1081. }
  1082. if (g_allocator.attached) {
  1083. (void)detour::uninstall(g_allocator);
  1084. }
  1085. }
  1086. } // namespace sunrise::client::diagnostics