stan 2 gün önce
ebeveyn
işleme
91cf4df2d0
45 değiştirilmiş dosya ile 235 ekleme ve 1520 silme
  1. 1 16
      Sunrise/Sunrise.vcxproj
  2. 0 3
      Sunrise/src/client/content/activity/activity_tag_reader.cpp
  3. 0 7
      Sunrise/src/client/content/investment/internal.h
  4. 0 236
      Sunrise/src/client/content/investment/investment_source.cpp
  5. 0 35
      Sunrise/src/client/content/investment/layout.h
  6. 0 120
      Sunrise/src/client/content/items/details/configured_item_detail_extractor.cpp
  7. 0 27
      Sunrise/src/client/content/items/details/configured_item_detail_extractor.h
  8. 0 100
      Sunrise/src/client/content/items/details/definition_detail_reader.cpp
  9. 0 34
      Sunrise/src/client/content/items/details/definition_detail_reader.h
  10. 0 112
      Sunrise/src/client/content/items/details/layout.h
  11. 0 126
      Sunrise/src/client/content/items/details/memory.h
  12. 0 39
      Sunrise/src/client/content/items/details/relative.h
  13. 0 131
      Sunrise/src/client/content/items/details/socket_detail_reader.cpp
  14. 0 38
      Sunrise/src/client/content/items/details/socket_detail_reader.h
  15. 0 179
      Sunrise/src/client/hooks/banner/banner_bind.cpp
  16. 0 75
      Sunrise/src/client/hooks/banner/banner_bind.h
  17. 0 32
      Sunrise/src/client/hooks/banner/banner_hook_lifecycle.cpp
  18. 0 17
      Sunrise/src/client/hooks/banner/banner_hook_lifecycle.h
  19. 1 4
      Sunrise/src/client/hooks/bitmap/bitmap_ref_guard.h
  20. 0 6
      Sunrise/src/client/hooks/graphics/graphics_hook_replacements.cpp
  21. 0 5
      Sunrise/src/client/hooks/graphics/graphics_hook_replacements.h
  22. 22 10
      Sunrise/src/client/hooks/inactivity/inactivity_override.cpp
  23. 0 1
      Sunrise/src/client/hooks/network/content_config/coordinator/content_config_call_coordinator.cpp
  24. 0 1
      Sunrise/src/client/hooks/network/content_config/coordinator/content_config_call_coordinator.h
  25. 0 2
      Sunrise/src/client/hooks/network/content_config/internal.h
  26. 1 3
      Sunrise/src/client/hooks/network/content_config/lifecycle/content_config_hook_install.cpp
  27. 0 1
      Sunrise/src/client/hooks/network/content_config/lifecycle/content_config_hook_lifecycle.cpp
  28. 0 2
      Sunrise/src/client/hooks/network/content_config/lifecycle/content_config_hook_storage.cpp
  29. 0 2
      Sunrise/src/client/hooks/network/coordinator/network_call_coordinator.cpp
  30. 0 2
      Sunrise/src/client/hooks/network/coordinator/network_call_coordinator.h
  31. 2 7
      Sunrise/src/client/hooks/queuez/queuez_hook_lifecycle.h
  32. 0 0
      Sunrise/src/client/hooks/queuez/queuez_null_payload_guard.cpp
  33. 0 8
      Sunrise/src/client/hooks/retail_log/retail_log_lifecycle.cpp
  34. 0 3
      Sunrise/src/client/hooks/retail_log/retail_log_lifecycle.h
  35. 12 2
      Sunrise/src/client/hooks/sense_chain_guard/sense_chain_guard.cpp
  36. 5 1
      Sunrise/src/client/hooks/stall_probe/stall_probe.cpp
  37. 13 6
      Sunrise/src/client/hooks/sword_skate/sword_skate.cpp
  38. 3 11
      Sunrise/src/client/hooks/teleport/internal.h
  39. 0 16
      Sunrise/src/client/hooks/teleport/runtime.h
  40. 10 1
      Sunrise/src/client/hooks/teleport/teleport_lifecycle.cpp
  41. 13 58
      Sunrise/src/client/hooks/teleport/teleport_move.cpp
  42. 18 0
      Sunrise/src/client/hooks/world_objects/internal.h
  43. 57 18
      Sunrise/src/client/hooks/world_objects/world_object_rebind_observer.cpp
  44. 34 21
      Sunrise/src/client/hooks/world_objects/world_object_registry.cpp
  45. 43 2
      Sunrise/src/client/runtime/client_runtime_lifecycle.cpp

+ 1 - 16
Sunrise/Sunrise.vcxproj

@@ -207,8 +207,6 @@
     <ClCompile Include="src\client\hooks\graphics\input\graphics_raw_input_window.cpp" />
     <ClCompile Include="src\client\hooks\graphics\input\graphics_raw_input_window.cpp" />
     <ClCompile Include="src\client\hooks\polled_input\polled_input_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\polled_input\polled_input_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\polled_input\polled_input_replacements.cpp" />
     <ClCompile Include="src\client\hooks\polled_input\polled_input_replacements.cpp" />
-    <ClCompile Include="src\client\hooks\banner\banner_bind.cpp" />
-    <ClCompile Include="src\client\hooks\banner\banner_hook_lifecycle.cpp" />
     <ClCompile Include="src\middleware\datagen\family4\loadout\subclass_socket_selection.cpp" />
     <ClCompile Include="src\middleware\datagen\family4\loadout\subclass_socket_selection.cpp" />
     <ClCompile Include="src\state\build_data\cache\records\cache_socket_record_codec.cpp" />
     <ClCompile Include="src\state\build_data\cache\records\cache_socket_record_codec.cpp" />
     <ClCompile Include="src\state\build_data\cache\records\cache_socket_plug_record_codec.cpp" />
     <ClCompile Include="src\state\build_data\cache\records\cache_socket_plug_record_codec.cpp" />
@@ -285,7 +283,7 @@
     <ClCompile Include="src\middleware\bap\activity_host_manager\request\selection\activity_manager_selection_bits.cpp" />
     <ClCompile Include="src\middleware\bap\activity_host_manager\request\selection\activity_manager_selection_bits.cpp" />
     <ClCompile Include="src\middleware\bap\activity_host_manager\request\selection\activity_manager_selection_snapshot.cpp" />
     <ClCompile Include="src\middleware\bap\activity_host_manager\request\selection\activity_manager_selection_snapshot.cpp" />
     <ClCompile Include="src\core\settings\state\activity_arrival_override_parser.cpp" />
     <ClCompile Include="src\core\settings\state\activity_arrival_override_parser.cpp" />
-    <ClCompile Include="src\client\hooks\queuez\svc123_null_payload_guard.cpp" />
+    <ClCompile Include="src\client\hooks\queuez\queuez_null_payload_guard.cpp" />
     <ClCompile Include="src\client\hooks\queuez\queuez_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\queuez\queuez_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\content\diagnostics\content_readiness_report.cpp" />
     <ClCompile Include="src\client\content\diagnostics\content_readiness_report.cpp" />
     <ClCompile Include="src\client\hooks\network\bubble_authority\bubble_authority_replacements.cpp" />
     <ClCompile Include="src\client\hooks\network\bubble_authority\bubble_authority_replacements.cpp" />
@@ -710,7 +708,6 @@
     <ClCompile Include="src\state\build_data\named_build_data_runtime.cpp" />
     <ClCompile Include="src\state\build_data\named_build_data_runtime.cpp" />
     <ClCompile Include="src\state\build_data\socket_entry_lists\socket_entry_list_catalog.cpp" />
     <ClCompile Include="src\state\build_data\socket_entry_lists\socket_entry_list_catalog.cpp" />
     <ClCompile Include="src\client\content\handles\handle_resolver.cpp" />
     <ClCompile Include="src\client\content\handles\handle_resolver.cpp" />
-    <ClCompile Include="src\client\content\activity\activity_tag_reader.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_authored_placements.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_authored_placements.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_container_index.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_container_index.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_container_placements.cpp" />
     <ClCompile Include="src\client\content\activity\scriptable_catalog_container_placements.cpp" />
@@ -786,12 +783,8 @@
     <ClCompile Include="src\middleware\content\packages\tables\authored_placement_reader.cpp" />
     <ClCompile Include="src\middleware\content\packages\tables\authored_placement_reader.cpp" />
     <ClCompile Include="src\middleware\content\packages\tables\authored_squad_reader.cpp" />
     <ClCompile Include="src\middleware\content\packages\tables\authored_squad_reader.cpp" />
     <ClCompile Include="src\client\content\bootstrap\bootstrap_token_publish.cpp" />
     <ClCompile Include="src\client\content\bootstrap\bootstrap_token_publish.cpp" />
-    <ClCompile Include="src\client\content\investment\investment_source.cpp" />
     <ClCompile Include="src\client\content\investment\investment_refresh.cpp" />
     <ClCompile Include="src\client\content\investment\investment_refresh.cpp" />
     <ClCompile Include="src\client\content\investment\worker\investment_refresh_worker.cpp" />
     <ClCompile Include="src\client\content\investment\worker\investment_refresh_worker.cpp" />
-    <ClCompile Include="src\client\content\items\details\configured_item_detail_extractor.cpp" />
-    <ClCompile Include="src\client\content\items\details\definition_detail_reader.cpp" />
-    <ClCompile Include="src\client\content\items\details\socket_detail_reader.cpp" />
     <ClCompile Include="src\middleware\datagen\character_record\character_record_encoder.cpp" />
     <ClCompile Include="src\middleware\datagen\character_record\character_record_encoder.cpp" />
     <ClCompile Include="src\middleware\datagen\character_record\appearance\character_appearance_stats.cpp" />
     <ClCompile Include="src\middleware\datagen\character_record\appearance\character_appearance_stats.cpp" />
     <ClCompile Include="src\middleware\datagen\character_record\appearance\character_appearance_abilities.cpp" />
     <ClCompile Include="src\middleware\datagen\character_record\appearance\character_appearance_abilities.cpp" />
@@ -1266,8 +1259,6 @@
     <ClInclude Include="src\client\hooks\cursor\runtime.h" />
     <ClInclude Include="src\client\hooks\cursor\runtime.h" />
     <ClInclude Include="src\client\hooks\polled_input\polled_input_replacements.h" />
     <ClInclude Include="src\client\hooks\polled_input\polled_input_replacements.h" />
     <ClInclude Include="src\client\hooks\polled_input\runtime.h" />
     <ClInclude Include="src\client\hooks\polled_input\runtime.h" />
-    <ClInclude Include="src\client\hooks\banner\banner_bind.h" />
-    <ClInclude Include="src\client\hooks\banner\banner_hook_lifecycle.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode504.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode504.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode701\opcode701_codec.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode701\opcode701_codec.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode904\opcode904_codec.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode904\opcode904_codec.h" />
@@ -1362,12 +1353,6 @@
     <ClInclude Include="src\client\content\investment\source.h" />
     <ClInclude Include="src\client\content\investment\source.h" />
     <ClInclude Include="src\client\content\investment\worker.h" />
     <ClInclude Include="src\client\content\investment\worker.h" />
     <ClInclude Include="src\client\content\items\layout.h" />
     <ClInclude Include="src\client\content\items\layout.h" />
-    <ClInclude Include="src\client\content\items\details\configured_item_detail_extractor.h" />
-    <ClInclude Include="src\client\content\items\details\layout.h" />
-    <ClInclude Include="src\client\content\items\details\memory.h" />
-    <ClInclude Include="src\client\content\items\details\definition_detail_reader.h" />
-    <ClInclude Include="src\client\content\items\details\relative.h" />
-    <ClInclude Include="src\client\content\items\details\socket_detail_reader.h" />
     <ClInclude Include="src\client\network\consumer.h" />
     <ClInclude Include="src\client\network\consumer.h" />
     <ClInclude Include="src\client\hooking\detour.h" />
     <ClInclude Include="src\client\hooking\detour.h" />
     <ClInclude Include="src\client\hooking\detour\transaction\detour_thread_transaction.h" />
     <ClInclude Include="src\client\hooking\detour\transaction\detour_thread_transaction.h" />

+ 0 - 3
Sunrise/src/client/content/activity/activity_tag_reader.cpp

@@ -1,3 +0,0 @@
-#include "source.h"
-
-namespace sunrise::client::content::activity {} // namespace sunrise::client::content::activity

+ 0 - 7
Sunrise/src/client/content/investment/internal.h

@@ -5,13 +5,6 @@
 
 
 namespace sunrise::client::content::investment {
 namespace sunrise::client::content::investment {
 
 
-/**
- * Resolves the one installed investment-globals candidate backed by the live content tables.
- * @param source Receives the checked runtime tag, handle tables, and bounded reader.
- * @return True when the globals, root, and dense item table all resolve.
- */
-[[nodiscard]] bool resolve_source(Source& source) noexcept;
-
 /** @return True when the next refresh slice needs one presented overlay before its package sweep.
 /** @return True when the next refresh slice needs one presented overlay before its package sweep.
  */
  */
 [[nodiscard]] bool requires_package_sweep() noexcept;
 [[nodiscard]] bool requires_package_sweep() noexcept;

+ 0 - 236
Sunrise/src/client/content/investment/investment_source.cpp

@@ -1,236 +0,0 @@
-#include <algorithm>
-#include <array>
-#include <atomic>
-#include <cstddef>
-#include <cstdint>
-#include <cstdio>
-
-#include "../../../core/logging/log.h"
-#include "../../../state/content/content_catalog.h"
-#include "../../memory/current_process_memory.h"
-#include "../../targets/game/content.h"
-#include "../handles/layout.h"
-#include "internal.h"
-#include "layout.h"
-
-namespace sunrise::client::content::investment {
-namespace {
-
-/** FNV-1 hash of the investment-globals bootstrap name, so the name itself is not shipped. */
-constexpr std::uint32_t kInvestmentGlobalsNameHash = 0x6F7125CBU;
-/**
- * The bootstrap name is not unique, so every match is collected.
- * A truncated lookup is not a failure: every copied candidate is still worth validating.
- */
-constexpr std::size_t kBootstrapMatchCapacity = 64;
-/** One native resolver prefix is enough to recover all descriptor field offsets. */
-constexpr std::size_t kResolverDiagnosticBytes = 128;
-/** Package handles name descriptor ids 1,024 slots above their package id. */
-constexpr std::uintptr_t kContentDescriptorBias = 1024;
-/** Package handles keep their package id above thirteen entry-index bits. */
-constexpr unsigned kPackageShift = 13;
-/** Installed definition tags begin at this package-handle base. */
-constexpr std::uint32_t kPackageTagBase = 0x80800000U;
-
-std::atomic_bool g_diagnosticsReported{false};
-
-/** Reads one complete scalar through the bounded live-process reader. */
-template <typename Value>
-[[nodiscard]] bool read(const Source& source, std::uintptr_t address, Value& value) noexcept {
-    return source.handles.read != nullptr
-           && source.handles.read(source.handles.context,
-                                  address,
-                                  std::span(reinterpret_cast<std::byte*>(&value), sizeof value));
-}
-
-/** Reads one scalar directly from the current process for layout diagnostics. */
-template <typename Value>
-[[nodiscard]] bool read_process(std::uintptr_t address, Value& value) noexcept {
-    return memory::read_current_process(
-        nullptr, address, std::span(reinterpret_cast<std::byte*>(&value), sizeof value));
-}
-
-/** Writes one bounded memory range as a single diagnostic line. */
-void report_bytes(const char* stage,
-                  std::uintptr_t address,
-                  std::span<const std::byte> bytes) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int prefix = std::snprintf(line.data(),
-                                     line.size(),
-                                     "ev=investment stage=%s address=0x%llX bytes=",
-                                     stage,
-                                     static_cast<unsigned long long>(address));
-    if (prefix <= 0) {
-        return;
-    }
-    std::size_t length = (std::min)(static_cast<std::size_t>(prefix), line.size() - 1U);
-    static_cast<void>(core::log::append_hex(line, length, bytes));
-    core::log::write(core::log::Channel::client, core::log::Level::debug, {line.data(), length});
-}
-
-/** Writes one candidate descriptor and its package identity as a diagnostic line. */
-void report_descriptor(unsigned depth,
-                       const state::content::Definition& candidate,
-                       std::uintptr_t address,
-                       std::span<const std::byte> bytes) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int prefix = std::snprintf(line.data(),
-                                     line.size(),
-                                     "ev=investment stage=descriptor depth=%u tag=0x%08X "
-                                     "class=0x%08X address=0x%llX bytes=",
-                                     depth,
-                                     candidate.tag,
-                                     candidate.classId,
-                                     static_cast<unsigned long long>(address));
-    if (prefix <= 0) {
-        return;
-    }
-    std::size_t length = (std::min)(static_cast<std::size_t>(prefix), line.size() - 1U);
-    static_cast<void>(core::log::append_hex(line, length, bytes));
-    core::log::write(core::log::Channel::client, core::log::Level::debug, {line.data(), length});
-}
-
-/** Captures the native resolver and every plausible descriptor base once, without mutation. */
-void report_layout_diagnostics(const targets::game::content::Targets& targets,
-                               std::span<const state::content::Definition> candidates) noexcept {
-    if (!core::log::accepts(core::log::Channel::client, core::log::Level::debug)
-        || g_diagnosticsReported.exchange(true, std::memory_order_relaxed)) {
-        return;
-    }
-    std::array<std::byte, kResolverDiagnosticBytes> resolver{};
-    if (targets.queuezObjectResolver != nullptr
-        && memory::read_current_process(
-            nullptr, reinterpret_cast<std::uintptr_t>(targets.queuezObjectResolver), resolver)) {
-        report_bytes("resolver_bytes",
-                     reinterpret_cast<std::uintptr_t>(targets.queuezObjectResolver),
-                     resolver);
-    }
-
-    const std::uintptr_t slot = reinterpret_cast<std::uintptr_t>(targets.contentHandleTablesSlot);
-    std::array<std::uintptr_t, 3> bases{};
-    const bool base0 = read_process(slot, bases[0]);
-    const bool base1 = base0 && bases[0] != 0 && read_process(bases[0], bases[1]);
-    const bool base2 = base1 && bases[1] != 0 && read_process(bases[1], bases[2]);
-    std::array<char, core::log::kLineCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=investment stage=table_chain slot=0x%llX "
-                                      "read=%u/%u/%u base=0x%llX/0x%llX/0x%llX",
-                                      static_cast<unsigned long long>(slot),
-                                      base0 ? 1U : 0U,
-                                      base1 ? 1U : 0U,
-                                      base2 ? 1U : 0U,
-                                      static_cast<unsigned long long>(bases[0]),
-                                      static_cast<unsigned long long>(bases[1]),
-                                      static_cast<unsigned long long>(bases[2]));
-    if (written > 0) {
-        core::log::write(
-            core::log::Channel::client,
-            core::log::Level::debug,
-            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
-    }
-
-    for (unsigned depth = 0; depth < bases.size(); ++depth) {
-        if (bases[depth] == 0) {
-            continue;
-        }
-        for (const state::content::Definition& candidate : candidates) {
-            if (candidate.tag < kPackageTagBase) {
-                continue;
-            }
-            const std::uintptr_t package =
-                static_cast<std::uintptr_t>(candidate.tag - kPackageTagBase) >> kPackageShift;
-            const std::uintptr_t descriptor =
-                bases[depth]
-                + (package + kContentDescriptorBias) * handles::layout::kTableDescriptorSize;
-            std::array<std::byte, handles::layout::kTableDescriptorSize> bytes{};
-            if (memory::read_current_process(nullptr, descriptor, bytes)) {
-                report_descriptor(depth, candidate, descriptor, bytes);
-            }
-        }
-    }
-}
-
-} // namespace
-
-/** Resolves the checked live investment source selected by its installed bootstrap name. */
-bool resolve_source(Source& source) noexcept {
-    source = {};
-    const auto& runtimeTargets = targets::game::content::get();
-    if (!targets::game::content::is_resolved()
-        || runtimeTargets.contentHandleTablesSlot == nullptr) {
-        return false;
-    }
-
-    std::array<state::content::Definition, kBootstrapMatchCapacity> candidates{};
-    std::size_t count = 0;
-    if (!state::content::lookup_hash(kInvestmentGlobalsNameHash, candidates, count) && count == 0) {
-        return false;
-    }
-    report_layout_diagnostics(runtimeTargets, std::span(candidates).first(count));
-    std::size_t globalsResolved = 0;
-    std::size_t rootTagsRead = 0;
-    std::size_t rootsResolved = 0;
-    std::size_t tableTagsRead = 0;
-    std::size_t tablesResolved = 0;
-    for (std::size_t index = 0; index < count; ++index) {
-        Source candidate{};
-        candidate.investmentGlobalsTag = candidates[index].tag;
-        candidate.handles.tablesSlot =
-            reinterpret_cast<std::uintptr_t>(runtimeTargets.contentHandleTablesSlot);
-        candidate.handles.read = &memory::read_current_process;
-
-        std::uintptr_t globals = 0;
-        std::uintptr_t root = 0;
-        std::uintptr_t table = 0;
-        std::uint32_t rootTag = 0;
-        std::uint32_t tableTag = 0;
-        std::uint64_t rowCount = 0;
-        if (!handles::resolve(candidate.handles, candidate.investmentGlobalsTag, globals)) {
-            continue;
-        }
-        ++globalsResolved;
-        if (!read(candidate, globals + layout::kGlobalsRootTagOffset, rootTag)) {
-            continue;
-        }
-        ++rootTagsRead;
-        if (!handles::resolve(candidate.handles, rootTag, root)) {
-            continue;
-        }
-        ++rootsResolved;
-        if (!read(candidate, root + layout::kItemTableTagOffset, tableTag)) {
-            continue;
-        }
-        ++tableTagsRead;
-        if (!handles::resolve(candidate.handles, tableTag, table)) {
-            continue;
-        }
-        ++tablesResolved;
-        if (!read(candidate, table + 8U, rowCount) || rowCount == 0 || rowCount > 32768U) {
-            continue;
-        }
-        source = candidate;
-        return true;
-    }
-    std::array<char, core::log::kLineCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=investment stage=source result=deferred candidates=%llu "
-                                      "globals=%llu root_tags=%llu roots=%llu table_tags=%llu "
-                                      "tables=%llu",
-                                      static_cast<unsigned long long>(count),
-                                      static_cast<unsigned long long>(globalsResolved),
-                                      static_cast<unsigned long long>(rootTagsRead),
-                                      static_cast<unsigned long long>(rootsResolved),
-                                      static_cast<unsigned long long>(tableTagsRead),
-                                      static_cast<unsigned long long>(tablesResolved));
-    if (written > 0) {
-        core::log::write(
-            core::log::Channel::client,
-            core::log::Level::warn,
-            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
-    }
-    return false;
-}
-
-} // namespace sunrise::client::content::investment

+ 0 - 35
Sunrise/src/client/content/investment/layout.h

@@ -1,49 +1,14 @@
 #pragma once
 #pragma once
 
 
-#include <array>
 #include <cstddef>
 #include <cstddef>
-#include <cstdint>
 
 
 namespace sunrise::client::content::investment::layout {
 namespace sunrise::client::content::investment::layout {
 
 
 /** The globals blob stores its investment-root tag after 16 ABI bytes. */
 /** The globals blob stores its investment-root tag after 16 ABI bytes. */
 inline constexpr std::size_t kGlobalsRootTagOffset = 16;
 inline constexpr std::size_t kGlobalsRootTagOffset = 16;
-/** The runtime bucket-table handle sits at byte 280 of the investment root. */
-inline constexpr std::size_t kInventoryBucketTableTagOffset = 280;
 /** The dense item-table handle sits at byte 776 of the investment root. */
 /** The dense item-table handle sits at byte 776 of the investment root. */
 inline constexpr std::size_t kItemTableTagOffset = 776;
 inline constexpr std::size_t kItemTableTagOffset = 776;
 /** The reusable/randomized plug-set table handle sits at byte 824 of the investment root. */
 /** The reusable/randomized plug-set table handle sits at byte 824 of the investment root. */
 inline constexpr std::size_t kPlugSetTableTagOffset = 824;
 inline constexpr std::size_t kPlugSetTableTagOffset = 824;
-/** The socket-entry-list table handle sits at byte 1,560 of the investment root. */
-inline constexpr std::size_t kSocketEntryListTableTagOffset = 1560;
-
-/** Fixed prefix of the loaded investment-globals blob. */
-struct InvestmentGlobals {
-    std::array<std::byte, kGlobalsRootTagOffset> opaqueBeforeInvestmentRootTag{};
-    std::uint32_t investmentRootTag{};
-};
-
-/**
- * Fixed investment-root prefix shared by the numeric runtime-table extractors.
- * One layout for the three verified handles stops modules quietly giving the same native prefix
- * different meanings.
- */
-struct InvestmentRoot {
-    std::array<std::byte, kInventoryBucketTableTagOffset> opaqueBeforeInventoryBucketTableTag{};
-    std::uint32_t inventoryBucketTableTag{};
-    std::array<std::byte,
-               kItemTableTagOffset - kInventoryBucketTableTagOffset - sizeof(std::uint32_t)>
-        opaqueBeforeItemTableTag{};
-    std::uint32_t itemTableTag{};
-    std::array<std::byte,
-               kSocketEntryListTableTagOffset - kItemTableTagOffset - sizeof(std::uint32_t)>
-        opaqueBeforeSocketEntryListTableTag{};
-    std::uint32_t socketEntryListTableTag{};
-};
-
-static_assert(offsetof(InvestmentGlobals, investmentRootTag) == kGlobalsRootTagOffset);
-static_assert(offsetof(InvestmentRoot, inventoryBucketTableTag) == kInventoryBucketTableTagOffset);
-static_assert(offsetof(InvestmentRoot, itemTableTag) == kItemTableTagOffset);
-static_assert(offsetof(InvestmentRoot, socketEntryListTableTag) == kSocketEntryListTableTagOffset);
 
 
 } // namespace sunrise::client::content::investment::layout
 } // namespace sunrise::client::content::investment::layout

+ 0 - 120
Sunrise/src/client/content/items/details/configured_item_detail_extractor.cpp

@@ -1,120 +0,0 @@
-#include "configured_item_detail_extractor.h"
-
-#include <algorithm>
-#include <bitset>
-#include <cstddef>
-#include <cstdint>
-#include <vector>
-
-#include "../../../../state/build_data/items/item_catalog.h"
-#include "../../../../state/build_data/socket_entry_lists/definition.h"
-#include "../../investment/layout.h"
-#include "../layout.h"
-#include "definition_detail_reader.h"
-#include "memory.h"
-
-namespace sunrise::client::content::items::details {
-namespace {
-
-namespace build_details = state::build_data::items::details;
-namespace build_items = state::build_data::items;
-namespace build_socket_lists = state::build_data::socket_entry_lists;
-namespace dense_layout = sunrise::client::content::items::layout;
-namespace investment_layout = sunrise::client::content::investment::layout;
-
-/**
- * Finds the installed item table once and checks its starting row count.
- * @param source Installed investment globals and bounded memory source.
- * @param table Receives the table address, first-row address, and starting count.
- * @return True when all three handles are found and the dense count fits native State.
- */
-[[nodiscard]] bool resolve_table(const investment::Source& source, reader::Table& table) noexcept {
-    std::uintptr_t globals = 0;
-    std::uintptr_t root = 0;
-    if (!handles::resolve(source.handles, source.investmentGlobalsTag, globals)
-        || !memory::resolve_member(
-            source,
-            globals,
-            offsetof(investment_layout::InvestmentGlobals, investmentRootTag),
-            root)
-        || !memory::resolve_member(source,
-                                   root,
-                                   offsetof(investment_layout::InvestmentRoot, itemTableTag),
-                                   table.tableAddress)) {
-        return false;
-    }
-
-    std::uint64_t rowCount = 0;
-    if (!memory::read_member(
-            source, table.tableAddress, offsetof(dense_layout::ItemTable, rowCount), rowCount)
-        || rowCount == 0 || rowCount > static_cast<std::uint64_t>(build_items::kDefinitionCapacity)
-        || !memory::member_address(
-            table.tableAddress, offsetof(dense_layout::ItemTable, firstRow), table.rowsAddress)) {
-        return false;
-    }
-    table.rowCount = static_cast<std::size_t>(rowCount);
-    return true;
-}
-
-/**
- * Confirms the dense table count still matches the value used for every row bound.
- * @param source Installed item-detail source.
- * @param table Found table and its starting row count.
- * @return True when a last full count read is unchanged.
- */
-[[nodiscard]] bool stable_count(const investment::Source& source,
-                                const reader::Table& table) noexcept {
-    std::uint64_t finalCount = 0;
-    return memory::read_member(
-               source, table.tableAddress, offsetof(dense_layout::ItemTable, rowCount), finalCount)
-           && finalCount == static_cast<std::uint64_t>(table.rowCount);
-}
-
-} // namespace
-
-/** Extracts full native details for configured definition indices. */
-bool extract(const investment::Source& source,
-             std::span<const std::uint16_t> requestedDefinitionIndices,
-             std::size_t socketEntryListRowCount,
-             std::span<build_details::Definition> output,
-             std::size_t& count) noexcept {
-    count = 0;
-    if (requestedDefinitionIndices.empty()) {
-        return true;
-    }
-    if (requestedDefinitionIndices.size() > build_details::kDefinitionCapacity
-        || requestedDefinitionIndices.size() > output.size() || socketEntryListRowCount == 0
-        || socketEntryListRowCount > build_socket_lists::kDefinitionCapacity) {
-        return false;
-    }
-
-    reader::Table table{};
-    if (!resolve_table(source, table)) {
-        return false;
-    }
-
-    std::bitset<build_items::kDefinitionCapacity> seen{};
-    std::vector<build_details::Definition> staged(requestedDefinitionIndices.size());
-    for (std::size_t position = 0; position < requestedDefinitionIndices.size(); ++position) {
-        const std::uint16_t definitionIndex = requestedDefinitionIndices[position];
-        if (static_cast<std::size_t>(definitionIndex) >= table.rowCount
-            || seen.test(definitionIndex)) {
-            return false;
-        }
-        seen.set(definitionIndex);
-        if (!reader::read_definition(
-                source, table, definitionIndex, socketEntryListRowCount, staged[position])) {
-            return false;
-        }
-    }
-
-    // The final count read is the commit gate for the staged caller snapshot.
-    if (!stable_count(source, table)) {
-        return false;
-    }
-    std::copy_n(staged.data(), requestedDefinitionIndices.size(), output.begin());
-    count = requestedDefinitionIndices.size();
-    return true;
-}
-
-} // namespace sunrise::client::content::items::details

+ 0 - 27
Sunrise/src/client/content/items/details/configured_item_detail_extractor.h

@@ -1,27 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-#include <span>
-
-#include "../../../../state/build_data/items/details/definition.h"
-#include "../../investment/source.h"
-
-namespace sunrise::client::content::items::details {
-
-/**
- * Extracts full native details for configured definition indices.
- * @param source Installed investment globals and bounded memory source.
- * @param requestedDefinitionIndices Unique native indices in caller order.
- * @param socketEntryListRowCount Current socket-entry-list table bound.
- * @param output Caller storage left unchanged on failure.
- * @param count Receives the detail count, or zero on failure.
- * @return True when the item-table count stays stable and every detail passes its checks.
- */
-[[nodiscard]] bool extract(const investment::Source& source,
-                           std::span<const std::uint16_t> requestedDefinitionIndices,
-                           std::size_t socketEntryListRowCount,
-                           std::span<state::build_data::items::details::Definition> output,
-                           std::size_t& count) noexcept;
-
-} // namespace sunrise::client::content::items::details

+ 0 - 100
Sunrise/src/client/content/items/details/definition_detail_reader.cpp

@@ -1,100 +0,0 @@
-#include "definition_detail_reader.h"
-
-#include <cstddef>
-#include <cstdint>
-
-#include "../../../../state/build_data/items/item_catalog.h"
-#include "../layout.h"
-#include "layout.h"
-#include "memory.h"
-#include "relative.h"
-#include "socket_detail_reader.h"
-
-namespace sunrise::client::content::items::details::reader {
-namespace {
-
-namespace build_details = state::build_data::items::details;
-namespace dense_layout = sunrise::client::content::items::layout;
-
-/**
- * Reads the optional signed equipment slot from one item definition.
- * @param source Installed item-detail source.
- * @param definition Loaded item definition address.
- * @param output Detail receiving the checked slot, if there is one.
- * @return True when the optional block is absent or carries slot 0 through 19.
- */
-[[nodiscard]] bool read_equipment_slot(const investment::Source& source,
-                                       std::uintptr_t definition,
-                                       build_details::Definition& output) noexcept {
-    bool present = false;
-    std::uintptr_t block = 0;
-    if (!relative::resolve_block(source,
-                                 definition,
-                                 offsetof(layout::ItemDefinition, equipmentBlockRelative),
-                                 present,
-                                 block)) {
-        return false;
-    }
-    if (!present) {
-        output.equipmentSlot.reset();
-        return true;
-    }
-
-    std::int8_t slot = 0;
-    if (!memory::read_member(source, block, offsetof(layout::EquipmentBlock, slot), slot)
-        || slot < 0 || static_cast<std::size_t>(slot) >= build_details::kEquipmentSlotCount) {
-        return false;
-    }
-    output.equipmentSlot = slot;
-    return true;
-}
-
-} // namespace
-
-/** Reads and checks one requested native item definition. */
-bool read_definition(const investment::Source& source,
-                     const Table& table,
-                     std::uint16_t definitionIndex,
-                     std::size_t socketEntryListRowCount,
-                     build_details::Definition& output) noexcept {
-    std::uintptr_t rowOffset = 0;
-    std::uintptr_t rowAddress = 0;
-    dense_layout::ItemIndexRow row{};
-    if (!memory::multiply_fits(definitionIndex, sizeof(dense_layout::ItemIndexRow), rowOffset)
-        || !memory::add_fits(table.rowsAddress, rowOffset, rowAddress)
-        || !memory::read_value(source, rowAddress, row)) {
-        return false;
-    }
-
-    std::uintptr_t target = 0;
-    if (!handles::resolve(source.handles, row.targetHandle, target)) {
-        return false;
-    }
-
-    build_details::Definition candidate{};
-    candidate.definitionIndex = definitionIndex;
-    std::uint8_t instancedDefinitionPredicate = 0;
-    if (!memory::read_member(
-            source, target, offsetof(layout::ItemDefinition, maxStackSize), candidate.maxStackSize)
-        || !memory::read_member(
-            source, target, offsetof(layout::ItemDefinition, bucketId), candidate.bucketId)
-        || !memory::read_member(source,
-                                target,
-                                offsetof(layout::ItemDefinition, instancedDefinitionPredicate),
-                                instancedDefinitionPredicate)
-        || candidate.maxStackSize <= 0
-        || candidate.bucketId == state::build_data::items::kUnresolvedBucketId
-        || !read_equipment_slot(source, target, candidate)
-        || !socket_reader::read_ordinary(source, target, table.rowCount, candidate)
-        || !socket_reader::read_list_index(source, target, socketEntryListRowCount, candidate)) {
-        return false;
-    }
-    candidate.instancedDefinitionState = instancedDefinitionPredicate == 0
-                                             ? build_details::InstancedDefinitionState::stackable
-                                             : build_details::InstancedDefinitionState::instanced;
-
-    output = candidate;
-    return true;
-}
-
-} // namespace sunrise::client::content::items::details::reader

+ 0 - 34
Sunrise/src/client/content/items/details/definition_detail_reader.h

@@ -1,34 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-
-#include "configured_item_detail_extractor.h"
-
-namespace sunrise::client::content::items::details::reader {
-
-namespace build_details = state::build_data::items::details;
-
-/** Resolved dense item table held only for one bounded extraction call. */
-struct Table {
-    std::uintptr_t tableAddress{};
-    std::uintptr_t rowsAddress{};
-    std::size_t rowCount{};
-};
-
-/**
- * Reads and checks one requested native item definition.
- * @param source Installed item-detail source.
- * @param table Stable-call table address, row start, and starting count.
- * @param definitionIndex Requested native row index.
- * @param socketEntryListRowCount Current socket-entry-list table bound.
- * @param output Receives a full detail only on success.
- * @return True when the target and every needed block pass their checks.
- */
-[[nodiscard]] bool read_definition(const investment::Source& source,
-                                   const Table& table,
-                                   std::uint16_t definitionIndex,
-                                   std::size_t socketEntryListRowCount,
-                                   build_details::Definition& output) noexcept;
-
-} // namespace sunrise::client::content::items::details::reader

+ 0 - 112
Sunrise/src/client/content/items/details/layout.h

@@ -1,112 +0,0 @@
-#pragma once
-
-#include <array>
-#include <cstddef>
-#include <cstdint>
-
-namespace sunrise::client::content::items::details::layout {
-
-/** The optional equipment block is self-relative from definition byte 16. */
-inline constexpr std::size_t kEquipmentBlockRelativeOffset = 16;
-/** The optional ordinary-socket block is self-relative from definition byte 104. */
-inline constexpr std::size_t kOrdinarySocketBlockRelativeOffset = 104;
-/** The optional socket-list block is self-relative from definition byte 128. */
-inline constexpr std::size_t kSocketListBlockRelativeOffset = 128;
-/** The native signed stack limit sits at definition byte 180. */
-inline constexpr std::size_t kMaxStackSizeOffset = 180;
-/** The native inventory bucket id sits at definition byte 184. */
-inline constexpr std::size_t kBucketIdOffset = 184;
-/** Definition byte 187 is zero for stackable rows and nonzero for instanced rows. */
-inline constexpr std::size_t kInstancedDefinitionPredicateOffset = 187;
-/** The equipment block stores its signed slot id at byte 24. */
-inline constexpr std::size_t kEquipmentSlotOffset = 24;
-/** The array header begins relative to the offset member at array-field byte 8. */
-inline constexpr std::size_t kArrayHeaderRelativeOffset = 8;
-/** The array marker sits in the 4 bytes right before the resolved header. */
-inline constexpr std::size_t kArrayMarkerBackOffset = sizeof(std::uint32_t);
-/** Loaded native arrays use this marker before their resolved header. */
-inline constexpr std::uint32_t kArrayHeaderMarker = 0x80809FBDU;
-/** Ordinary socket arrays declare this native element class. */
-inline constexpr std::uint32_t kOrdinarySocketElementClass = 0x808077C4U;
-/** Array entries start 16 bytes after the resolved header. */
-inline constexpr std::size_t kArrayFirstEntryOffset = 16;
-/** One ordinary socket entry is 80 native bytes. */
-inline constexpr std::size_t kOrdinarySocketEntrySize = 80;
-/** An ordinary socket entry stores its initial plug index at byte 2. */
-inline constexpr std::size_t kInitialPlugIndexOffset = 2;
-
-/** Self-relative array field at the start of the ordinary-socket block. */
-struct RelativeArray {
-    std::uint64_t count{};
-    std::int64_t headerRelative{};
-};
-
-/** Fixed item-definition fields used by configured detail extraction. */
-struct ItemDefinition {
-    std::array<std::byte, kEquipmentBlockRelativeOffset> opaqueBeforeEquipmentBlock{};
-    std::int64_t equipmentBlockRelative{};
-    std::array<std::byte,
-               kOrdinarySocketBlockRelativeOffset - kEquipmentBlockRelativeOffset
-                   - sizeof(std::int64_t)>
-        opaqueBeforeOrdinarySocketBlock{};
-    std::int64_t ordinarySocketBlockRelative{};
-    /** Stats are read from the package blob, never from loaded memory, so this stays opaque. */
-    std::array<std::byte,
-               kSocketListBlockRelativeOffset - kOrdinarySocketBlockRelativeOffset
-                   - sizeof(std::int64_t)>
-        opaqueBeforeSocketListBlock{};
-    std::int64_t socketListBlockRelative{};
-    std::array<std::byte,
-               kMaxStackSizeOffset - kSocketListBlockRelativeOffset - sizeof(std::int64_t)>
-        opaqueBeforeMaxStackSize{};
-    std::int32_t maxStackSize{};
-    std::uint8_t bucketId{};
-    std::array<std::byte,
-               kInstancedDefinitionPredicateOffset - kBucketIdOffset - sizeof(std::uint8_t)>
-        opaqueBeforeInstancedDefinitionPredicate{};
-    std::uint8_t instancedDefinitionPredicate{};
-};
-
-/** Fixed prefix of the optional equipment block. */
-struct EquipmentBlock {
-    std::array<std::byte, kEquipmentSlotOffset> opaqueBeforeSlot{};
-    std::int8_t slot{};
-};
-
-/** One fixed native ordinary-socket entry. */
-struct OrdinarySocketEntry {
-    std::uint16_t socketTypeIndex{};
-    std::uint16_t initialPlugIndex{};
-    std::array<std::byte,
-               kOrdinarySocketEntrySize - kInitialPlugIndexOffset - sizeof(std::uint16_t)>
-        opaqueAfterInitialPlug{};
-};
-
-/** Resolved fixed native array header before its ordinary-socket entries. */
-struct ArrayHeader {
-    std::uint64_t repeatedCount{};
-    std::uint32_t elementClass{};
-    std::array<std::byte, kArrayFirstEntryOffset - sizeof(std::uint64_t) - sizeof(std::uint32_t)>
-        opaqueBeforeFirstEntry{};
-};
-
-/** Fixed prefix of the optional socket-entry-list selection block. */
-struct SocketListBlock {
-    std::uint16_t index{};
-};
-
-static_assert(offsetof(ItemDefinition, equipmentBlockRelative) == kEquipmentBlockRelativeOffset);
-static_assert(offsetof(ItemDefinition, ordinarySocketBlockRelative)
-              == kOrdinarySocketBlockRelativeOffset);
-static_assert(offsetof(ItemDefinition, socketListBlockRelative) == kSocketListBlockRelativeOffset);
-static_assert(offsetof(ItemDefinition, maxStackSize) == kMaxStackSizeOffset);
-static_assert(offsetof(ItemDefinition, bucketId) == kBucketIdOffset);
-static_assert(offsetof(ItemDefinition, instancedDefinitionPredicate)
-              == kInstancedDefinitionPredicateOffset);
-static_assert(offsetof(EquipmentBlock, slot) == kEquipmentSlotOffset);
-static_assert(offsetof(RelativeArray, headerRelative) == kArrayHeaderRelativeOffset);
-static_assert(offsetof(OrdinarySocketEntry, initialPlugIndex) == kInitialPlugIndexOffset);
-static_assert(sizeof(OrdinarySocketEntry) == kOrdinarySocketEntrySize);
-static_assert(sizeof(ArrayHeader) == kArrayFirstEntryOffset);
-
-} // namespace sunrise::client::content::items::details::layout

+ 0 - 126
Sunrise/src/client/content/items/details/memory.h

@@ -1,126 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-#include <limits>
-#include <span>
-
-#include "../../investment/source.h"
-#include "../layout.h"
-
-namespace sunrise::client::content::items::details::memory {
-
-[[nodiscard]] inline bool
-add_fits(std::uintptr_t left, std::uintptr_t right, std::uintptr_t& result) noexcept {
-    if (right > (std::numeric_limits<std::uintptr_t>::max)() - left) {
-        return false;
-    }
-    result = left + right;
-    return true;
-}
-
-/**
- * Adds a signed self-relative offset without wrapping the address space.
- * @param base Address of the relative-offset field.
- * @param relative Signed byte offset from that field.
- * @param result Receives the address found.
- * @return True when the signed sum fits.
- */
-[[nodiscard]] inline bool
-add_signed_fits(std::uintptr_t base, std::int64_t relative, std::uintptr_t& result) noexcept {
-    if (relative >= 0) {
-        const auto positive = static_cast<std::uint64_t>(relative);
-        if (positive > (std::numeric_limits<std::uintptr_t>::max)()) {
-            return false;
-        }
-        return add_fits(base, static_cast<std::uintptr_t>(positive), result);
-    }
-
-    // Adding one before negation keeps the most-negative signed value representable.
-    const std::uint64_t magnitude = static_cast<std::uint64_t>(-(relative + 1)) + 1U;
-    if (magnitude > (std::numeric_limits<std::uintptr_t>::max)()
-        || static_cast<std::uintptr_t>(magnitude) > base) {
-        return false;
-    }
-    result = base - static_cast<std::uintptr_t>(magnitude);
-    return true;
-}
-
-/**
- * Multiplies two address parts without wrapping the address space.
- * @param result Receives the whole product.
- * @return True when the product fits.
- */
-[[nodiscard]] inline bool
-multiply_fits(std::uintptr_t left, std::uintptr_t right, std::uintptr_t& result) noexcept {
-    if (left != 0 && right > (std::numeric_limits<std::uintptr_t>::max)() / left) {
-        return false;
-    }
-    result = left * right;
-    return true;
-}
-
-/**
- * Reads one whole scalar through the caller's bounded memory source.
- * @tparam Value Trivially copied scalar or layout object.
- * @param source Installed item-detail source.
- * @param value Receives the whole value.
- * @return True when the callback reads every byte.
- */
-template <typename Value>
-[[nodiscard]] bool
-read_value(const investment::Source& source, std::uintptr_t address, Value& value) noexcept {
-    return source.handles.read != nullptr
-           && source.handles.read(source.handles.context,
-                                  address,
-                                  std::span(reinterpret_cast<std::byte*>(&value), sizeof value));
-}
-
-/**
- * Works out a checked address for one typed member of a loaded blob.
- * @param base Loaded blob address.
- * @param offset Byte offset of the member.
- * @param address Receives the member address.
- * @return True when the member address fits.
- */
-[[nodiscard]] inline bool
-member_address(std::uintptr_t base, std::size_t offset, std::uintptr_t& address) noexcept {
-    return add_fits(base, static_cast<std::uintptr_t>(offset), address);
-}
-
-/**
- * Reads one typed member at a checked offset from a loaded blob.
- * @tparam Value Trivially copied member type.
- * @param source Installed item-detail source.
- * @param base Loaded blob address.
- * @param offset Byte offset of the member.
- * @param value Receives the whole member.
- * @return True when the address fits and the read succeeds.
- */
-template <typename Value>
-[[nodiscard]] bool read_member(const investment::Source& source,
-                               std::uintptr_t base,
-                               std::size_t offset,
-                               Value& value) noexcept {
-    std::uintptr_t address = 0;
-    return member_address(base, offset, address) && read_value(source, address, value);
-}
-
-/**
- * Reads one handle member and finds the blob it points to.
- * @param source Installed item-detail source.
- * @param parent Loaded parent blob address.
- * @param offset Byte offset of the handle member.
- * @param child Receives the loaded child blob address.
- * @return True when the member reads and its loaded handle is found.
- */
-[[nodiscard]] inline bool resolve_member(const investment::Source& source,
-                                         std::uintptr_t parent,
-                                         std::size_t offset,
-                                         std::uintptr_t& child) noexcept {
-    std::uint32_t handle = 0;
-    return read_member(source, parent, offset, handle)
-           && handles::resolve(source.handles, handle, child);
-}
-
-} // namespace sunrise::client::content::items::details::memory

+ 0 - 39
Sunrise/src/client/content/items/details/relative.h

@@ -1,39 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-
-#include "memory.h"
-
-namespace sunrise::client::content::items::details::relative {
-
-/**
- * Finds one optional self-relative definition member.
- * @param source Installed item-detail source.
- * @param definition Loaded item definition address.
- * @param memberOffset Byte offset of the signed relative member.
- * @param present Receives whether the relative member is nonzero.
- * @param block Receives the block address when present.
- * @return True when the member reads and any nonzero offset is safe.
- */
-[[nodiscard]] inline bool resolve_block(const investment::Source& source,
-                                        std::uintptr_t definition,
-                                        std::size_t memberOffset,
-                                        bool& present,
-                                        std::uintptr_t& block) noexcept {
-    present = false;
-    block = 0;
-    std::int64_t displacement = 0;
-    std::uintptr_t memberAddress = 0;
-    if (!memory::member_address(definition, memberOffset, memberAddress)
-        || !memory::read_value(source, memberAddress, displacement)) {
-        return false;
-    }
-    if (displacement == 0) {
-        return true;
-    }
-    present = true;
-    return memory::add_signed_fits(memberAddress, displacement, block);
-}
-
-} // namespace sunrise::client::content::items::details::relative

+ 0 - 131
Sunrise/src/client/content/items/details/socket_detail_reader.cpp

@@ -1,131 +0,0 @@
-#include "socket_detail_reader.h"
-
-#include <cstddef>
-#include <cstdint>
-
-#include "layout.h"
-#include "memory.h"
-#include "relative.h"
-
-namespace sunrise::client::content::items::details::socket_reader {
-namespace {
-
-namespace build_details = state::build_data::items::details;
-
-/**
- * Finds and checks an ordinary-socket array header.
- * @param source Installed item-detail source.
- * @param block Loaded ordinary-socket block address.
- * @param field Receives the self-relative array field.
- * @param headerAddress Receives the array header address.
- * @param header Receives the whole fixed header prefix.
- * @return True when count, marker, repeated count, and element class match the ABI.
- */
-[[nodiscard]] bool read_header(const investment::Source& source,
-                               std::uintptr_t block,
-                               layout::RelativeArray& field,
-                               std::uintptr_t& headerAddress,
-                               layout::ArrayHeader& header) noexcept {
-    if (!memory::read_value(source, block, field)
-        || field.count > static_cast<std::uint64_t>(build_details::kInitialPlugCapacity)) {
-        return false;
-    }
-
-    std::uintptr_t relativeAddress = 0;
-    std::uintptr_t markerAddress = 0;
-    if (!memory::member_address(
-            block, offsetof(layout::RelativeArray, headerRelative), relativeAddress)
-        || !memory::add_signed_fits(relativeAddress, field.headerRelative, headerAddress)
-        || !memory::add_signed_fits(headerAddress,
-                                    -static_cast<std::int64_t>(layout::kArrayMarkerBackOffset),
-                                    markerAddress)) {
-        return false;
-    }
-
-    std::uint32_t marker = 0;
-    return memory::read_value(source, markerAddress, marker) && marker == layout::kArrayHeaderMarker
-           && memory::read_value(source, headerAddress, header)
-           && header.repeatedCount == field.count
-           && header.elementClass == layout::kOrdinarySocketElementClass;
-}
-
-} // namespace
-
-/** Reads the optional ordinary-socket block and its initial plug indices. */
-bool read_ordinary(const investment::Source& source,
-                   std::uintptr_t definition,
-                   std::size_t itemRowCount,
-                   build_details::Definition& output) noexcept {
-    bool present = false;
-    std::uintptr_t block = 0;
-    if (!relative::resolve_block(source,
-                                 definition,
-                                 offsetof(layout::ItemDefinition, ordinarySocketBlockRelative),
-                                 present,
-                                 block)) {
-        return false;
-    }
-    if (!present) {
-        output.ordinarySocketState = build_details::OrdinarySocketState::absent;
-        output.ordinarySocketCount = 0;
-        return true;
-    }
-
-    layout::RelativeArray field{};
-    layout::ArrayHeader header{};
-    std::uintptr_t headerAddress = 0;
-    if (!read_header(source, block, field, headerAddress, header)) {
-        return false;
-    }
-
-    std::uintptr_t entriesAddress = 0;
-    if (!memory::member_address(headerAddress, sizeof(layout::ArrayHeader), entriesAddress)) {
-        return false;
-    }
-    for (std::size_t index = 0; index < static_cast<std::size_t>(field.count); ++index) {
-        std::uintptr_t entryOffset = 0;
-        std::uintptr_t entryAddress = 0;
-        layout::OrdinarySocketEntry entry{};
-        if (!memory::multiply_fits(index, sizeof(layout::OrdinarySocketEntry), entryOffset)
-            || !memory::add_fits(entriesAddress, entryOffset, entryAddress)
-            || !memory::read_value(source, entryAddress, entry)
-            || (entry.initialPlugIndex != build_details::kUnavailableItemIndex
-                && static_cast<std::size_t>(entry.initialPlugIndex) >= itemRowCount)) {
-            return false;
-        }
-        output.initialPlugIndices[index] = entry.initialPlugIndex;
-    }
-    output.ordinarySocketState = build_details::OrdinarySocketState::present;
-    output.ordinarySocketCount = static_cast<std::uint8_t>(field.count);
-    return true;
-}
-
-/** Reads the optional socket-entry-list row selection. */
-bool read_list_index(const investment::Source& source,
-                     std::uintptr_t definition,
-                     std::size_t rowCount,
-                     build_details::Definition& output) noexcept {
-    bool present = false;
-    std::uintptr_t block = 0;
-    if (!relative::resolve_block(source,
-                                 definition,
-                                 offsetof(layout::ItemDefinition, socketListBlockRelative),
-                                 present,
-                                 block)) {
-        return false;
-    }
-    if (!present) {
-        output.socketEntryListIndex = build_details::kEmptySocketEntryListIndex;
-        return true;
-    }
-
-    std::uint16_t index = 0;
-    if (!memory::read_member(source, block, offsetof(layout::SocketListBlock, index), index)
-        || static_cast<std::size_t>(index) >= rowCount) {
-        return false;
-    }
-    output.socketEntryListIndex = index;
-    return true;
-}
-
-} // namespace sunrise::client::content::items::details::socket_reader

+ 0 - 38
Sunrise/src/client/content/items/details/socket_detail_reader.h

@@ -1,38 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-
-#include "configured_item_detail_extractor.h"
-
-namespace sunrise::client::content::items::details::socket_reader {
-
-namespace build_details = state::build_data::items::details;
-
-/**
- * Reads the optional ordinary-socket block and its initial plug indices.
- * @param source Installed item-detail source.
- * @param definition Loaded item definition address.
- * @param itemRowCount Current dense item-table row bound.
- * @param output Detail receiving block state, count, and initial plugs.
- * @return True when the optional array is absent or fully good.
- */
-[[nodiscard]] bool read_ordinary(const investment::Source& source,
-                                 std::uintptr_t definition,
-                                 std::size_t itemRowCount,
-                                 build_details::Definition& output) noexcept;
-
-/**
- * Reads the optional socket-entry-list row selection.
- * @param source Installed item-detail source.
- * @param definition Loaded item definition address.
- * @param rowCount Current socket-entry-list table bound.
- * @param output Detail receiving row 0 or the checked pick.
- * @return True when the optional block is absent or picks an in-range row.
- */
-[[nodiscard]] bool read_list_index(const investment::Source& source,
-                                   std::uintptr_t definition,
-                                   std::size_t rowCount,
-                                   build_details::Definition& output) noexcept;
-
-} // namespace sunrise::client::content::items::details::socket_reader

+ 0 - 179
Sunrise/src/client/hooks/banner/banner_bind.cpp

@@ -1,179 +0,0 @@
-/**
- * Bind for the orbit banner component. Its tick calls the update body only when a dirty byte is
- * set, and an unbound component can never become dirty, so it keeps the constructor's values for
- * the whole run: power zero, no art, emblem state 2. The bind writes the identity triple State
- * already publishes and marks the component dirty once.
- */
-
-#include "banner_bind.h"
-
-#include <array>
-#include <atomic>
-#include <cstddef>
-#include <cstdint>
-#include <cstdio>
-#include <cstring>
-
-#include "../../../core/logging/log.h"
-#include "../../../state/runtime/runtime.h"
-#include "../../hooking/detour.h"
-
-namespace sunrise::client::hooks::banner {
-namespace {
-
-using BannerTick = std::int64_t(__fastcall*)(void*, std::int64_t);
-
-hooking::detour::Handle g_handle{};
-std::atomic<BannerTick> g_original{nullptr};
-std::atomic<unsigned> g_reported{0};
-
-/** What the update body left in the component, read after it has run. */
-struct Published {
-    std::int32_t light{};
-    std::uint16_t definition{};
-    std::uint32_t art{};
-    std::uint32_t state{};
-};
-
-/** One resolved identity the banner can be bound to. */
-struct Identity {
-    std::uint64_t account{};
-    std::uint64_t character{};
-    bool resolved{};
-};
-
-/** @return The account and selected-character keys State publishes, when both are set. */
-[[nodiscard]] Identity selected_identity() noexcept {
-    const state::AccountState account = state::account_snapshot();
-    if (account.primarySoid == 0) {
-        return Identity{};
-    }
-    for (std::size_t index = 0; index < account.characterCount; ++index) {
-        const state::CharacterState& character = account.characters[index];
-        if (character.selected && character.soid != 0) {
-            return Identity{account.primarySoid, character.soid, true};
-        }
-    }
-    return Identity{};
-}
-
-/**
- * Reports one bind and what the update body then published, so a boot says whether the bind was
- * enough, not just that it happened.
- * @param identity Keys the component was bound to.
- * @param self Component address, which separates one instance from another.
- * @param published What the update body wrote once it had run.
- */
-void report(const Identity& identity, const void* self, const Published& published) noexcept {
-    const unsigned emitted = g_reported.load(std::memory_order_relaxed);
-    if (emitted >= kMaxBindReports) {
-        return;
-    }
-    g_reported.store(emitted + 1, std::memory_order_relaxed);
-    std::array<char, kReportCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=banner stage=bind this=0x%016llX character=0x%016llX"
-                                      " light=%d emblem_def=%u emblem_art=0x%08X emblem_state=%u"
-                                      " result=bound",
-                                      reinterpret_cast<unsigned long long>(self),
-                                      static_cast<unsigned long long>(identity.character),
-                                      static_cast<int>(published.light),
-                                      static_cast<unsigned>(published.definition),
-                                      static_cast<unsigned>(published.art),
-                                      static_cast<unsigned>(published.state));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-}
-
-/**
- * Binds an unbound component before its tick reads the triple, then runs the original.
- * @param self Borrowed banner component.
- * @param frame Frame argument the tick forwards unchanged.
- * @return The tick's own result.
- */
-__declspec(noinline) std::int64_t __fastcall tick(void* self, std::int64_t frame) noexcept {
-    const BannerTick original = g_original.load(std::memory_order_acquire);
-    if (original == nullptr) {
-        return 0;
-    }
-    if (self == nullptr) {
-        return original(self, frame);
-    }
-    std::byte* const component = static_cast<std::byte*>(self);
-    std::uint64_t account = 0;
-    std::uint8_t family = 0;
-    std::memcpy(&account, component + BannerLayout::accountKey, sizeof account);
-    std::memcpy(&family, component + BannerLayout::family, sizeof family);
-    if (account != 0 && family != kBannerFamilyNone) {
-        return original(self, frame);
-    }
-    const Identity identity = selected_identity();
-    if (!identity.resolved) {
-        return original(self, frame);
-    }
-    std::memcpy(component + BannerLayout::accountKey, &identity.account, sizeof identity.account);
-    std::memcpy(
-        component + BannerLayout::characterKey, &identity.character, sizeof identity.character);
-    std::memcpy(component + BannerLayout::family, &kBannerFamily, sizeof kBannerFamily);
-    std::memcpy(component + BannerLayout::dirty, &kBannerDirty, sizeof kBannerDirty);
-    // The update body runs inside this call, so its published fields are read after it returns.
-    const std::int64_t result = original(self, frame);
-    Published published{};
-    std::memcpy(&published.light, component + BannerLayout::light, sizeof published.light);
-    std::memcpy(&published.definition,
-                component + BannerLayout::emblemDefinition,
-                sizeof published.definition);
-    std::memcpy(&published.art, component + BannerLayout::emblemArt, sizeof published.art);
-    std::memcpy(&published.state, component + BannerLayout::emblemState, sizeof published.state);
-    report(identity, self, published);
-    return result;
-}
-
-/** @param reason Key naming the step that failed. @return False, for a direct return. */
-bool fail_install(const char* reason) noexcept {
-    std::array<char, kReportCapacity> line{};
-    const int written = std::snprintf(
-        line.data(), line.size(), "ev=banner stage=bind result=fail reason=%s", reason);
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::warn,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-    return false;
-}
-
-} // namespace
-
-/** Finds the banner tick and attaches the bind. */
-bool install_banner_bind() noexcept {
-    if (g_handle.attached) {
-        return true;
-    }
-    std::byte* const target = scan_main_image_unique(kBannerTickSignature, "banner_tick");
-    if (target == nullptr) {
-        return fail_install("target");
-    }
-    const hooking::detour::Spec spec{target, reinterpret_cast<void*>(&tick)};
-    if (!hooking::detour::install(spec, g_handle)) {
-        return fail_install("attach");
-    }
-    g_original.store(reinterpret_cast<BannerTick>(g_handle.original), std::memory_order_release);
-    core::log::write(
-        core::log::Channel::client, core::log::Level::info, "ev=banner stage=bind result=ok");
-    return true;
-}
-
-/** Detaches the banner bind and drops the trampoline it got. */
-void uninstall_banner_bind() noexcept {
-    if (g_handle.attached) {
-        (void)hooking::detour::uninstall(g_handle);
-    }
-    g_original.store(nullptr, std::memory_order_release);
-    g_reported.store(0, std::memory_order_release);
-}
-
-} // namespace sunrise::client::hooks::banner

+ 0 - 75
Sunrise/src/client/hooks/banner/banner_bind.h

@@ -1,75 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-#include <string_view>
-
-#include "../../patterns/image_scan.h"
-
-namespace sunrise::client::hooks::banner {
-
-using patterns::scan_main_image_unique;
-using patterns::signature;
-using patterns::signature_length;
-
-/** The orbit banner component's per-frame tick, matched on its prologue and argument moves. */
-inline constexpr std::string_view kBannerTickSignatureText =
-    "48 89 5C 24 ? 55 56 57 41 55 41 56 48 83 EC ? 4C 8B F2 48 8B D9";
-/** Compiled pattern bytes of the signature text above. */
-inline constexpr auto kBannerTickSignature =
-    signature<signature_length(kBannerTickSignatureText)>(kBannerTickSignatureText);
-
-/**
- * Fields of the banner component the bind writes, as byte offsets from its base. The tick calls
- * its update body only when a dirty byte is set, and an unbound component never becomes dirty.
- */
-struct BannerLayout {
-    /** Account key of the identity triple the update body looks up. */
-    static constexpr std::size_t accountKey = 136;
-    /** Character key of the same triple. */
-    static constexpr std::size_t characterKey = 144;
-    /** Queuez family the triple is resolved against, read as a signed byte. */
-    static constexpr std::size_t family = 152;
-    /** Dirty byte the tick tests and then clears. */
-    static constexpr std::size_t dirty = 1196;
-    /** Light the update body publishes. Read only, to report whether the bind produced one. */
-    static constexpr std::size_t light = 548;
-    /**
-     * Emblem fields the update body publishes, as `a3 + 492/504/512` where `a3` is this base plus
-     * 160: the item definition index, the resolved art entry, and the state. State 2 is the
-     * unresolved branch, which writes -1 into both of the others.
-     */
-    static constexpr std::size_t emblemDefinition = 652;
-    /** Resolved art entry, written into both halves of a qword. */
-    static constexpr std::size_t emblemArt = 664;
-    /** Emblem state, 0 when resolved and 2 when the update body found no item. */
-    static constexpr std::size_t emblemState = 672;
-};
-
-/**
- * Family 4 carries the objects whose banners do update, and it picks the update arm that finds
- * both the emblem and the light. The registrar refuses anything above 7.
- */
-inline constexpr std::uint8_t kBannerFamily = 4;
-/** Family value of an unbound component. It is read signed, so it sign-extends to -1. */
-inline constexpr std::uint8_t kBannerFamilyNone = 0xFF;
-/** Value written into the dirty byte so the tick runs its update body once. */
-inline constexpr std::uint8_t kBannerDirty = 1;
-
-/** Bind lines allowed per run. The tick runs every frame, so an uncapped report buries the log. */
-inline constexpr unsigned kMaxBindReports = 12;
-
-/** Bounds one report line at the width of its fields. */
-inline constexpr std::size_t kReportCapacity = 224;
-
-/**
- * Attaches the banner bind, which gives the orbit banner component an identity so its update body
- * runs. Without it the component keeps its constructor values: power zero, no art, emblem state 2.
- * @return True when the target is found and the detour attaches.
- */
-[[nodiscard]] bool install_banner_bind() noexcept;
-
-/** Detaches the banner bind. */
-void uninstall_banner_bind() noexcept;
-
-} // namespace sunrise::client::hooks::banner

+ 0 - 32
Sunrise/src/client/hooks/banner/banner_hook_lifecycle.cpp

@@ -1,32 +0,0 @@
-#include "banner_hook_lifecycle.h"
-
-#include <atomic>
-
-#include "banner_bind.h"
-
-namespace sunrise::client::hooks::banner {
-namespace {
-
-std::atomic_bool g_installed{false};
-
-} // namespace
-
-/** Attaches the banner identity bind. */
-bool install() noexcept {
-    const bool bind = install_banner_bind();
-    g_installed.store(bind, std::memory_order_release);
-    return bind;
-}
-
-/** Detaches the banner chain, in the reverse order of install. */
-void uninstall() noexcept {
-    uninstall_banner_bind();
-    g_installed.store(false, std::memory_order_release);
-}
-
-/** @return True while the banner bind is attached. */
-bool is_installed() noexcept {
-    return g_installed.load(std::memory_order_acquire);
-}
-
-} // namespace sunrise::client::hooks::banner

+ 0 - 17
Sunrise/src/client/hooks/banner/banner_hook_lifecycle.h

@@ -1,17 +0,0 @@
-#pragma once
-
-namespace sunrise::client::hooks::banner {
-
-/**
- * Attaches the orbit banner component bind.
- * @return True when the bind attached.
- */
-[[nodiscard]] bool install() noexcept;
-
-/** Detaches the banner component bind. */
-void uninstall() noexcept;
-
-/** @return True while the banner bind is attached. */
-[[nodiscard]] bool is_installed() noexcept;
-
-} // namespace sunrise::client::hooks::banner

+ 1 - 4
Sunrise/src/client/hooks/bitmap/bitmap_ref_guard.h

@@ -57,10 +57,7 @@ inline constexpr unsigned kMaxReports = 8;
 /** Size of one guard line, set by its reference, tag and state fields. */
 /** Size of one guard line, set by its reference, tag and state fields. */
 inline constexpr std::size_t kReportCapacity = 128;
 inline constexpr std::size_t kReportCapacity = 128;
 
 
-/**
- * New references reported per run while watching is on. It answers one question: does a bitmap
- * ever reach a widget. Repeats are dropped, so the cap counts distinct references.
- */
+/** Distinct references reported per run. Repeats are dropped, so the cap counts new ones. */
 inline constexpr unsigned kMaxSeenReports = 40;
 inline constexpr unsigned kMaxSeenReports = 40;
 
 
 /**
 /**

+ 0 - 6
Sunrise/src/client/hooks/graphics/graphics_hook_replacements.cpp

@@ -134,10 +134,4 @@ ProtectedEntries protected_entries() noexcept {
     };
     };
 }
 }
 
 
-HRESULT STDMETHODCALLTYPE present(IDXGISwapChain* swapChain,
-                                  UINT syncInterval,
-                                  UINT flags) noexcept {
-    return present_body(swapChain, syncInterval, flags);
-}
-
 } // namespace sunrise::client::hooks::graphics::replacement
 } // namespace sunrise::client::hooks::graphics::replacement

+ 0 - 5
Sunrise/src/client/hooks/graphics/graphics_hook_replacements.h

@@ -118,11 +118,6 @@ using ProtectedEntries = std::array<hooking::detour::ProtectedCodeEntry, kProtec
 /** @return Unwind-backed call-lifetime bodies needed for safe detachment. */
 /** @return Unwind-backed call-lifetime bodies needed for safe detachment. */
 [[nodiscard]] ProtectedEntries protected_entries() noexcept;
 [[nodiscard]] ProtectedEntries protected_entries() noexcept;
 
 
-/** SDK-compatible IDXGISwapChain::Present detour. */
-HRESULT STDMETHODCALLTYPE present(IDXGISwapChain* swapChain,
-                                  UINT syncInterval,
-                                  UINT flags) noexcept;
-
 } // namespace replacement
 } // namespace replacement
 
 
 } // namespace sunrise::client::hooks::graphics
 } // namespace sunrise::client::hooks::graphics

+ 22 - 10
Sunrise/src/client/hooks/inactivity/inactivity_override.cpp

@@ -29,14 +29,22 @@ using patterns::signature;
 using patterns::signature_length;
 using patterns::signature_length;
 
 
 /**
 /**
- * The activity config getter. Every obfuscated pointer getter shares this prologue, so the load of
- * its own global stays unwildcarded; it is image-relative, so ASLR does not move it.
+ * The boot-flow step-timeout body, the anchor for the activity config getter it calls. Every
+ * obfuscated pointer getter shares the getter's own prologue and tail, so the getter has no
+ * unique shape without its global's displacement.
  */
  */
-constexpr std::string_view kConfigGetterText =
-    "40 53 48 83 EC 20 48 8B 1D 2B 10 1A 02 48 85 DB 0F 84 ? ? ? ? 48 89 5C 24 30 "
-    "E8 ? ? ? ? 33 C3";
-/** Compiled pattern bytes of the config getter signature. */
-constexpr auto kConfigGetter = signature<signature_length(kConfigGetterText)>(kConfigGetterText);
+constexpr std::string_view kStepTimeoutText =
+    "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 20 B9 03 00 00 00 C7 02 12 00 00 "
+    "00 48 8B FA E8 ? ? ? ? 84 C0 0F 84 ? ? ? ? E8 ? ? ? ? 8B D8 E8 ? ? ? ? 48 8B E8 E8 ? ? ? ? "
+    "83 F8 FF 74 0C 8B C8 E8 ? ? ? ? 48 8B F0 EB 02 33 F6 8D 43 E5 A9 FD FF FF FF 74 6F 83 FB "
+    "26 75 4D E8 ? ? ? ?";
+/** Compiled pattern bytes of the step-timeout signature. */
+constexpr auto kStepTimeout = signature<signature_length(kStepTimeoutText)>(kStepTimeoutText);
+/** The getter call inside that body: its E8, its operand, and the instruction after it. */
+constexpr std::size_t kGetterCall = 0x65;
+constexpr std::size_t kGetterOperand = 0x66;
+constexpr std::size_t kGetterNext = 0x6A;
+constexpr std::byte kCallOpcode{0xE8};
 
 
 /** Activity lanes the Client keeps a separate inactivity timeout for. */
 /** Activity lanes the Client keeps a separate inactivity timeout for. */
 constexpr std::size_t kActivityCount = 14;
 constexpr std::size_t kActivityCount = 14;
@@ -161,15 +169,19 @@ bool install() noexcept {
         ReleaseSRWLockExclusive(&g_lock);
         ReleaseSRWLockExclusive(&g_lock);
         return true;
         return true;
     }
     }
-    std::byte* const match = scan_main_image_unique(kConfigGetter, "inactivity_config_getter");
-    if (match == nullptr) {
+    std::byte* const match = scan_main_image_unique(kStepTimeout, "inactivity_step_timeout");
+    std::byte* const getter =
+        match != nullptr && match[kGetterCall] == kCallOpcode
+            ? patterns::resolve_relative(match + kGetterOperand, match + kGetterNext)
+            : nullptr;
+    if (getter == nullptr) {
         ReleaseSRWLockExclusive(&g_lock);
         ReleaseSRWLockExclusive(&g_lock);
         core::log::write(core::log::Channel::client,
         core::log::write(core::log::Channel::client,
                          core::log::Level::warn,
                          core::log::Level::warn,
                          "ev=inactivity stage=install result=fail reason=target");
                          "ev=inactivity stage=install result=fail reason=target");
         return false;
         return false;
     }
     }
-    g_getter = reinterpret_cast<ConfigGetter>(match);
+    g_getter = reinterpret_cast<ConfigGetter>(getter);
     ReleaseSRWLockExclusive(&g_lock);
     ReleaseSRWLockExclusive(&g_lock);
     core::log::write(core::log::Channel::client,
     core::log::write(core::log::Channel::client,
                      core::log::Level::info,
                      core::log::Level::info,

+ 0 - 1
Sunrise/src/client/hooks/network/content_config/coordinator/content_config_call_coordinator.cpp

@@ -35,7 +35,6 @@ __declspec(noinline) void ingress_body(CallLease& lease, HookSlot slot) noexcept
     AcquireSRWLockShared(&g_lifecycleLock);
     AcquireSRWLockShared(&g_lifecycleLock);
     if (g_trackCalls) {
     if (g_trackCalls) {
         lease.original = g_originals[index(slot)];
         lease.original = g_originals[index(slot)];
-        lease.fetch = reinterpret_cast<ContentFetch>(g_contentFetch);
         lease.accepting = g_accepting[index(slot)];
         lease.accepting = g_accepting[index(slot)];
         ++g_callDepth;
         ++g_callDepth;
         lease.tracked = true;
         lease.tracked = true;

+ 0 - 1
Sunrise/src/client/hooks/network/content_config/coordinator/content_config_call_coordinator.h

@@ -9,7 +9,6 @@ namespace sunrise::client::hooks::network::content_config::coordinator {
 /** State copied at ingress and kept until egress. */
 /** State copied at ingress and kept until egress. */
 struct CallLease final {
 struct CallLease final {
     void* original{};
     void* original{};
-    ContentFetch fetch{};
     bool accepting{};
     bool accepting{};
     bool tracked{};
     bool tracked{};
 };
 };

+ 0 - 2
Sunrise/src/client/hooks/network/content_config/internal.h

@@ -10,7 +10,6 @@
 
 
 namespace sunrise::client::hooks::network::content_config {
 namespace sunrise::client::hooks::network::content_config {
 
 
-using ContentFetch = std::int64_t(__fastcall*)(const char*, const char*, char);
 using ContentTick = void(__fastcall*)(void*);
 using ContentTick = void(__fastcall*)(void*);
 
 
 /** Stable hook slots for local content routing. */
 /** Stable hook slots for local content routing. */
@@ -28,7 +27,6 @@ extern std::array<hooking::detour::Handle, kHookCount> g_handles;
 extern std::array<void*, kHookCount> g_originals;
 extern std::array<void*, kHookCount> g_originals;
 extern std::array<void*, kHookCount> g_targetEntries;
 extern std::array<void*, kHookCount> g_targetEntries;
 extern std::array<bool, kHookCount> g_accepting;
 extern std::array<bool, kHookCount> g_accepting;
-extern void* g_contentFetch;
 extern bool g_trackCalls;
 extern bool g_trackCalls;
 extern volatile LONG g_activeCalls;
 extern volatile LONG g_activeCalls;
 
 

+ 1 - 3
Sunrise/src/client/hooks/network/content_config/lifecycle/content_config_hook_install.cpp

@@ -23,8 +23,7 @@ void finish_failed_install() noexcept {
 bool install() noexcept {
 bool install() noexcept {
     AcquireSRWLockExclusive(&g_lifecycleLock);
     AcquireSRWLockExclusive(&g_lifecycleLock);
     if (!lifecycle::g_transitionActive && lifecycle::all_installed() && g_trackCalls
     if (!lifecycle::g_transitionActive && lifecycle::all_installed() && g_trackCalls
-        && lifecycle::all_accepting() && g_contentFetch != nullptr
-        && gate::is_disabled(lifecycle::g_gateOwnership)) {
+        && lifecycle::all_accepting() && gate::is_disabled(lifecycle::g_gateOwnership)) {
         ReleaseSRWLockExclusive(&g_lifecycleLock);
         ReleaseSRWLockExclusive(&g_lifecycleLock);
         return true;
         return true;
     }
     }
@@ -72,7 +71,6 @@ bool install() noexcept {
         g_targetEntries[offset] = specs[offset].target;
         g_targetEntries[offset] = specs[offset].target;
         g_accepting[offset] = true;
         g_accepting[offset] = true;
     }
     }
-    g_contentFetch = resolved.contentConfigFetch;
     g_trackCalls = true;
     g_trackCalls = true;
     lifecycle::g_transitionActive = false;
     lifecycle::g_transitionActive = false;
     ReleaseSRWLockExclusive(&g_lifecycleLock);
     ReleaseSRWLockExclusive(&g_lifecycleLock);

+ 0 - 1
Sunrise/src/client/hooks/network/content_config/lifecycle/content_config_hook_lifecycle.cpp

@@ -9,7 +9,6 @@ bool is_installed() noexcept {
     AcquireSRWLockShared(&g_lifecycleLock);
     AcquireSRWLockShared(&g_lifecycleLock);
     const bool installed = !lifecycle::g_transitionActive && lifecycle::all_installed()
     const bool installed = !lifecycle::g_transitionActive && lifecycle::all_installed()
                            && g_trackCalls && lifecycle::all_accepting()
                            && g_trackCalls && lifecycle::all_accepting()
-                           && g_contentFetch != nullptr
                            && gate::is_disabled(lifecycle::g_gateOwnership);
                            && gate::is_disabled(lifecycle::g_gateOwnership);
     ReleaseSRWLockShared(&g_lifecycleLock);
     ReleaseSRWLockShared(&g_lifecycleLock);
     return installed;
     return installed;

+ 0 - 2
Sunrise/src/client/hooks/network/content_config/lifecycle/content_config_hook_storage.cpp

@@ -12,7 +12,6 @@ std::array<hooking::detour::Handle, kHookCount> g_handles{};
 std::array<void*, kHookCount> g_originals{};
 std::array<void*, kHookCount> g_originals{};
 std::array<void*, kHookCount> g_targetEntries{};
 std::array<void*, kHookCount> g_targetEntries{};
 std::array<bool, kHookCount> g_accepting{};
 std::array<bool, kHookCount> g_accepting{};
-void* g_contentFetch{};
 bool g_trackCalls{};
 bool g_trackCalls{};
 volatile LONG g_activeCalls{};
 volatile LONG g_activeCalls{};
 
 
@@ -59,7 +58,6 @@ void clear_runtime() noexcept {
     g_originals = {};
     g_originals = {};
     g_targetEntries = {};
     g_targetEntries = {};
     g_accepting = {};
     g_accepting = {};
-    g_contentFetch = nullptr;
     g_trackCalls = false;
     g_trackCalls = false;
 }
 }
 
 

+ 0 - 2
Sunrise/src/client/hooks/network/coordinator/network_call_coordinator.cpp

@@ -46,8 +46,6 @@ ingress_body(CallLease& lease, HookSlot slot, ConsumerKind consumer) noexcept {
     lease.accepting = g_accepting[static_cast<std::size_t>(slot)];
     lease.accepting = g_accepting[static_cast<std::size_t>(slot)];
     if (consumer == ConsumerKind::http) {
     if (consumer == ConsumerKind::http) {
         lease.httpConsumer = g_httpConsumer.load(std::memory_order_acquire);
         lease.httpConsumer = g_httpConsumer.load(std::memory_order_acquire);
-    } else if (consumer == ConsumerKind::bap) {
-        lease.bapConsumer = g_bapConsumer.load(std::memory_order_acquire);
     }
     }
     ++g_callDepth;
     ++g_callDepth;
     ReleaseSRWLockShared(&g_lock);
     ReleaseSRWLockShared(&g_lock);

+ 0 - 2
Sunrise/src/client/hooks/network/coordinator/network_call_coordinator.h

@@ -10,14 +10,12 @@ namespace sunrise::client::hooks::network::coordinator {
 enum class ConsumerKind {
 enum class ConsumerKind {
     none,
     none,
     http,
     http,
-    bap,
 };
 };
 
 
 /** State copied at ingress and kept until egress. */
 /** State copied at ingress and kept until egress. */
 struct CallLease final {
 struct CallLease final {
     void* original{};
     void* original{};
     sunrise::client::network::HttpConsumer httpConsumer{};
     sunrise::client::network::HttpConsumer httpConsumer{};
-    sunrise::client::network::BapConsumer bapConsumer{};
     bool accepting{};
     bool accepting{};
 };
 };
 
 

+ 2 - 7
Sunrise/src/client/hooks/queuez/queuez_hook_lifecycle.h

@@ -2,15 +2,10 @@
 
 
 namespace sunrise::client::hooks::queuez {
 namespace sunrise::client::hooks::queuez {
 
 
-/**
- * Attaches the family-zero source-list seed. The sweep declares one record per source key, so a
- * list the producer left empty leaves the client with no family-zero record, and no subscribe for
- * the object push to answer.
- * @return True when every fix attached.
- */
+/** Attaches the queuez null-payload guard. @return True when every fix attached. */
 [[nodiscard]] bool install() noexcept;
 [[nodiscard]] bool install() noexcept;
 
 
-/** Detaches the family-zero source-list seed. */
+/** Detaches the queuez null-payload guard. */
 void uninstall() noexcept;
 void uninstall() noexcept;
 
 
 /** @return True while at least one fix is attached. */
 /** @return True while at least one fix is attached. */

+ 0 - 0
Sunrise/src/client/hooks/queuez/svc123_null_payload_guard.cpp → Sunrise/src/client/hooks/queuez/queuez_null_payload_guard.cpp


+ 0 - 8
Sunrise/src/client/hooks/retail_log/retail_log_lifecycle.cpp

@@ -53,12 +53,4 @@ bool uninstall() noexcept {
     return removed;
     return removed;
 }
 }
 
 
-/** @return True while the retail-log capture hook is attached. */
-bool is_installed() noexcept {
-    AcquireSRWLockShared(&g_lock);
-    const bool attached = g_handle.attached;
-    ReleaseSRWLockShared(&g_lock);
-    return attached;
-}
-
 } // namespace sunrise::client::hooks::retail_log
 } // namespace sunrise::client::hooks::retail_log

+ 0 - 3
Sunrise/src/client/hooks/retail_log/retail_log_lifecycle.h

@@ -8,7 +8,4 @@ namespace sunrise::client::hooks::retail_log {
 /** Removes the retail-log capture hook. */
 /** Removes the retail-log capture hook. */
 [[nodiscard]] bool uninstall() noexcept;
 [[nodiscard]] bool uninstall() noexcept;
 
 
-/** @return True while the retail-log capture hook is attached. */
-[[nodiscard]] bool is_installed() noexcept;
-
 } // namespace sunrise::client::hooks::retail_log
 } // namespace sunrise::client::hooks::retail_log

+ 12 - 2
Sunrise/src/client/hooks/sense_chain_guard/sense_chain_guard.cpp

@@ -126,6 +126,8 @@ StructSize g_structSize{};
 RecordPayload g_payload{};
 RecordPayload g_payload{};
 std::atomic<std::uint64_t> g_nextReportTick{0};
 std::atomic<std::uint64_t> g_nextReportTick{0};
 std::atomic<unsigned> g_reports{0};
 std::atomic<unsigned> g_reports{0};
+/** Set while a walk runs. Two walks at once would share the chain buffer below. */
+std::atomic_flag g_walking{};
 
 
 /** Rebuilds a pointer from an address without an implementation-defined integer conversion. */
 /** Rebuilds a pointer from an address without an implementation-defined integer conversion. */
 [[nodiscard]] std::byte* address_pointer(std::uint64_t address) noexcept {
 [[nodiscard]] std::byte* address_pointer(std::uint64_t address) noexcept {
@@ -188,7 +190,7 @@ void extract_record(std::uint64_t tables, std::byte* record) noexcept {
     record[kRecordExtractedOffset] = std::byte{1};
     record[kRecordExtractedOffset] = std::byte{1};
 }
 }
 
 
-/** Chain handles the dry pass collected. The walk runs only on the sim fiber job. */
+/** Chain handles the dry pass collected. Written only while `g_walking` is held. */
 std::uint32_t g_chain[kChainCap];
 std::uint32_t g_chain[kChainCap];
 
 
 /**
 /**
@@ -288,7 +290,13 @@ __declspec(noinline) std::int64_t __fastcall sense_extract(std::byte* owner,
         return 0;
         return 0;
     }
     }
     WalkTrail trail{};
     WalkTrail trail{};
+    // A second walk while one runs extracts nothing this tick; the chain buffer is not shared.
+    if (g_walking.test_and_set(std::memory_order_acquire)) {
+        report_walk(trail, "busy");
+        return 0;
+    }
     const int outcome = walk_and_extract(list, trail);
     const int outcome = walk_and_extract(list, trail);
+    g_walking.clear(std::memory_order_release);
     if (outcome == 0) {
     if (outcome == 0) {
         report_walk(trail, "runaway");
         report_walk(trail, "runaway");
     } else if (outcome == 2) {
     } else if (outcome == 2) {
@@ -361,7 +369,9 @@ bool uninstall() noexcept {
     if (!g_installed.load(std::memory_order_acquire)) {
     if (!g_installed.load(std::memory_order_acquire)) {
         return true;
         return true;
     }
     }
-    if (!hooking::detour::uninstall(g_handle)) {
+    // The replacement reads the pointers below, so a thread still inside it refuses the detach.
+    bool replacementActive = false;
+    if (!hooking::detour::uninstall(g_handle, replacementActive)) {
         return false;
         return false;
     }
     }
     g_tables = nullptr;
     g_tables = nullptr;

+ 5 - 1
Sunrise/src/client/hooks/stall_probe/stall_probe.cpp

@@ -21,6 +21,7 @@
 #include "../../../steam/runtime/runtime.h"
 #include "../../../steam/runtime/runtime.h"
 #include "../../diagnostics/module_range.h"
 #include "../../diagnostics/module_range.h"
 #include "../../memory/current_process_memory.h"
 #include "../../memory/current_process_memory.h"
+#include "../../process/freeze/client_process_freeze.h"
 
 
 namespace sunrise::client::hooks::stall_probe {
 namespace sunrise::client::hooks::stall_probe {
 namespace {
 namespace {
@@ -163,12 +164,14 @@ void report_thread(std::uint32_t tid) noexcept {
     }
     }
 }
 }
 
 
-/** Captures every other thread in the process, one at a time. */
+/** Captures every other thread in the process, one at a time, as the only suspender. */
 void dump_all_threads() noexcept {
 void dump_all_threads() noexcept {
     const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
     const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
     if (snapshot == INVALID_HANDLE_VALUE) {
     if (snapshot == INVALID_HANDLE_VALUE) {
         return;
         return;
     }
     }
+    // A Detours transaction suspends threads too; two suspenders at once freeze each other.
+    process::freeze::enter_exclusive();
     THREADENTRY32 entry{};
     THREADENTRY32 entry{};
     entry.dwSize = sizeof(entry);
     entry.dwSize = sizeof(entry);
     const DWORD processId = GetCurrentProcessId();
     const DWORD processId = GetCurrentProcessId();
@@ -179,6 +182,7 @@ void dump_all_threads() noexcept {
             report_thread(entry.th32ThreadID);
             report_thread(entry.th32ThreadID);
         }
         }
     }
     }
+    process::freeze::leave_exclusive();
     CloseHandle(snapshot);
     CloseHandle(snapshot);
 }
 }
 
 

+ 13 - 6
Sunrise/src/client/hooks/sword_skate/sword_skate.cpp

@@ -39,12 +39,17 @@ constexpr SHORT kKeyHeldBit = static_cast<SHORT>(0x8000);
 constexpr std::uint16_t kJumpAction =
 constexpr std::uint16_t kJumpAction =
     static_cast<std::uint16_t>(state::account::settings::bindings::Action::jump);
     static_cast<std::uint16_t>(state::account::settings::bindings::Action::jump);
 
 
+/** Milliseconds a read binding is trusted before the account is copied again. */
+constexpr std::uint64_t kBindingRereadMs = 5000;
+
 /** Jump held on the previous tick, so the flag is only cleared on the press and not on the hold. */
 /** Jump held on the previous tick, so the flag is only cleared on the press and not on the hold. */
 bool g_jumpHeld{false};
 bool g_jumpHeld{false};
 /** Both halves of the jump binding. An unbound half stays empty. */
 /** Both halves of the jump binding. An unbound half stays empty. */
 std::array<std::optional<std::uint16_t>, 2> g_jumpBinding{};
 std::array<std::optional<std::uint16_t>, 2> g_jumpBinding{};
-/** Set once the binding is read, so the costly account snapshot runs once. */
+/** Set once the binding is read; the costly account snapshot then runs once per interval. */
 bool g_bindingRead{false};
 bool g_bindingRead{false};
+/** Tick of the last read, so a rebinding during the run is picked up. */
+std::uint64_t g_bindingReadTick{};
 
 
 /**
 /**
  * Reads one value out of game memory without faulting on a torn pointer.
  * Reads one value out of game memory without faulting on a torn pointer.
@@ -71,21 +76,23 @@ bool g_bindingRead{false};
 }
 }
 
 
 /**
 /**
- * Takes the account's jump binding once it is loaded.
- * @return True when the binding has been read, whether or not either half is bound.
+ * Takes the account's jump binding once it is loaded, and again once per interval.
+ * @return True when a binding has been read, whether or not either half is bound.
  */
  */
 [[nodiscard]] bool read_jump_binding() noexcept {
 [[nodiscard]] bool read_jump_binding() noexcept {
-    if (g_bindingRead) {
+    const std::uint64_t now = GetTickCount64();
+    if (g_bindingRead && now < g_bindingReadTick + kBindingRereadMs) {
         return true;
         return true;
     }
     }
-    // The snapshot copies the whole account, so it stops once the bindings arrive.
+    // The snapshot copies the whole account, so it runs once per interval, not per tick.
     const state::AccountState account = state::account_snapshot();
     const state::AccountState account = state::account_snapshot();
     if (!account.settings.keyBindings.configured) {
     if (!account.settings.keyBindings.configured) {
-        return false;
+        return g_bindingRead;
     }
     }
     const auto& binding = account.settings.keyBindings.values[kJumpAction];
     const auto& binding = account.settings.keyBindings.values[kJumpAction];
     g_jumpBinding = {binding.primary, binding.secondary};
     g_jumpBinding = {binding.primary, binding.secondary};
     g_bindingRead = true;
     g_bindingRead = true;
+    g_bindingReadTick = now;
     return true;
     return true;
 }
 }
 
 

+ 3 - 11
Sunrise/src/client/hooks/teleport/internal.h

@@ -32,15 +32,7 @@ inline constexpr std::size_t kCameraAspect = kCameraPositionX + 0xB8;
 
 
 /** Object handle the physics component drives, as a u16. */
 /** Object handle the physics component drives, as a u16. */
 inline constexpr std::size_t kPhysicsComponentObjectHandle = 44;
 inline constexpr std::size_t kPhysicsComponentObjectHandle = 44;
-/** Non-zero here stops the sync before it reads anything else. */
-inline constexpr std::size_t kPhysicsComponentSuppress = 568;
-
-/** Body flag word the sync tests before it publishes a transform. */
-inline constexpr std::size_t kBodyFlags = 76;
-/** The bit in that word the sync requires. Clearing it skips the transform publish entirely. */
-inline constexpr std::uint32_t kBodyActiveBit = 0x40;
-/** Motion type. The sync excludes some values from publishing a transform. */
-inline constexpr std::size_t kBodyMotionType = 352;
+
 /** Rigid-body array on the physics component. */
 /** Rigid-body array on the physics component. */
 inline constexpr std::size_t kPhysicsComponentBodyArray = 400;
 inline constexpr std::size_t kPhysicsComponentBodyArray = 400;
 /** Index into that array, signed. */
 /** Index into that array, signed. */
@@ -50,8 +42,8 @@ inline constexpr std::size_t kBodyEntryStride = 80;
 inline constexpr std::size_t kBodyPointer = 32;
 inline constexpr std::size_t kBodyPointer = 32;
 
 
 /**
 /**
- * Rigid-body world position. Still a hypothesis: it was lined up through a third-party Havok
- * layout, and the document meant to confirm it was never written.
+ * Swept-transform centre of mass 1 of the body's motion state. The transform translation at
+ * +416 is rebuilt from it by the physics step, so a body at rest does not follow a write here.
  */
  */
 inline constexpr std::size_t kBodyPositionX = 448;
 inline constexpr std::size_t kBodyPositionX = 448;
 /** Rigid-body velocity. The sync copies this into the physics component every tick. */
 /** Rigid-body velocity. The sync copies this into the physics component every tick. */

+ 0 - 16
Sunrise/src/client/hooks/teleport/runtime.h

@@ -108,22 +108,6 @@ void apply_pending(void* component) noexcept;
  */
  */
 [[nodiscard]] void* local_player_component() noexcept;
 [[nodiscard]] void* local_player_component() noexcept;
 
 
-/**
- * Writes the world position of the body a physics component drives.
- * @param component Physics component.
- * @param position Three lanes to store.
- * @return True when the body was found and written.
- */
-[[nodiscard]] bool write_position(void* component, const Vector& position) noexcept;
-
-/**
- * Reads the linear velocity of the body a physics component drives.
- * @param component Physics component.
- * @param velocity Receives the three lanes.
- * @return True when the body was found and read.
- */
-[[nodiscard]] bool read_velocity(void* component, Vector& velocity) noexcept;
-
 /**
 /**
  * Writes the linear velocity of the body a physics component drives.
  * Writes the linear velocity of the body a physics component drives.
  * @param component Physics component.
  * @param component Physics component.

+ 10 - 1
Sunrise/src/client/hooks/teleport/teleport_lifecycle.cpp

@@ -204,7 +204,16 @@ void uninstall() noexcept {
     hooks::fly::reset();
     hooks::fly::reset();
     client::player::position::reset();
     client::player::position::reset();
     polled_input::release_key();
     polled_input::release_key();
-    (void)hooking::detour::uninstall(g_handles);
+    // A thread still inside a replacement keeps the detours; the cleared targets make them inert.
+    bool replacementActive = false;
+    if (!hooking::detour::uninstall(g_handles, replacementActive)) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::warn,
+                         replacementActive
+                             ? "ev=teleport stage=uninstall result=fail reason=active"
+                             : "ev=teleport stage=uninstall result=fail reason=detach");
+        return;
+    }
     g_handles = {};
     g_handles = {};
 }
 }
 
 

+ 13 - 58
Sunrise/src/client/hooks/teleport/teleport_move.cpp

@@ -63,7 +63,7 @@ std::atomic_uint32_t g_pressFrames{0};
 ControlledHandle g_controlledHandle{};
 ControlledHandle g_controlledHandle{};
 CameraSingleton g_cameraSingleton{};
 CameraSingleton g_cameraSingleton{};
 
 
-/** Written by the camera hook and read by the physics hook. Both run on the same thread. */
+/** Camera forward vector for the next physics tick. Every access holds g_cameraPoseLock. */
 std::array<float, kVectorLanes> g_forward{};
 std::array<float, kVectorLanes> g_forward{};
 
 
 /** Withdraws the pose when the camera block is not readable for this frame. */
 /** Withdraws the pose when the camera block is not readable for this frame. */
@@ -74,6 +74,13 @@ void invalidate_camera_pose() noexcept {
     ReleaseSRWLockExclusive(&g_cameraPoseLock);
     ReleaseSRWLockExclusive(&g_cameraPoseLock);
 }
 }
 
 
+/** @param forward Receives the published camera forward vector, copied under the pose lock. */
+void copy_forward(Vector& forward) noexcept {
+    AcquireSRWLockShared(&g_cameraPoseLock);
+    forward = g_forward;
+    ReleaseSRWLockShared(&g_cameraPoseLock);
+}
+
 /**
 /**
  * Reads one value out of game memory without faulting on a torn pointer.
  * Reads one value out of game memory without faulting on a torn pointer.
  * @param address Source address.
  * @param address Source address.
@@ -177,41 +184,6 @@ void end_press() noexcept {
     }
     }
 }
 }
 
 
-/**
- * Reports the gate values the sync tests before it publishes a transform.
- *
- * The move lands while moving and does nothing at rest for all three write targets, so what
- * changes at rest is upstream of the write. These four gates are what the sync reads first.
- *
- * @param component Physics component owning the player.
- * @param body Rigid body behind it.
- */
-void report_gates(const std::byte* component, const std::byte* body) noexcept {
-    std::uint8_t suppressed = 0;
-    std::int32_t bodyIndex = 0;
-    std::uint32_t bodyFlags = 0;
-    std::uint8_t motionType = 0;
-    (void)read_at(component + kPhysicsComponentSuppress, suppressed);
-    (void)read_at(component + kPhysicsComponentBodyIndex, bodyIndex);
-    (void)read_at(body + kBodyFlags, bodyFlags);
-    (void)read_at(body + kBodyMotionType, motionType);
-    std::array<char, 160> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=teleport stage=gates suppress=%u index=%d "
-                                      "flags=0x%08X active=%u motion=%u",
-                                      static_cast<unsigned>(suppressed),
-                                      static_cast<int>(bodyIndex),
-                                      static_cast<unsigned>(bodyFlags),
-                                      (bodyFlags & kBodyActiveBit) != 0 ? 1U : 0U,
-                                      static_cast<unsigned>(motionType));
-    if (written > 0) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-}
-
 /**
 /**
  * @param component Candidate physics component.
  * @param component Candidate physics component.
  * @return True when it drives the object the local player controls.
  * @return True when it drives the object the local player controls.
@@ -286,9 +258,11 @@ void set_vertical_velocity(std::byte* body, float value) noexcept {
  * @return True when the new position was stored.
  * @return True when the new position was stored.
  */
  */
 [[nodiscard]] bool move_body(std::byte* body, float distance) noexcept {
 [[nodiscard]] bool move_body(std::byte* body, float distance) noexcept {
+    Vector forward{};
+    copy_forward(forward);
     std::array<float, kVectorLanes> delta{};
     std::array<float, kVectorLanes> delta{};
     for (std::size_t lane = 0; lane < kVectorLanes; ++lane) {
     for (std::size_t lane = 0; lane < kVectorLanes; ++lane) {
-        delta[lane] = g_forward[lane] * distance;
+        delta[lane] = forward[lane] * distance;
     }
     }
     std::array<float, kVectorLanes> position{};
     std::array<float, kVectorLanes> position{};
     std::array<float, kVectorLanes> moved{};
     std::array<float, kVectorLanes> moved{};
@@ -327,7 +301,6 @@ void set_vertical_velocity(std::byte* body, float value) noexcept {
         report_skip("no_body");
         report_skip("no_body");
         return false;
         return false;
     }
     }
-    report_gates(component, body);
     set_vertical_velocity(body, 0.0F);
     set_vertical_velocity(body, 0.0F);
     if (!move_body(body, client::movement::get().distance)) {
     if (!move_body(body, client::movement::get().distance)) {
         return false;
         return false;
@@ -381,8 +354,8 @@ void capture_camera_pose(std::uint32_t playerIndex) noexcept {
     AcquireSRWLockExclusive(&g_cameraPoseLock);
     AcquireSRWLockExclusive(&g_cameraPoseLock);
     g_cameraPose = pose;
     g_cameraPose = pose;
     g_cameraPoseValid = true;
     g_cameraPoseValid = true;
-    ReleaseSRWLockExclusive(&g_cameraPoseLock);
     g_forward = pose.forward;
     g_forward = pose.forward;
+    ReleaseSRWLockExclusive(&g_cameraPoseLock);
     g_forwardValid.store(true, std::memory_order_release);
     g_forwardValid.store(true, std::memory_order_release);
 }
 }
 
 
@@ -477,24 +450,6 @@ bool read_position(void* component, Vector& position) noexcept {
     return body != nullptr && read_at(body + kBodyPositionX, position);
     return body != nullptr && read_at(body + kBodyPositionX, position);
 }
 }
 
 
-/** Writes the world position of the body a physics component drives. */
-bool write_position(void* component, const Vector& position) noexcept {
-    if (component == nullptr) {
-        return false;
-    }
-    std::byte* const body = body_of(static_cast<std::byte*>(component));
-    return body != nullptr && write_vector(body + kBodyPositionX, position);
-}
-
-/** Reads the linear velocity of the body a physics component drives. */
-bool read_velocity(void* component, Vector& velocity) noexcept {
-    if (component == nullptr) {
-        return false;
-    }
-    std::byte* const body = body_of(static_cast<std::byte*>(component));
-    return body != nullptr && read_at(body + kBodyVelocityX, velocity);
-}
-
 /** Writes the linear velocity of the body a physics component drives. */
 /** Writes the linear velocity of the body a physics component drives. */
 bool write_velocity(void* component, const Vector& velocity) noexcept {
 bool write_velocity(void* component, const Vector& velocity) noexcept {
     if (component == nullptr) {
     if (component == nullptr) {
@@ -509,7 +464,7 @@ bool camera_forward(Vector& forward) noexcept {
     if (!g_forwardValid.load(std::memory_order_acquire)) {
     if (!g_forwardValid.load(std::memory_order_acquire)) {
         return false;
         return false;
     }
     }
-    forward = g_forward;
+    copy_forward(forward);
     return true;
     return true;
 }
 }
 
 

+ 18 - 0
Sunrise/src/client/hooks/world_objects/internal.h

@@ -192,6 +192,24 @@ void erase(std::uint32_t handle) noexcept;
 /** @return Signatures the squad rebind and observer trace needs, in resolve order. */
 /** @return Signatures the squad rebind and observer trace needs, in resolve order. */
 [[nodiscard]] std::span<const patterns::Pattern> trace_patterns() noexcept;
 [[nodiscard]] std::span<const patterns::Pattern> trace_patterns() noexcept;
 
 
+/**
+ * Decodes the actor iterator from the rebind body's own call, and checks the other four calls
+ * still reach the targets the image pass resolved.
+ * @param rebind Resolved rebind body.
+ * @param source Resolved source lookup.
+ * @param resolveSource Resolved source resolver.
+ * @param predicate Resolved predicate dispatch.
+ * @param bind Resolved actor bind.
+ * @param iterator Receives the iterator; null when any call site moved.
+ * @return True when every call site holds a near call to its expected target.
+ */
+[[nodiscard]] bool bind_rebind_calls(std::byte* rebind,
+                                     std::byte* source,
+                                     std::byte* resolveSource,
+                                     std::byte* predicate,
+                                     std::byte* bind,
+                                     std::byte*& iterator) noexcept;
+
 /** Calls native construction first, then retains the successfully initialized identity. */
 /** Calls native construction first, then retains the successfully initialized identity. */
 __declspec(noinline) std::uint32_t* __fastcall instantiate(std::uint32_t* output,
 __declspec(noinline) std::uint32_t* __fastcall instantiate(std::uint32_t* output,
                                                            const void* entry,
                                                            const void* entry,

+ 57 - 18
Sunrise/src/client/hooks/world_objects/world_object_rebind_observer.cpp

@@ -12,6 +12,7 @@
 #include <string_view>
 #include <string_view>
 
 
 #include "../../../core/logging/log.h"
 #include "../../../core/logging/log.h"
+#include "../../patterns/image_scan.h"
 #include "../../patterns/registry.h"
 #include "../../patterns/registry.h"
 #include "../../patterns/signature_text.h"
 #include "../../patterns/signature_text.h"
 #include "internal.h"
 #include "internal.h"
@@ -47,6 +48,15 @@ constexpr std::size_t kSquadMembers = 80, kSquadCountOffset = 0x314, kSquadRefsO
 /** The actor record holds its binding as two qwords; a shorter stride cannot carry them. */
 /** The actor record holds its binding as two qwords; a shorter stride cannot carry them. */
 constexpr std::uintptr_t kActorBindingOffset = 0x38;
 constexpr std::uintptr_t kActorBindingOffset = 0x38;
 constexpr std::uint32_t kActorBindingStride = 0x48;
 constexpr std::uint32_t kActorBindingStride = 0x48;
+/** Return addresses of the rebind body's five direct calls, as offsets from its start. */
+constexpr std::uintptr_t kIteratorReturn = 0x5F;
+constexpr std::uintptr_t kSourceReturn = 0x83;
+constexpr std::uintptr_t kResolveReturn = 0xA5;
+constexpr std::uintptr_t kPredicateReturn = 0x103;
+constexpr std::uintptr_t kBindReturn = 0x135;
+/** A near call is five bytes: the E8 sits five before its return address. */
+constexpr std::uintptr_t kNearCallBytes = 5;
+constexpr std::byte kCallOpcode{0xE8};
 
 
 struct ActorSource final {
 struct ActorSource final {
     std::uint32_t key{kNone};
     std::uint32_t key{kNone};
@@ -84,19 +94,18 @@ constexpr std::string_view kObserverTraceText =
 /** Compiled form of that pattern; its length fixes the two operands read at +17 and +30. */
 /** Compiled form of that pattern; its length fixes the two operands read at +17 and +30. */
 constexpr auto kObserverTracePattern =
 constexpr auto kObserverTracePattern =
     signature<signature_length(kObserverTraceText)>(kObserverTraceText);
     signature<signature_length(kObserverTraceText)>(kObserverTraceText);
-/** Native entry the rebind pass runs, detoured for the trace. */
+/**
+ * Native entry the rebind pass runs, detoured for the trace. A sibling body shares its first 61
+ * bytes, so the pattern runs past the first branch. The iterator it calls has no unique shape
+ * of its own and is decoded from the call at `kIteratorReturn`.
+ */
 constexpr std::string_view kRebindTraceText =
 constexpr std::string_view kRebindTraceText =
     "48 89 5C 24 10 48 89 7C 24 18 55 48 8D AC 24 30 FF FF FF 48 81 EC D0 01 00 00 48 8B 05 ? ? ? "
     "48 89 5C 24 10 48 89 7C 24 18 55 48 8D AC 24 30 FF FF FF 48 81 EC D0 01 00 00 48 8B 05 ? ? ? "
-    "? 48 33 C4 48 89 85 C0 00 00 00 33 FF 48 8D 4C 24 40 33 D2 89 7C 24 70 E8 E3 47 59 00";
+    "? 48 33 C4 48 89 85 C0 00 00 00 33 FF 48 8D 4C 24 40 33 D2 89 7C 24 70 E8 ? ? ? ? 48 8D 4C "
+    "24 40 E8 ? ? ? ? 84 C0 0F 84 1E 01 00 00 90 48 8D 54 24 24 48 8D 4C 24 40 E8 ? ? ? ?";
 /** Compiled form of that pattern; the scan requires one match. */
 /** Compiled form of that pattern; the scan requires one match. */
 constexpr auto kRebindTracePattern =
 constexpr auto kRebindTracePattern =
     signature<signature_length(kRebindTraceText)>(kRebindTraceText);
     signature<signature_length(kRebindTraceText)>(kRebindTraceText);
-/** Native iterator the rebind pass calls directly, detoured for the trace. */
-constexpr std::string_view kIteratorTraceText =
-    "40 53 48 83 EC 20 48 8B DA E8 82 7D FB FF 48 8B C3 48 83 C4 20 5B C3";
-/** Compiled form of that pattern; the scan requires one match. */
-constexpr auto kIteratorTracePattern =
-    signature<signature_length(kIteratorTraceText)>(kIteratorTraceText);
 /** Native source lookup the rebind pass calls, detoured for the trace. */
 /** Native source lookup the rebind pass calls, detoured for the trace. */
 constexpr std::string_view kSourceTraceText =
 constexpr std::string_view kSourceTraceText =
     "48 89 5C 24 10 56 48 83 EC 20 48 8B F2 8B D9 83 F9 FF 0F 84 8C 00 00 00 48 89 7C 24 30";
     "48 89 5C 24 10 56 48 83 EC 20 48 8B F2 8B D9 83 F9 FF 0F 84 8C 00 00 00 48 89 7C 24 30";
@@ -134,11 +143,10 @@ constexpr std::string_view kActorOwnerTraceText =
 constexpr auto kActorOwnerTracePattern =
 constexpr auto kActorOwnerTracePattern =
     signature<signature_length(kActorOwnerTraceText)>(kActorOwnerTraceText);
     signature<signature_length(kActorOwnerTraceText)>(kActorOwnerTraceText);
 
 
-/** The nine trace signatures, in the order the registry's target list expects them. */
+/** The eight trace signatures, in the order the registry's target list expects them. */
 constexpr std::array kTraceSignatures{
 constexpr std::array kTraceSignatures{
     patterns::Pattern{"squad_trace_observer", kObserverTracePattern},
     patterns::Pattern{"squad_trace_observer", kObserverTracePattern},
     patterns::Pattern{"squad_trace_rebind", kRebindTracePattern},
     patterns::Pattern{"squad_trace_rebind", kRebindTracePattern},
-    patterns::Pattern{"squad_trace_iterator", kIteratorTracePattern},
     patterns::Pattern{"squad_trace_source", kSourceTracePattern},
     patterns::Pattern{"squad_trace_source", kSourceTracePattern},
     patterns::Pattern{"squad_trace_resolveSource", kResolveSourceTracePattern},
     patterns::Pattern{"squad_trace_resolveSource", kResolveSourceTracePattern},
     patterns::Pattern{"squad_trace_predicate", kPredicateTracePattern},
     patterns::Pattern{"squad_trace_predicate", kPredicateTracePattern},
@@ -227,6 +235,17 @@ bool trace_actor_binding(std::uint32_t actor, std::array<std::uint64_t, 2>& outp
                    output);
                    output);
 }
 }
 
 
+/** @return The target of the near call whose return address is `returnOffset`, or null. */
+[[nodiscard]] std::byte* rebind_call_target(std::byte* rebind,
+                                            std::uintptr_t returnOffset) noexcept {
+    std::byte* const site = rebind + returnOffset - kNearCallBytes;
+    std::byte opcode{};
+    if (!read_at(reinterpret_cast<std::uintptr_t>(site), opcode) || opcode != kCallOpcode) {
+        return nullptr;
+    }
+    return patterns::resolve_relative(site + 1, site + kNearCallBytes);
+}
+
 } // namespace
 } // namespace
 
 
 /** @return Signatures the squad rebind and observer trace needs, in resolve order. */
 /** @return Signatures the squad rebind and observer trace needs, in resolve order. */
@@ -234,6 +253,24 @@ std::span<const patterns::Pattern> trace_patterns() noexcept {
     return kTraceSignatures;
     return kTraceSignatures;
 }
 }
 
 
+/** Decodes the iterator from the rebind body, and refuses a body whose other calls moved. */
+bool bind_rebind_calls(std::byte* rebind,
+                       std::byte* source,
+                       std::byte* resolveSource,
+                       std::byte* predicate,
+                       std::byte* bind,
+                       std::byte*& iterator) noexcept {
+    iterator = nullptr;
+    if (rebind == nullptr || rebind_call_target(rebind, kSourceReturn) != source
+        || rebind_call_target(rebind, kResolveReturn) != resolveSource
+        || rebind_call_target(rebind, kPredicateReturn) != predicate
+        || rebind_call_target(rebind, kBindReturn) != bind) {
+        return false;
+    }
+    iterator = rebind_call_target(rebind, kIteratorReturn);
+    return iterator != nullptr;
+}
+
 /** Preserves the observer's original call and records the bubble used by its native gate. */
 /** Preserves the observer's original call and records the bubble used by its native gate. */
 __declspec(noinline) std::uintptr_t __fastcall trace_observer(void* observer,
 __declspec(noinline) std::uintptr_t __fastcall trace_observer(void* observer,
                                                               void* activity,
                                                               void* activity,
@@ -293,12 +330,13 @@ __declspec(noinline) std::uintptr_t __fastcall trace_iterator(void* iterator,
     const auto result = original ? original(iterator, output) : 0;
     const auto result = original ? original(iterator, output) : 0;
     auto* value = t_rebindTrace;
     auto* value = t_rebindTrace;
     const auto caller = reinterpret_cast<std::uintptr_t>(_ReturnAddress());
     const auto caller = reinterpret_cast<std::uintptr_t>(_ReturnAddress());
-    if (value && value->association.active && caller == g_rebindAddress + 0x5F
+    if (value && value->association.active && caller == g_rebindAddress + kIteratorReturn
         && (value->association.iterator == 0
         && (value->association.iterator == 0
             || value->association.iterator == reinterpret_cast<std::uintptr_t>(iterator))) {
             || value->association.iterator == reinterpret_cast<std::uintptr_t>(iterator))) {
         flush_rebind_actor(*value);
         flush_rebind_actor(*value);
-        if (value->association.visit(
-                caller, g_rebindAddress + 0x5F, reinterpret_cast<std::uintptr_t>(iterator))) {
+        if (value->association.visit(caller,
+                                     g_rebindAddress + kIteratorReturn,
+                                     reinterpret_cast<std::uintptr_t>(iterator))) {
             value->source = {};
             value->source = {};
             value->sourceResult = value->resolveResult = value->predicate = -1;
             value->sourceResult = value->resolveResult = value->predicate = -1;
             value->countBefore = value->countAfter = -1;
             value->countBefore = value->countAfter = -1;
@@ -325,7 +363,7 @@ __declspec(noinline) std::uint8_t __fastcall trace_source(std::uint32_t owner, v
     const std::uint8_t result = original ? original(owner, output) : std::uint8_t{};
     const std::uint8_t result = original ? original(owner, output) : std::uint8_t{};
     auto* value = t_rebindTrace;
     auto* value = t_rebindTrace;
     if (value && value->association.active && value->association.actor != kNone
     if (value && value->association.active && value->association.actor != kNone
-        && reinterpret_cast<std::uintptr_t>(_ReturnAddress()) == g_rebindAddress + 0x83) {
+        && reinterpret_cast<std::uintptr_t>(_ReturnAddress()) == g_rebindAddress + kSourceReturn) {
         value->association.owner = owner;
         value->association.owner = owner;
         value->sourceResult = result;
         value->sourceResult = result;
         value->flagsKnown = trace_flags(owner, value->flags);
         value->flagsKnown = trace_flags(owner, value->flags);
@@ -344,7 +382,7 @@ __declspec(noinline) std::uint8_t __fastcall trace_resolve(const void* source, v
     auto* value = t_rebindTrace;
     auto* value = t_rebindTrace;
     ActorSource actual{};
     ActorSource actual{};
     if (value && value->association.active && value->sourceKnown
     if (value && value->association.active && value->sourceKnown
-        && reinterpret_cast<std::uintptr_t>(_ReturnAddress()) == g_rebindAddress + 0xA5
+        && reinterpret_cast<std::uintptr_t>(_ReturnAddress()) == g_rebindAddress + kResolveReturn
         && read_value(static_cast<const ActorSource*>(source), actual)
         && read_value(static_cast<const ActorSource*>(source), actual)
         && actual.key == value->source.key && actual.type == value->source.type
         && actual.key == value->source.key && actual.type == value->source.type
         && actual.index == value->source.index) {
         && actual.index == value->source.index) {
@@ -360,8 +398,9 @@ __declspec(noinline) std::uintptr_t __fastcall trace_predicate(std::uint32_t own
     const auto result = original ? original(owner) : 0;
     const auto result = original ? original(owner) : 0;
     auto* value = t_rebindTrace;
     auto* value = t_rebindTrace;
     if (value
     if (value
-        && value->association.matches(
-            reinterpret_cast<std::uintptr_t>(_ReturnAddress()), g_rebindAddress + 0x103, owner)) {
+        && value->association.matches(reinterpret_cast<std::uintptr_t>(_ReturnAddress()),
+                                      g_rebindAddress + kPredicateReturn,
+                                      owner)) {
         value->predicate = static_cast<std::uint8_t>(result);
         value->predicate = static_cast<std::uint8_t>(result);
     }
     }
     return result;
     return result;
@@ -374,7 +413,7 @@ __declspec(noinline) void __fastcall trace_bind(void* squad, std::uint32_t actor
     auto* value = t_rebindTrace;
     auto* value = t_rebindTrace;
     const bool selected =
     const bool selected =
         value && value->association.active && value->association.actor == actor
         value && value->association.active && value->association.actor == actor
-        && reinterpret_cast<std::uintptr_t>(_ReturnAddress()) == g_rebindAddress + 0x135;
+        && reinterpret_cast<std::uintptr_t>(_ReturnAddress()) == g_rebindAddress + kBindReturn;
     const auto count = reinterpret_cast<std::uintptr_t>(squad) + kSquadCountOffset;
     const auto count = reinterpret_cast<std::uintptr_t>(squad) + kSquadCountOffset;
     if (selected) {
     if (selected) {
         value->bindCalled = true;
         value->bindCalled = true;

+ 34 - 21
Sunrise/src/client/hooks/world_objects/world_object_registry.cpp

@@ -40,7 +40,7 @@ constexpr std::size_t kRegistryMask = kRegistryCapacity - 1;
 static_assert((kRegistryCapacity & kRegistryMask) == 0);
 static_assert((kRegistryCapacity & kRegistryMask) == 0);
 
 
 /** Placement, entity and trace signatures resolved together in one image pass. */
 /** Placement, entity and trace signatures resolved together in one image pass. */
-constexpr std::size_t kTargetCount = 21;
+constexpr std::size_t kTargetCount = 20;
 
 
 /** Native entry that instantiates a placed world object. */
 /** Native entry that instantiates a placed world object. */
 constexpr std::string_view kInstantiateSignatureText =
 constexpr std::string_view kInstantiateSignatureText =
@@ -335,13 +335,33 @@ struct Targets final {
             return false;
             return false;
         }
         }
     }
     }
-    output = {matches[0].address,  matches[1].address,  matches[2].address,  matches[3].address,
-              matches[4].address,  matches[5].address,  matches[6].address,  matches[7].address,
-              matches[8].address,  matches[9].address,  matches[10].address, matches[11].address,
-              matches[12].address, matches[13].address, matches[14].address, matches[15].address,
-              matches[16].address, matches[17].address, matches[18].address, matches[19].address,
-              matches[20].address};
-    return true;
+    output.instantiate = matches[0].address;
+    output.destroy = matches[1].address;
+    output.allocate = matches[2].address;
+    output.logicalDestroy = matches[3].address;
+    output.resolvePair = matches[4].address;
+    output.validatePair = matches[5].address;
+    output.datumLayout = matches[6].address;
+    output.createEntity = matches[7].address;
+    output.purgeEntities = matches[8].address;
+    output.glueMapping = matches[9].address;
+    output.entityPool = matches[10].address;
+    output.entityPolicy = matches[11].address;
+    output.observer = matches[12].address;
+    output.rebind = matches[13].address;
+    output.source = matches[14].address;
+    output.resolveSource = matches[15].address;
+    output.predicate = matches[16].address;
+    output.bind = matches[17].address;
+    output.teardown = matches[18].address;
+    output.actorOwner = matches[19].address;
+    // The iterator comes from the rebind body, which also proves the trace's return offsets.
+    return bind_rebind_calls(output.rebind,
+                             output.source,
+                             output.resolveSource,
+                             output.predicate,
+                             output.bind,
+                             output.iterator);
 }
 }
 
 
 /** Binds the two datum globals encoded at fixed operands in the checked layout signature. */
 /** Binds the two datum globals encoded at fixed operands in the checked layout signature. */
@@ -468,18 +488,13 @@ bool read_datum_identity(std::uint32_t handle, DatumIdentity& output) noexcept {
 /** Installs the generation-checked placed-object lifetime capture. */
 /** Installs the generation-checked placed-object lifetime capture. */
 bool install() noexcept {
 bool install() noexcept {
     AcquireSRWLockExclusive(&g_lock);
     AcquireSRWLockExclusive(&g_lock);
-    if (g_handles[0].attached && g_handles[1].attached && g_handles[2].attached
-        && g_handles[3].attached && g_handles[4].attached && g_handles[5].attached
-        && std::all_of(
-            g_handles.begin() + 6, g_handles.end(), [](const auto& h) { return h.attached; })) {
+    const auto attached = [](const auto& handle) { return handle.attached; };
+    if (std::all_of(g_handles.begin(), g_handles.end(), attached)) {
         const bool accepting = g_accepting.load(std::memory_order_acquire);
         const bool accepting = g_accepting.load(std::memory_order_acquire);
         ReleaseSRWLockExclusive(&g_lock);
         ReleaseSRWLockExclusive(&g_lock);
         return accepting;
         return accepting;
     }
     }
-    if (g_handles[0].attached || g_handles[1].attached || g_handles[2].attached
-        || g_handles[3].attached || g_handles[4].attached || g_handles[5].attached
-        || std::any_of(
-            g_handles.begin() + 6, g_handles.end(), [](const auto& h) { return h.attached; })) {
+    if (std::any_of(g_handles.begin(), g_handles.end(), attached)) {
         ReleaseSRWLockExclusive(&g_lock);
         ReleaseSRWLockExclusive(&g_lock);
         return false;
         return false;
     }
     }
@@ -516,7 +531,6 @@ bool install() noexcept {
         hooking::detour::Spec{targets.predicate, reinterpret_cast<void*>(&trace_predicate)},
         hooking::detour::Spec{targets.predicate, reinterpret_cast<void*>(&trace_predicate)},
         hooking::detour::Spec{targets.bind, reinterpret_cast<void*>(&trace_bind)},
         hooking::detour::Spec{targets.bind, reinterpret_cast<void*>(&trace_bind)},
         hooking::detour::Spec{targets.teardown, reinterpret_cast<void*>(&trace_teardown)},
         hooking::detour::Spec{targets.teardown, reinterpret_cast<void*>(&trace_teardown)},
-
     };
     };
     if (!hooking::detour::install(specs, g_handles)) {
     if (!hooking::detour::install(specs, g_handles)) {
         g_resolvePair = nullptr;
         g_resolvePair = nullptr;
@@ -585,10 +599,9 @@ bool install() noexcept {
 /** Removes both lifetime hooks only after native calls have left their trampolines. */
 /** Removes both lifetime hooks only after native calls have left their trampolines. */
 bool uninstall() noexcept {
 bool uninstall() noexcept {
     AcquireSRWLockExclusive(&g_lock);
     AcquireSRWLockExclusive(&g_lock);
-    if (!g_handles[0].attached && !g_handles[1].attached && !g_handles[2].attached
-        && !g_handles[3].attached && !g_handles[4].attached && !g_handles[5].attached
-        && std::none_of(
-            g_handles.begin() + 6, g_handles.end(), [](const auto& h) { return h.attached; })) {
+    if (std::none_of(g_handles.begin(), g_handles.end(), [](const auto& handle) {
+            return handle.attached;
+        })) {
         clear_registry();
         clear_registry();
         ReleaseSRWLockExclusive(&g_lock);
         ReleaseSRWLockExclusive(&g_lock);
         return true;
         return true;

+ 43 - 2
Sunrise/src/client/runtime/client_runtime_lifecycle.cpp

@@ -9,9 +9,12 @@
 #include "../hooks/bitmap/bitmap_hook_lifecycle.h"
 #include "../hooks/bitmap/bitmap_hook_lifecycle.h"
 #include "../hooks/bootflow/bootflow_hook_lifecycle.h"
 #include "../hooks/bootflow/bootflow_hook_lifecycle.h"
 #include "../hooks/bootflow/bootflow_texture_override.h"
 #include "../hooks/bootflow/bootflow_texture_override.h"
+#include "../hooks/cine_auth_probe/cine_auth_probe.h"
+#include "../hooks/cine_probe/cine_probe.h"
 #include "../hooks/config_getter/config_getter_lifecycle.h"
 #include "../hooks/config_getter/config_getter_lifecycle.h"
 #include "../hooks/cursor/runtime.h"
 #include "../hooks/cursor/runtime.h"
 #include "../hooks/graphics/graphics_hook_lifecycle.h"
 #include "../hooks/graphics/graphics_hook_lifecycle.h"
+#include "../hooks/hitch_probe/hitch_probe.h"
 #include "../hooks/inactivity/inactivity_override.h"
 #include "../hooks/inactivity/inactivity_override.h"
 #include "../hooks/infinite_ammo/infinite_ammo.h"
 #include "../hooks/infinite_ammo/infinite_ammo.h"
 #include "../hooks/membership_probe/membership_probe.h"
 #include "../hooks/membership_probe/membership_probe.h"
@@ -21,6 +24,8 @@
 #include "../hooks/polled_input/runtime.h"
 #include "../hooks/polled_input/runtime.h"
 #include "../hooks/queuez/queuez_hook_lifecycle.h"
 #include "../hooks/queuez/queuez_hook_lifecycle.h"
 #include "../hooks/retail_log/retail_log_lifecycle.h"
 #include "../hooks/retail_log/retail_log_lifecycle.h"
+#include "../hooks/sense_chain_guard/sense_chain_guard.h"
+#include "../hooks/stall_probe/stall_probe.h"
 #include "../hooks/teleport/runtime.h"
 #include "../hooks/teleport/runtime.h"
 #include "../hooks/world_objects/world_object_registry.h"
 #include "../hooks/world_objects/world_object_registry.h"
 #include "../movement/movement_settings_store.h"
 #include "../movement/movement_settings_store.h"
@@ -86,8 +91,22 @@ bool shutdown() noexcept {
         ReleaseSRWLockExclusive(&runtime::g_lock);
         ReleaseSRWLockExclusive(&runtime::g_lock);
         return false;
         return false;
     }
     }
-    // Attached last, so it detaches first. The probe reads through a detour, so one left in
-    // place is a branch into code a later unload unmaps.
+    // Every probe below reads through a detour, so one left in place is a branch into code a
+    // later unload unmaps.
+    if (!hooks::cine_auth_probe::uninstall()) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=shutdown stage=cine_auth_probe result=fail");
+        ReleaseSRWLockExclusive(&runtime::g_lock);
+        return false;
+    }
+    if (!hooks::cine_probe::uninstall()) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=shutdown stage=cine_probe result=fail");
+        ReleaseSRWLockExclusive(&runtime::g_lock);
+        return false;
+    }
     if (!hooks::membership_probe::uninstall()) {
     if (!hooks::membership_probe::uninstall()) {
         core::log::write(core::log::Channel::client,
         core::log::write(core::log::Channel::client,
                          core::log::Level::error,
                          core::log::Level::error,
@@ -95,6 +114,28 @@ bool shutdown() noexcept {
         ReleaseSRWLockExclusive(&runtime::g_lock);
         ReleaseSRWLockExclusive(&runtime::g_lock);
         return false;
         return false;
     }
     }
+    if (!hooks::sense_chain_guard::uninstall()) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=shutdown stage=sense_chain_guard result=fail");
+        ReleaseSRWLockExclusive(&runtime::g_lock);
+        return false;
+    }
+    // The watcher runs on its own thread, so it stops before the targets it reads are cleared.
+    if (!hooks::stall_probe::uninstall()) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=shutdown stage=stall_probe result=fail");
+        ReleaseSRWLockExclusive(&runtime::g_lock);
+        return false;
+    }
+    if (!hooks::hitch_probe::uninstall()) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=shutdown stage=hitch_probe result=fail");
+        ReleaseSRWLockExclusive(&runtime::g_lock);
+        return false;
+    }
     hooks::bitmap::uninstall();
     hooks::bitmap::uninstall();
     hooks::bootflow::uninstall();
     hooks::bootflow::uninstall();
     hooks::infinite_ammo::uninstall();
     hooks::infinite_ammo::uninstall();