stan 2 روز پیش
والد
کامیت
d7d330e4a1
32فایلهای تغییر یافته به همراه245 افزوده شده و 1126 حذف شده
  1. 0 2
      Sunrise/Sunrise.vcxproj
  2. 0 1
      Sunrise/resources/default_settings.json
  3. 1 1
      Sunrise/src/client/content/activity/activity_sdk_generation_worker.cpp
  4. 0 1
      Sunrise/src/client/content/activity/activity_sdk_generation_worker.h
  5. 39 3
      Sunrise/src/client/content/items/packages/internal.h
  6. 11 0
      Sunrise/src/client/content/items/packages/package_item_build.cpp
  7. 1 40
      Sunrise/src/client/content/items/packages/package_item_rows.cpp
  8. 45 61
      Sunrise/src/client/content/items/packages/package_node_build.cpp
  9. 12 82
      Sunrise/src/client/content/items/packages/package_record_build.cpp
  10. 5 53
      Sunrise/src/client/content/items/packages/package_season_pass_build.cpp
  11. 32 220
      Sunrise/src/client/hooks/inactivity/inactivity_override.cpp
  12. 3 36
      Sunrise/src/client/hooks/inactivity/inactivity_override.h
  13. 0 273
      Sunrise/src/client/inactivity/inactivity_settings_store.cpp
  14. 0 99
      Sunrise/src/client/inactivity/inactivity_settings_store.h
  15. 6 3
      Sunrise/src/client/player/player_settings_store.cpp
  16. 2 0
      Sunrise/src/client/player/player_settings_store.h
  17. 2 7
      Sunrise/src/client/runtime/client_runtime_lifecycle.cpp
  18. 11 156
      Sunrise/src/client/ui/player/player_panel.cpp
  19. 2 4
      Sunrise/src/core/settings/settings.h
  20. 2 8
      Sunrise/src/core/settings/settings_parser.cpp
  21. 3 2
      Sunrise/src/core/settings/settings_upgrade.cpp
  22. 2 4
      Sunrise/src/server/bap/encrypted/push/activity/activity_roster_push.cpp
  23. 3 5
      Sunrise/src/server/bap/encrypted/push/activity/activity_roster_snapshot.cpp
  24. 1 1
      Sunrise/src/server/bap/encrypted/push/activity/internal.h
  25. 2 4
      Sunrise/src/server/gameplay/peer/peer_out_of_band.cpp
  26. 0 1
      Sunrise/src/state/build_data/cache/records/format.h
  27. 0 6
      Sunrise/src/state/build_data/items/catalysts/definition.h
  28. 36 34
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.cpp
  29. 2 2
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.h
  30. 16 13
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.cpp
  31. 3 4
      Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.h
  32. 3 0
      Sunrise/src/state/runtime/state_progression_runtime.cpp

+ 0 - 2
Sunrise/Sunrise.vcxproj

@@ -238,7 +238,6 @@
     <ClCompile Include="src\client\ui\runtime\client_ui_module_runtime.cpp" />
     <ClCompile Include="src\client\ui\movement\movement_panel.cpp" />
     <ClCompile Include="src\client\movement\movement_settings_store.cpp" />
-    <ClCompile Include="src\client\inactivity\inactivity_settings_store.cpp" />
     <ClCompile Include="src\client\hooks\inactivity\inactivity_override.cpp" />
     <ClCompile Include="src\client\ui\player\player_panel.cpp" />
     <ClCompile Include="src\client\player\player_position.cpp" />
@@ -1318,7 +1317,6 @@
     <ClInclude Include="src\core\ui\layout\credits\sunrise_credits_badge.h" />
     <ClInclude Include="src\client\runtime\runtime.h" />
     <ClInclude Include="src\client\movement\movement_settings_store.h" />
-    <ClInclude Include="src\client\inactivity\inactivity_settings_store.h" />
     <ClInclude Include="src\client\hooks\async_io\async_io_lifetime_guard.h" />
     <ClInclude Include="src\client\hooks\inactivity\inactivity_override.h" />
     <ClInclude Include="src\client\ui\player\player_panel.h" />

+ 0 - 1
Sunrise/resources/default_settings.json

@@ -13,7 +13,6 @@
       }
     },
     "activity_sdk_generation": {
-      "enabled": true,
       "lua_declarations": true
     }
   },

+ 1 - 1
Sunrise/src/client/content/activity/activity_sdk_generation_worker.cpp

@@ -573,7 +573,7 @@ void initialize(void* module, const Policy& policy) noexcept {
     AcquireSRWLockExclusive(&g_lock);
     g_configured = resolved && !sdkDirectory.empty() && !scenarioDirectory.empty()
                    && !catalogPath.empty() && !packPath.empty();
-    g_enabled = policy.enabled && g_configured;
+    g_enabled = g_configured;
     g_luaDeclarations = policy.luaDeclarations;
     g_started = false;
     g_packageReadyAfter = 0;

+ 0 - 1
Sunrise/src/client/content/activity/activity_sdk_generation_worker.h

@@ -64,7 +64,6 @@ struct OfflineBuildResult final {
 
 /** Immutable boot policy for the live generator. Its pack backs host roster mission seeds. */
 struct Policy final {
-    bool enabled{};
     /** Writes the sdk/lua declaration tree, which no runtime loads. */
     bool luaDeclarations{};
 };

+ 39 - 3
Sunrise/src/client/content/items/packages/internal.h

@@ -56,9 +56,33 @@ struct DetailSource {
 /** The container name is not always unique, so every match is a candidate. */
 inline constexpr std::size_t kContainerCandidates = 16;
 
+/** A slot is a signed 16-bit value, so the widest addressable slot space is this. */
+inline constexpr std::size_t kSlotSpace = 32768;
+
+/** Value of an unmapped slot. Every bank domain uses 0xFFFF as its unavailable index. */
+inline constexpr std::uint16_t kUnmappedSlot = 0xFFFFU;
+static_assert(kUnmappedSlot == state::build_data::nodes::kUnavailableValueIndex);
+static_assert(kUnmappedSlot == state::build_data::records::kUnavailableValueIndex);
+static_assert(kUnmappedSlot == state::build_data::season_pass::kUnavailableFlagIndex);
+static_assert(kUnmappedSlot
+              == state::build_data::items::catalysts::kUnavailableCompletionFlagIndex);
+
+/** Bank index per unlock slot, indexed by slot. The first mapping row of a slot wins. */
+using SlotMap = std::array<std::uint16_t, kSlotSpace>;
+
+/** The four maps one root's two unlock mapping tables carry. 256 KiB together. */
+struct SlotMaps {
+    SlotMap accountFlag{};
+    SlotMap characterFlag{};
+    SlotMap accountValue{};
+    SlotMap characterValue{};
+};
+
 /** Lock-owned storage kept off the caller stack, shared by every stage of the pass. */
 struct Storage {
     reader::Scratch scratch{};
+    /** Read once per root. Every domain resolves its unlock slots through these. */
+    SlotMaps slotMaps{};
     /** Node rows held until the value slot and owned records are resolved. */
     std::array<state::build_data::nodes::Definition, state::build_data::nodes::kDefinitionCapacity>
         nodeRows{};
@@ -128,12 +152,10 @@ struct Storage {
                state::build_data::progressions::kStepCapacity>
         progressionSteps{};
     std::size_t progressionStepCount{};
-    /** Season pass reward rows, their raw claim slots, and the wrapper items they grant. */
+    /** Season pass reward rows and the wrapper items they grant. */
     std::array<state::build_data::season_pass::Reward,
                state::build_data::season_pass::kRewardCapacity>
         seasonPassRewards{};
-    std::array<std::uint16_t, state::build_data::season_pass::kRewardCapacity>
-        seasonPassClaimSlots{};
     std::array<state::build_data::season_pass::Package,
                state::build_data::season_pass::kPackageCapacity>
         seasonPassPackages{};
@@ -297,6 +319,20 @@ read_investment_constants(const reader::Source& source,
     std::array<std::uint8_t, state::build_data::socket_entry_lists::kEntryCapacity>&
         output) noexcept;
 
+/**
+ * Reads one root's two unlock mapping tables into the pass slot maps.
+ * @param source Package source.
+ * @param storage Pass storage receiving the four maps.
+ * @param root Investment root bytes.
+ * @return True when both account maps read. A character map may stay unmapped.
+ */
+[[nodiscard]] bool read_unlock_slot_maps(const reader::Source& source,
+                                         Storage& storage,
+                                         std::span<const std::byte> root) noexcept;
+
+/** @param map Slot map. @param slot Raw unlock slot. @return Bank index, or the unmapped one. */
+[[nodiscard]] std::uint16_t bank_index(const SlotMap& map, std::int32_t slot) noexcept;
+
 /**
  * Reads nodes and resolves their value slots, owned records and lore parent bars.
  * The record rows must already be built: a node's lore-book flag and parent bar come from them.

+ 11 - 0
Sunrise/src/client/content/items/packages/package_item_build.cpp

@@ -113,6 +113,17 @@ bool build() noexcept {
             }
             // The same root names the bucket and socket-list tables.
             storage.root = storage.child;
+            // Records, nodes, season pass rewards and catalysts all resolve slots through the
+            // two unlock mapping tables, so they are read once here.
+            if (!state::build_data::record_definitions_ready()
+                || !state::build_data::node_definitions_ready()
+                || !state::build_data::season_pass_ready() || !exotic_catalysts_settled()) {
+                reason = "unlock_maps";
+                if (!read_unlock_slot_maps(
+                        source, storage, std::span<const std::byte>{storage.root})) {
+                    continue;
+                }
+            }
             if (!state::build_data::socket_plug_rules_ready()) {
                 std::uint32_t plugSetTag = 0;
                 tables::Array plugSets{};

+ 1 - 40
Sunrise/src/client/content/items/packages/package_item_rows.cpp

@@ -1,13 +1,9 @@
 #include <array>
-#include <cstring>
 #include <span>
-#include <vector>
 
-#include "../../../../middleware/content/packages/tables/definition_index_table.h"
 #include "../../../../state/build_data/items/catalysts/exotic_catalyst_builder.h"
 #include "../../../../state/build_data/items/details/item_detail_catalog.h"
 #include "../../../../state/build_data/runtime.h"
-#include "../../../../state/unlocks/definition.h"
 #include "internal.h"
 #include "package_socket_plug_build.h"
 
@@ -24,35 +20,6 @@ namespace build_items = state::build_data::items;
  */
 bool g_catalystsUnsupported = false;
 
-/** Reads the package's account flag-map table into its own blob, leaving the item blobs alone. */
-[[nodiscard]] bool read_catalyst_account_mappings(
-    const reader::Source& source,
-    Storage& storage,
-    std::vector<build_items::catalysts::AccountFlagMapping>& output) noexcept {
-    std::uint32_t tag = 0;
-    tables::Array rows{};
-    std::vector<std::byte> blob;
-    if (!tables::slot_tag(storage.root, tables::kUnlockFlagMapTableSlot, tag) || tag == 0
-        || tables::package_of(tag) == tables::kAbsentPackageId
-        || !reader::read_tag(source, storage.scratch, tag, blob)
-        || !tables::find_array_at(blob, tables::kAccountFlagMapDescriptor, rows) || rows.count == 0
-        || rows.count > state::unlocks::kAccountFlagCapacity || rows.dataOffset > blob.size()
-        || rows.count > (blob.size() - rows.dataOffset) / tables::kUnlockMapRowStride) {
-        return false;
-    }
-    output.clear();
-    for (std::size_t row = 0; row < rows.count; ++row) {
-        std::int16_t slot = -1;
-        const auto at = rows.dataOffset + row * tables::kUnlockMapRowStride
-                        + tables::kUnlockMapDestinationSlotOffset;
-        std::memcpy(&slot, blob.data() + at, sizeof slot);
-        if (slot >= 0) {
-            output.push_back({static_cast<std::uint16_t>(slot), static_cast<std::uint16_t>(row)});
-        }
-    }
-    return true;
-}
-
 /** @return True when one extracted detail can join the currently published numeric domains. */
 [[nodiscard]] bool publishable_detail(const build_details::Definition& detail) noexcept {
     const std::size_t itemCount = state::build_data::item_definition_count();
@@ -227,12 +194,6 @@ bool build_item_rows(const reader::Source& source,
             catalystRows{};
         std::size_t catalystCount = 0;
         state::build_data::items::catalysts::Report catalystReport{};
-        std::vector<state::build_data::items::catalysts::AccountFlagMapping> catalystMappings;
-        if (published && needCatalysts
-            && !read_catalyst_account_mappings(source, storage, catalystMappings)) {
-            reason = "catalyst_account_mapping";
-            return false;
-        }
         const state::build_data::items::catalysts::Source catalystSource{
             {},
             std::span(storage.rows).first(rowCount),
@@ -243,7 +204,7 @@ bool build_item_rows(const reader::Source& source,
             storage.catalystCompletionConditions,
             storage.catalystAcquisitionGates,
             storage.catalystObjectiveValues,
-            catalystMappings,
+            storage.slotMaps.accountFlag,
         };
         bool catalystBuilt = false;
         if (published && needCatalysts) {

+ 45 - 61
Sunrise/src/client/content/items/packages/package_node_build.cpp

@@ -25,26 +25,9 @@ void report(const char* stage, unsigned long long detail) noexcept {
     }
 }
 
-/** A slot is a signed 16-bit value, so the widest addressable slot space is this. */
-constexpr std::size_t kSlotSpace = 32768;
-
-/** Unlock slot to bank mapping row, indexed by slot. The first mapping row of a slot wins. */
-using SlotMap = std::array<std::uint16_t, kSlotSpace>;
-
-/** The four maps one node pass reads. 256 KiB together, so they never sit on the caller stack. */
-struct SlotMaps {
-    SlotMap accountFlag{};
-    SlotMap characterFlag{};
-    SlotMap accountValue{};
-    SlotMap characterValue{};
-};
-
-/** Held here because the pass owns the build-data lock and runs once per process. */
-SlotMaps g_slotMaps{};
-
-/** Clears one map so every slot reads as unavailable. */
+/** Clears one map so every slot reads as unmapped. */
 void clear_slot_map(SlotMap& output) noexcept {
-    output.fill(domain::kUnavailableValueIndex);
+    output.fill(kUnmappedSlot);
 }
 
 /** Clears all four so an unread table cannot leave a prior pass's indexes addressable. */
@@ -71,7 +54,7 @@ read_slot_map(std::span<const std::byte> blob, std::size_t descriptor, SlotMap&
                > blob.size()) {
         return false;
     }
-    for (std::uint64_t row = 0; row < rows.count && row <= domain::kUnavailableValueIndex; ++row) {
+    for (std::uint64_t row = 0; row < rows.count && row < kUnmappedSlot; ++row) {
         std::int16_t slot = 0;
         std::memcpy(&slot,
                     blob.data() + rows.dataOffset
@@ -83,21 +66,13 @@ read_slot_map(std::span<const std::byte> blob, std::size_t descriptor, SlotMap&
             continue;
         }
         std::uint16_t& existing = output[static_cast<std::size_t>(slot)];
-        if (existing == domain::kUnavailableValueIndex) {
+        if (existing == kUnmappedSlot) {
             existing = static_cast<std::uint16_t>(row);
         }
     }
     return true;
 }
 
-/** @param map Slot map. @param slot Raw unlock slot. @return Bank index, or the unavailable one. */
-[[nodiscard]] std::uint16_t bank_index(const SlotMap& map, std::int16_t slot) noexcept {
-    if (slot < 0) {
-        return domain::kUnavailableValueIndex;
-    }
-    return map[static_cast<std::size_t>(slot)];
-}
-
 /**
  * Resolves one node's lore-book flag and the value slot its parent bar reads.
  * A book's parent triumph is the one child record that displays no lore. Its first objective
@@ -135,39 +110,48 @@ read_slot_map(std::span<const std::byte> blob, std::size_t descriptor, SlotMap&
 
 } // namespace
 
-/** Reads nodes and resolves their value slots, owned records and lore parent bars. */
-bool build_nodes(const reader::Source& source,
-                 Storage& storage,
-                 std::span<const std::byte> root) noexcept {
-    storage.nodeCount = 0;
-    clear_slot_maps(g_slotMaps);
-
-    // A gate names a flag slot, not an index. The index is the mapping row whose destination is
-    // that slot.
+/** Reads both unlock mapping tables. A gate names a slot; the bank index is the row naming it. */
+bool read_unlock_slot_maps(const reader::Source& source,
+                           Storage& storage,
+                           std::span<const std::byte> root) noexcept {
+    SlotMaps& maps = storage.slotMaps;
+    clear_slot_maps(maps);
     std::uint32_t flagMapTag = 0;
-    if (tables::slot_tag(root, tables::kUnlockFlagMapTableSlot, flagMapTag) && flagMapTag != 0
-        && tables::package_of(flagMapTag) != tables::kAbsentPackageId
-        && reader::read_tag(source, storage.scratch, flagMapTag, storage.child)) {
-        const std::span<const std::byte> blob{storage.child};
-        (void)read_slot_map(blob, tables::kAccountFlagMapDescriptor, g_slotMaps.accountFlag);
-        (void)read_slot_map(blob, tables::kCharacterFlagMapDescriptor, g_slotMaps.characterFlag);
+    if (!tables::slot_tag(root, tables::kUnlockFlagMapTableSlot, flagMapTag) || flagMapTag == 0
+        || tables::package_of(flagMapTag) == tables::kAbsentPackageId
+        || !reader::read_tag(source, storage.scratch, flagMapTag, storage.child)) {
+        return false;
     }
-
-    std::uint32_t mapTag = 0;
-    if (!tables::slot_tag(root, tables::kUnlockValueMapTableSlot, mapTag) || mapTag == 0
-        || tables::package_of(mapTag) == tables::kAbsentPackageId
-        || !reader::read_tag(source, storage.scratch, mapTag, storage.child)) {
-        report("value_map_fail", mapTag);
+    const std::span<const std::byte> flagMap{storage.child};
+    const bool flagMapRead =
+        read_slot_map(flagMap, tables::kAccountFlagMapDescriptor, maps.accountFlag);
+    (void)read_slot_map(flagMap, tables::kCharacterFlagMapDescriptor, maps.characterFlag);
+
+    std::uint32_t valueMapTag = 0;
+    if (!flagMapRead || !tables::slot_tag(root, tables::kUnlockValueMapTableSlot, valueMapTag)
+        || valueMapTag == 0 || tables::package_of(valueMapTag) == tables::kAbsentPackageId
+        || !reader::read_tag(source, storage.scratch, valueMapTag, storage.child)) {
         return false;
     }
     const std::span<const std::byte> valueMap{storage.child};
     const bool valueMapRead =
-        read_slot_map(valueMap, tables::kAccountValueMapDescriptor, g_slotMaps.accountValue);
-    (void)read_slot_map(valueMap, tables::kCharacterValueMapDescriptor, g_slotMaps.characterValue);
-    if (!valueMapRead) {
-        report("value_map_fail", mapTag);
-        return false;
+        read_slot_map(valueMap, tables::kAccountValueMapDescriptor, maps.accountValue);
+    (void)read_slot_map(valueMap, tables::kCharacterValueMapDescriptor, maps.characterValue);
+    return valueMapRead;
+}
+
+std::uint16_t bank_index(const SlotMap& map, std::int32_t slot) noexcept {
+    if (slot < 0 || static_cast<std::size_t>(slot) >= map.size()) {
+        return kUnmappedSlot;
     }
+    return map[static_cast<std::size_t>(slot)];
+}
+
+/** Reads nodes and resolves their value slots, owned records and lore parent bars. */
+bool build_nodes(const reader::Source& source,
+                 Storage& storage,
+                 std::span<const std::byte> root) noexcept {
+    storage.nodeCount = 0;
 
     std::uint32_t tableTag = 0;
     tables::Array rows{};
@@ -201,10 +185,10 @@ bool build_nodes(const reader::Source& source,
                 table, at, tables::kNodeExpressionFieldAlternate, slot);
         if (named) {
             definition.valueSlot = slot;
-            definition.valueIndex = bank_index(g_slotMaps.accountValue, slot);
+            definition.valueIndex = bank_index(storage.slotMaps.accountValue, slot);
             // One book's bar reads a slot only the character table carries, so both scopes resolve.
             definition.characterValueSlot = slot;
-            definition.characterValueIndex = bank_index(g_slotMaps.characterValue, slot);
+            definition.characterValueIndex = bank_index(storage.slotMaps.characterValue, slot);
         }
 
         // A category gated on a flag never opens from progress alone, so resolve that flag.
@@ -212,9 +196,9 @@ bool build_nodes(const reader::Source& source,
         if (tables::expression_flag_slot(table, at, tables::kNodeExpressionFieldPrimary, gateSlot)
             || tables::expression_flag_slot(
                 table, at, tables::kNodeExpressionFieldAlternate, gateSlot)) {
-            definition.visibilityFlagIndex = bank_index(g_slotMaps.accountFlag, gateSlot);
+            definition.visibilityFlagIndex = bank_index(storage.slotMaps.accountFlag, gateSlot);
             definition.visibilityCharacterFlagIndex =
-                bank_index(g_slotMaps.characterFlag, gateSlot);
+                bank_index(storage.slotMaps.characterFlag, gateSlot);
         }
 
         // Records the node owns, four bytes each as a row and a gate.
@@ -251,9 +235,9 @@ bool build_nodes(const reader::Source& source,
         std::int16_t parentSlot = -1;
         definition.loreBook = resolve_book(storage, definition, parentSlot);
         if (definition.loreBook && parentSlot >= 0) {
-            definition.parentValueIndex = bank_index(g_slotMaps.accountValue, parentSlot);
+            definition.parentValueIndex = bank_index(storage.slotMaps.accountValue, parentSlot);
             definition.parentCharacterValueIndex =
-                bank_index(g_slotMaps.characterValue, parentSlot);
+                bank_index(storage.slotMaps.characterValue, parentSlot);
         }
         books += definition.loreBook ? 1U : 0U;
         ++storage.nodeCount;

+ 12 - 82
Sunrise/src/client/content/items/packages/package_record_build.cpp

@@ -25,61 +25,12 @@ void report(const char* stage, unsigned long long detail) noexcept {
     }
 }
 
-/** A record with no completion flag carries a non-positive slot, which addresses nothing. */
-[[nodiscard]] constexpr bool addressable_slot(std::int16_t slot) noexcept {
-    return slot > 0;
-}
-
-/** A slot is a signed 16-bit value, so the widest addressable slot space is this. */
-constexpr std::size_t kSlotSpace = 32768;
-
 /** A record score is held as 16 bits. */
 constexpr std::uint32_t kScoreCeiling = 0xFFFFU;
 
-/** Unlock slot to bank mapping row, indexed by slot. The first mapping row of a slot wins. */
-using SlotMap = std::vector<std::uint16_t>;
-
-/**
- * Builds one slot-to-bank-index map from an unlock mapping table.
- * @param blob Blob holding the mapping table.
- * @param descriptor Array descriptor of the mapping table inside that blob.
- * @param output Receives one bank index per addressable slot, or the unavailable index.
- * @return True when the table resolves and every row fits the blob.
- */
-[[nodiscard]] bool
-build_slot_map(std::span<const std::byte> blob, std::size_t descriptor, SlotMap& output) noexcept {
-    output.clear();
-    tables::Array rows{};
-    if (!tables::find_array_at(blob, descriptor, rows) || rows.count == 0
-        || rows.dataOffset + static_cast<std::size_t>(rows.count) * tables::kUnlockMapRowStride
-               > blob.size()) {
-        return false;
-    }
-    output.assign(kSlotSpace, domain::kUnavailableValueIndex);
-    for (std::uint64_t row = 0; row < rows.count && row <= domain::kUnavailableValueIndex; ++row) {
-        std::int16_t slot = 0;
-        std::memcpy(&slot,
-                    blob.data() + rows.dataOffset
-                        + static_cast<std::size_t>(row) * tables::kUnlockMapRowStride
-                        + tables::kUnlockMapDestinationSlotOffset,
-                    sizeof slot);
-        if (!addressable_slot(slot)) {
-            continue;
-        }
-        std::uint16_t& existing = output[static_cast<std::size_t>(slot)];
-        if (existing == domain::kUnavailableValueIndex) {
-            existing = static_cast<std::uint16_t>(row);
-        }
-    }
-    return true;
-}
-
-/** @param map Slot map. @param slot Raw unlock slot. @return Bank index, or the unavailable one. */
-[[nodiscard]] std::uint16_t bank_index(const SlotMap& map, std::int32_t slot) noexcept {
-    if (map.empty() || slot <= 0 || static_cast<std::size_t>(slot) >= kSlotSpace) {
-        return domain::kUnavailableValueIndex;
-    }
-    return map[static_cast<std::size_t>(slot)];
+/** @return Bank index of a positive slot. A record with no flag carries a non-positive slot. */
+[[nodiscard]] std::uint16_t record_bank_index(const SlotMap& map, std::int32_t slot) noexcept {
+    return slot > 0 ? bank_index(map, slot) : domain::kUnavailableValueIndex;
 }
 
 /** One located inline array of a definition row. */
@@ -169,32 +120,10 @@ bool build_records(const reader::Source& source,
     storage.recordIntervalCount = 0;
     storage.recordRewardCount = 0;
 
-    // The account flag mapping table, read first because the record rows are matched against it.
-    std::uint32_t mapTag = 0;
-    SlotMap flagIndexBySlot{};
-    if (!tables::slot_tag(root, tables::kUnlockFlagMapTableSlot, mapTag) || mapTag == 0
-        || tables::package_of(mapTag) == tables::kAbsentPackageId
-        || !reader::read_tag(source, storage.scratch, mapTag, storage.child)
-        || !build_slot_map(std::span<const std::byte>{storage.child},
-                           tables::kAccountFlagMapDescriptor,
-                           flagIndexBySlot)) {
-        report("flag_map_fail", mapTag);
-        return false;
-    }
-
-    // The account value mapping table. A record names value slots for its category, its
-    // redeemed-interval count and each objective's progress source; all become bank indices here.
-    std::uint32_t valueMapTag = 0;
-    SlotMap valueIndexBySlot{};
-    if (!tables::slot_tag(root, tables::kUnlockValueMapTableSlot, valueMapTag) || valueMapTag == 0
-        || tables::package_of(valueMapTag) == tables::kAbsentPackageId
-        || !reader::read_tag(source, storage.scratch, valueMapTag, storage.child)
-        || !build_slot_map(std::span<const std::byte>{storage.child},
-                           tables::kAccountValueMapDescriptor,
-                           valueIndexBySlot)) {
-        report("value_map_fail", valueMapTag);
-        return false;
-    }
+    // A record names its completion flag, its category and redeemed-count values and each
+    // objective's progress source by slot; the pass slot maps turn them into bank indices.
+    const SlotMap& flagIndexBySlot = storage.slotMaps.accountFlag;
+    const SlotMap& valueIndexBySlot = storage.slotMaps.accountValue;
 
     // The objective table carries every threshold and progress source a record names.
     std::uint32_t objectiveTag = 0;
@@ -283,17 +212,17 @@ bool build_records(const reader::Source& source,
         std::int16_t flagSlot = 0;
         std::memcpy(
             &flagSlot, table.data() + at + tables::kRecordCompletionFlagOffset, sizeof flagSlot);
-        definition.completionFlagIndex = bank_index(flagIndexBySlot, flagSlot);
+        definition.completionFlagIndex = record_bank_index(flagIndexBySlot, flagSlot);
         std::int16_t categorySlot = 0;
         if (tables::expression_value_slot(
                 table, at, tables::kRecordCategoryExpressionField, categorySlot)) {
-            definition.categoryValueIndex = bank_index(valueIndexBySlot, categorySlot);
+            definition.categoryValueIndex = record_bank_index(valueIndexBySlot, categorySlot);
         }
         std::uint16_t redeemedSlot = 0;
         std::memcpy(&redeemedSlot,
                     table.data() + at + tables::kRecordRedeemedCountSlotOffset,
                     sizeof redeemedSlot);
-        definition.redeemedCountValueIndex = bank_index(valueIndexBySlot, redeemedSlot);
+        definition.redeemedCountValueIndex = record_bank_index(valueIndexBySlot, redeemedSlot);
 
         RowArray objectives{};
         RowArray intervals{};
@@ -342,7 +271,8 @@ bool build_records(const reader::Source& source,
             objective.valueIndex = static_cast<std::uint16_t>(valueIndex + entry);
             objective.sourceValueSlot =
                 objective_source_slot(objectiveTable, objectiveRows, objectiveRow);
-            objective.sourceValueIndex = bank_index(valueIndexBySlot, objective.sourceValueSlot);
+            objective.sourceValueIndex =
+                record_bank_index(valueIndexBySlot, objective.sourceValueSlot);
             ++storage.recordObjectiveCount;
         }
 

+ 5 - 53
Sunrise/src/client/content/items/packages/package_season_pass_build.cpp

@@ -23,45 +23,6 @@ read(std::span<const std::byte> blob, std::size_t offset, Value& value) noexcept
     return true;
 }
 
-/**
- * Resolves each reward's claim flag slot to its account flag bank row.
- * The map is walked once: a per-slot table would cost more storage than the pass has rewards.
- * @param blob Blob holding the account flag mapping table.
- * @param rewards Reward rows whose claim slots are already read.
- * @param count Rows in use.
- * @param claimSlots Raw claim slot of each reward row, in the same order.
- * @return True when the mapping table resolves.
- */
-[[nodiscard]] bool resolve_claim_flags(std::span<const std::byte> blob,
-                                       std::span<domain::Reward> rewards,
-                                       std::size_t count,
-                                       std::span<std::uint16_t> claimSlots) noexcept {
-    tables::Array rows{};
-    if (!tables::find_array_at(blob, tables::kAccountFlagMapDescriptor, rows) || rows.count == 0
-        || rows.count > (std::numeric_limits<std::uint16_t>::max)()
-        || rows.dataOffset + static_cast<std::size_t>(rows.count) * tables::kUnlockMapRowStride
-               > blob.size()) {
-        return false;
-    }
-    for (std::uint64_t row = 0; row < rows.count; ++row) {
-        std::int16_t slot = 0;
-        if (!read(blob,
-                  rows.dataOffset + static_cast<std::size_t>(row) * tables::kUnlockMapRowStride
-                      + tables::kUnlockMapDestinationSlotOffset,
-                  slot)
-            || slot <= 0) {
-            continue;
-        }
-        for (std::size_t reward = 0; reward < count; ++reward) {
-            if (claimSlots[reward] == static_cast<std::uint16_t>(slot)
-                && rewards[reward].claimFlagIndex == domain::kUnavailableFlagIndex) {
-                rewards[reward].claimFlagIndex = static_cast<std::uint16_t>(row);
-            }
-        }
-    }
-    return true;
-}
-
 /**
  * Reads the item set one reward's wrapper item opens into.
  * @param definition Whole item definition blob.
@@ -177,8 +138,11 @@ bool build_season_pass(const reader::Source& source,
         reward.itemHash = entry.definitionHash;
         reward.itemIndex = static_cast<std::uint16_t>(itemIndex);
         reward.requiredRank = static_cast<std::uint8_t>(rank);
-        storage.seasonPassClaimSlots[storage.seasonPassRewardCount] =
-            static_cast<std::uint16_t>(claimSlot);
+        // A reward with no claim flag carries slot 0.
+        if (claimSlot != 0) {
+            reward.claimFlagIndex =
+                bank_index(storage.slotMaps.accountFlag, static_cast<std::int32_t>(claimSlot));
+        }
 
         // A wrapper reward opens into a set; a plain reward declares none and keeps zero items.
         // One wrapper can be granted at several ranks, so it is recorded once.
@@ -201,18 +165,6 @@ bool build_season_pass(const reader::Source& source,
         }
         ++storage.seasonPassRewardCount;
     }
-
-    std::uint32_t mapTag = 0;
-    if (!tables::slot_tag(root, tables::kUnlockFlagMapTableSlot, mapTag) || mapTag == 0
-        || !reader::read_tag(source, storage.scratch, mapTag, storage.child)
-        || !resolve_claim_flags(std::span<const std::byte>{storage.child},
-                                storage.seasonPassRewards,
-                                storage.seasonPassRewardCount,
-                                storage.seasonPassClaimSlots)) {
-        storage.seasonPassRewardCount = 0;
-        storage.seasonPassPackageCount = 0;
-        return false;
-    }
     return storage.seasonPassRewardCount != 0;
 }
 

+ 32 - 220
Sunrise/src/client/hooks/inactivity/inactivity_override.cpp

@@ -3,8 +3,8 @@
  *
  * The Client keeps 14 activity timeouts in one live object and ends a session idle for longer.
  * The pointer to that object is obfuscated, so this module resolves the Client's own getter by
- * signature and calls it. Lanes are re-applied on an interval because an activity change
- * re-authors the whole block.
+ * signature and calls it. The block is re-applied on an interval because an activity change
+ * re-authors it.
  */
 
 #include "inactivity_override.h"
@@ -15,26 +15,22 @@
 #include <array>
 #include <cstddef>
 #include <cstdint>
-#include <cstdio>
 #include <string_view>
 
 #include "../../../core/logging/log.h"
-#include "../../inactivity/inactivity_settings_store.h"
 #include "../../patterns/image_scan.h"
+#include "../../player/player_settings_store.h"
 
 namespace sunrise::client::hooks::inactivity {
 namespace {
 
-namespace settings = client::inactivity;
-
 using patterns::scan_main_image_unique;
 using patterns::signature;
 using patterns::signature_length;
 
 /**
- * The activity config getter. Every obfuscated pointer getter shares this prologue, so the load
- * of its own global is the only part that tells this one apart: wildcarding that displacement
- * takes the pattern from one match to 242. It is image-relative, so ASLR does not move it.
+ * 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.
  */
 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 "
@@ -42,77 +38,44 @@ constexpr std::string_view kConfigGetterText =
 /** Compiled pattern bytes of the config getter signature. */
 constexpr auto kConfigGetter = signature<signature_length(kConfigGetterText)>(kConfigGetterText);
 
-/** The controlled player's index, which the idle clock is keyed by. */
-constexpr std::string_view kControlledIndexText = "48 8B 05 ? ? ? ? 8B 80 60 04 00 00 C3";
-/** Compiled pattern bytes of the controlled index signature. */
-constexpr auto kControlledIndex =
-    signature<signature_length(kControlledIndexText)>(kControlledIndexText);
-
-/** The idle clock, in the lanes' own unit. Runs to the 0x1B0 stride, which makes it unique. */
-constexpr std::string_view kIdleClockText =
-    "40 53 48 83 EC 20 48 63 D9 48 8D 0D ? ? ? ? 8B D3 E8 ? ? ? ? 84 C0 74 ? "
-    "48 8B 05 ? ? ? ? 48 69 CB B0 01 00 00";
-/** Compiled pattern bytes of the idle clock signature. */
-constexpr auto kIdleClock = signature<signature_length(kIdleClockText)>(kIdleClockText);
-
-/** The session clock, which is what the grace is measured against. */
-constexpr std::string_view kSessionClockText =
-    "48 83 EC 28 E8 ? ? ? ? 48 85 C0 74 ? 80 3D ? ? ? ? 00 48 89 5C 24 20";
-/** Compiled pattern bytes of the session clock signature. */
-constexpr auto kSessionClock = signature<signature_length(kSessionClockText)>(kSessionClockText);
-
-/**
- * The session grace. Read for the interface and never written, because shortening it can only
- * bring a kick forward. It gates the matchmade lanes only, not orbit.
- */
-constexpr std::size_t kGraceOffset = 0x84;
-
+/** Activity lanes the Client keeps a separate inactivity timeout for. */
+constexpr std::size_t kActivityCount = 14;
 /** Where the lanes start in the object the getter returns. */
 constexpr std::size_t kTimeoutBlockOffset = 0xAC;
+/** Milliseconds every lane is held at. A day outlasts any session. */
+constexpr std::uint32_t kHeldTimeoutMs = 86400000;
 /** Milliseconds between re-applications, so an activity change cannot outlast the hold. */
 constexpr std::uint64_t kHoldIntervalMs = 2000;
 
 /** Fourteen consecutive milliseconds, in block order. */
-using Lanes = std::array<std::uint32_t, settings::kActivityCount>;
+using Lanes = std::array<std::uint32_t, kActivityCount>;
 /** Bytes of the block. */
 constexpr std::size_t kBlockBytes = sizeof(Lanes);
 
+/** @return Every lane at its longest. */
+[[nodiscard]] consteval Lanes held_lanes() noexcept {
+    Lanes values{};
+    values.fill(kHeldTimeoutMs);
+    return values;
+}
+
+/** The block a hold writes. */
+constexpr Lanes kHeldLanes = held_lanes();
+
 /** Returns the activity config object. The pointer in its global is obfuscated, so we call it. */
 using ConfigGetter = std::byte*(__fastcall*)();
-/** Answers -1 when nothing is being controlled, which is not an error and not an index. */
-using IndexGetter = std::int32_t(__fastcall*)();
-using IdleGetter = std::uint64_t(__fastcall*)(std::int32_t);
-using SessionGetter = std::uint64_t(__fastcall*)();
 
 SRWLOCK g_lock{SRWLOCK_INIT};
 ConfigGetter g_getter{};
 std::uint64_t g_nextHoldTick{};
-/** The block this module last wrote. Anything else in the object is the Client's own. */
-Lanes g_applied{};
-bool g_appliedValid{};
 /** The Client's own lanes for the activity in play. */
 Lanes g_captured{};
 bool g_capturedValid{};
 /** Set while a hold is in place, so releasing it writes the captured lanes exactly once. */
 bool g_holding{};
-/** The intent the last poll acted on, so a changed one does not wait for the hold interval. */
-Lanes g_intentLanes{};
+/** The setting the last poll acted on, so a change does not wait for the hold interval. */
 bool g_intentHolding{};
 bool g_intentValid{};
-Lanes g_live{};
-bool g_liveValid{};
-/** Never written, so unlike the lanes there is nothing to capture and put back. */
-std::uint32_t g_liveGrace{};
-bool g_liveGraceValid{};
-
-/** Null is a normal state: a build that does not match still holds its lanes without them. */
-IndexGetter g_indexGetter{};
-IdleGetter g_idleGetter{};
-SessionGetter g_sessionGetter{};
-
-/** Last sample, taken only while a caller is asking for one. */
-Timers g_timers{};
-bool g_timersWanted{};
 
 /**
  * Calls the getter without faulting. The body is obfuscated game code, and it runs before the
@@ -144,19 +107,6 @@ bool g_timersWanted{};
            && read == kBlockBytes;
 }
 
-/**
- * @param object Config object.
- * @param value Receives the milliseconds.
- * @return True when Windows copied it.
- */
-[[nodiscard]] bool read_grace(const std::byte* object, std::uint32_t& value) noexcept {
-    SIZE_T read = 0;
-    return ReadProcessMemory(
-               GetCurrentProcess(), object + kGraceOffset, &value, sizeof value, &read)
-               != FALSE
-           && read == sizeof value;
-}
-
 /**
  * Writes one run of milliseconds into the object.
  * @param object Config object.
@@ -183,32 +133,13 @@ void capture_locked(const Lanes& current) noexcept {
     // the Client's own way of switching that lane off, so it is captured like any other value.
     const bool authored = std::any_of(
         current.begin(), current.end(), [](std::uint32_t value) noexcept { return value != 0; });
-    if (!authored || (g_appliedValid && current == g_applied)) {
+    if (!authored || (g_holding && current == kHeldLanes)) {
         return;
     }
     g_captured = current;
     g_capturedValid = true;
 }
 
-/** @return True while either switch asks for a hold. */
-[[nodiscard]] bool holds(const settings::Settings& configured) noexcept {
-    return configured.enabled || configured.custom;
-}
-
-/**
- * @param configured Current configuration.
- * @return The lanes a hold puts in place.
- */
-[[nodiscard]] Lanes held_lanes(const settings::Settings& configured) noexcept {
-    // A hand-edited file can carry both switches, so the blanket hold wins as the safer one.
-    const bool set = configured.custom && !configured.enabled;
-    Lanes values = set ? configured.timeouts : settings::kDefaultTimeouts;
-    // Orbit is held at its longest whatever the grid or the file carries, because a timeout that
-    // fires there ends a session this Client cannot re-establish.
-    values[settings::kOrbitLane] = settings::kMaximumTimeoutMs;
-    return values;
-}
-
 /**
  * Writes the captured lanes back and ends the hold.
  * @return True when lanes were put back, false when there was no hold to end.
@@ -218,68 +149,9 @@ void capture_locked(const Lanes& current) noexcept {
         return false;
     }
     g_holding = false;
-    g_appliedValid = false;
     return true;
 }
 
-/**
- * Caller holds the lock. Failure is not propagated: a build whose signatures have moved should
- * still install and still hold its lanes.
- */
-void resolve_clocks_locked() noexcept {
-    std::byte* const index =
-        scan_main_image_unique(kControlledIndex, "inactivity_controlled_index");
-    std::byte* const idle = scan_main_image_unique(kIdleClock, "inactivity_idle_clock");
-    std::byte* const session = scan_main_image_unique(kSessionClock, "inactivity_session_clock");
-    if (index == nullptr || idle == nullptr || session == nullptr) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::warn,
-                         "ev=inactivity stage=clocks result=fail");
-        return;
-    }
-    g_indexGetter = reinterpret_cast<IndexGetter>(index);
-    g_idleGetter = reinterpret_cast<IdleGetter>(idle);
-    g_sessionGetter = reinterpret_cast<SessionGetter>(session);
-    core::log::write(
-        core::log::Channel::client, core::log::Level::info, "ev=inactivity stage=clocks result=ok");
-}
-
-/**
- * Refreshes the cached clocks when a caller asked for them. The getters are obfuscated Client
- * code, so they are called with no lock held and guarded against an early frame.
- */
-void sample_timers() noexcept {
-    AcquireSRWLockExclusive(&g_lock);
-    const bool wanted = g_timersWanted;
-    g_timersWanted = false;
-    const IndexGetter index = g_indexGetter;
-    const IdleGetter idle = g_idleGetter;
-    const SessionGetter session = g_sessionGetter;
-    ReleaseSRWLockExclusive(&g_lock);
-    if (!wanted || index == nullptr || idle == nullptr || session == nullptr) {
-        return;
-    }
-    Timers sampled{};
-    __try {
-        const std::int32_t controlled = index();
-        if (controlled >= 0) {
-            sampled.idleMs = idle(controlled);
-            sampled.idleValid = true;
-        }
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        sampled.idleValid = false;
-    }
-    __try {
-        sampled.sessionMs = session();
-        sampled.sessionValid = true;
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        sampled.sessionValid = false;
-    }
-    AcquireSRWLockExclusive(&g_lock);
-    g_timers = sampled;
-    ReleaseSRWLockExclusive(&g_lock);
-}
-
 } // namespace
 
 /** Resolves the activity config getter. */
@@ -298,8 +170,6 @@ bool install() noexcept {
         return false;
     }
     g_getter = reinterpret_cast<ConfigGetter>(match);
-    // Scanned here rather than on first display, because the scan walks the whole image.
-    resolve_clocks_locked();
     ReleaseSRWLockExclusive(&g_lock);
     core::log::write(core::log::Channel::client,
                      core::log::Level::info,
@@ -315,46 +185,29 @@ void uninstall() noexcept {
         (void)release_locked(object);
     }
     g_getter = nullptr;
-    g_indexGetter = nullptr;
-    g_idleGetter = nullptr;
-    g_sessionGetter = nullptr;
     g_nextHoldTick = 0;
-    g_applied = Lanes{};
-    g_appliedValid = false;
     g_captured = Lanes{};
     g_capturedValid = false;
     g_holding = false;
-    g_intentLanes = Lanes{};
     g_intentHolding = false;
     g_intentValid = false;
-    g_live = Lanes{};
-    g_liveValid = false;
-    g_liveGrace = 0;
-    g_liveGraceValid = false;
-    g_timers = Timers{};
-    g_timersWanted = false;
     ReleaseSRWLockExclusive(&g_lock);
 }
 
-/** Holds the configured milliseconds, or puts back the ones the Client authored. */
+/** Holds every lane at its longest, or puts back the ones the Client authored. */
 void poll() noexcept {
-    sample_timers();
-    const settings::Settings configured = settings::get();
-    const bool holding = holds(configured);
-    const Lanes desired = held_lanes(configured);
+    const bool holding = client::player::get().antiAfkEnabled;
     AcquireSRWLockExclusive(&g_lock);
     const std::uint64_t now = GetTickCount64();
-    // A changed intent is the operator waiting on this call, so it does not wait for the interval.
-    const bool changed =
-        !g_intentValid || g_intentHolding != holding || (holding && g_intentLanes != desired);
+    // A changed setting is the operator waiting on this call, so it does not wait for the interval.
+    const bool changed = !g_intentValid || g_intentHolding != holding;
     if (g_getter == nullptr || (now < g_nextHoldTick && !changed)) {
         ReleaseSRWLockExclusive(&g_lock);
         return;
     }
     g_nextHoldTick = now + kHoldIntervalMs;
     // Recorded before the object is reached, so a poll that finds no activity cannot leave the
-    // intent looking changed and skip the interval on every later frame.
-    g_intentLanes = desired;
+    // setting looking changed and skip the interval on every later frame.
     g_intentHolding = holding;
     g_intentValid = true;
     std::byte* const object = config_object();
@@ -363,14 +216,8 @@ void poll() noexcept {
         return;
     }
     if (Lanes current{}; read_block(object, current)) {
-        g_live = current;
-        g_liveValid = true;
         capture_locked(current);
     }
-    if (std::uint32_t grace = 0; read_grace(object, grace)) {
-        g_liveGrace = grace;
-        g_liveGraceValid = true;
-    }
     if (!holding) {
         const bool released = release_locked(object);
         ReleaseSRWLockExclusive(&g_lock);
@@ -384,53 +231,18 @@ void poll() noexcept {
         return;
     }
     // Held rather than written once, because an activity change re-authors these lanes.
-    const bool wrote = write_block(object, desired);
+    const bool wrote = write_block(object, kHeldLanes);
     if (wrote) {
-        g_applied = desired;
-        g_appliedValid = true;
         g_holding = true;
     }
-    // The Client picks its lane at runtime, so the shortest is the only figure that says when a
-    // kick can first happen without naming a lane that may not be in force.
-    const std::uint32_t shortest = *std::min_element(desired.begin(), desired.end());
     ReleaseSRWLockExclusive(&g_lock);
     if (changed) {
         // Only on a change, so a steady hold does not fill the log every interval.
-        std::array<char, 128> line{};
-        const int length =
-            std::snprintf(line.data(),
-                          line.size(),
-                          "ev=inactivity stage=hold mode=%s shortest_ms=%u result=%s",
-                          configured.enabled ? "disable" : "set",
-                          shortest,
-                          wrote ? "ok" : "fail");
-        if (length > 0) {
-            core::log::write(core::log::Channel::client,
-                             core::log::Level::info,
-                             {line.data(), static_cast<std::size_t>(length)});
-        }
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::info,
+                         wrote ? "ev=inactivity stage=hold result=ok"
+                               : "ev=inactivity stage=hold result=fail");
     }
 }
 
-/** Reports what the override reached. */
-Status status() noexcept {
-    Status output{};
-    AcquireSRWLockShared(&g_lock);
-    output.live = g_live;
-    output.liveValid = g_liveValid;
-    output.liveGraceMs = g_liveGrace;
-    output.liveGraceValid = g_liveGraceValid;
-    ReleaseSRWLockShared(&g_lock);
-    return output;
-}
-
-/** Reads the last sample and arms the next one. */
-Timers timers() noexcept {
-    AcquireSRWLockExclusive(&g_lock);
-    g_timersWanted = true;
-    const Timers output = g_timers;
-    ReleaseSRWLockExclusive(&g_lock);
-    return output;
-}
-
 } // namespace sunrise::client::hooks::inactivity

+ 3 - 36
Sunrise/src/client/hooks/inactivity/inactivity_override.h

@@ -1,53 +1,20 @@
 #pragma once
 
-#include <array>
-#include <cstdint>
-
-#include "../../inactivity/inactivity_settings_store.h"
-
 namespace sunrise::client::hooks::inactivity {
 
-/** What the Client is holding in its lanes now, read back rather than assumed. */
-struct Status {
-    std::array<std::uint32_t, client::inactivity::kActivityCount> live{};
-    bool liveValid{};
-    /** Session grace. Zero stops the Client gating on it at all. */
-    std::uint32_t liveGraceMs{};
-    bool liveGraceValid{};
-};
-
-/** The Client's own two clocks. A lane fires once idle passes its milliseconds. */
-struct Timers {
-    /** Input resets this, so it does not track the session and the two can diverge widely. */
-    std::uint64_t idleMs{};
-    std::uint64_t sessionMs{};
-    bool idleValid{};
-    bool sessionValid{};
-};
-
 /**
- * Reads the clocks poll() last sampled and asks it to sample again next frame.
- * A sample enters Client code, so it is taken from poll() and never from a draw.
- * @return The clocks, with a validity flag for each.
- */
-[[nodiscard]] Timers timers() noexcept;
-
-/**
- * Resolves the activity config getter, which the lanes are reached through.
+ * Resolves the activity config getter, which the timeouts are reached through.
  * @return True when it was found.
  */
 [[nodiscard]] bool install() noexcept;
 
-/** Puts the Client's own lanes back and drops the resolved getter. */
+/** Puts the Client's own timeouts back and drops the resolved getter. */
 void uninstall() noexcept;
 
 /**
- * Holds the configured milliseconds, or puts back the ones the Client authored.
+ * Holds every timeout at its longest while the setting is on, or puts the Client's own back.
  * Enters Client code, so call it once a frame from a tick that holds no lock.
  */
 void poll() noexcept;
 
-/** @return A consistent copy of what the override reached. */
-[[nodiscard]] Status status() noexcept;
-
 } // namespace sunrise::client::hooks::inactivity

+ 0 - 273
Sunrise/src/client/inactivity/inactivity_settings_store.cpp

@@ -1,273 +0,0 @@
-/**
- * The inactivity configuration store. It is separate from Core settings because the interface
- * changes these values while the game runs and saves each change at once, which Core settings,
- * read once at boot, do not do.
- */
-
-#include "inactivity_settings_store.h"
-
-#include <Windows.h>
-
-#include <algorithm>
-#include <array>
-#include <cstddef>
-#include <cstdio>
-#include <cstdlib>
-#include <string_view>
-
-#include "../../core/filesystem/path.h"
-#include "../../core/logging/log.h"
-
-namespace sunrise::client::inactivity {
-namespace {
-
-/** The module-owned configuration file, beside the generated settings and logs. */
-constexpr std::wstring_view kFileSuffix = L"\\inactivity.json";
-/** Fourteen scalars and their keys fit well inside this. */
-constexpr std::size_t kFileCapacity = 2048;
-/** Longest scalar accepted from the file. Anything longer is malformed rather than large. */
-constexpr std::size_t kScalarCapacity = 32;
-
-SRWLOCK g_lock{SRWLOCK_INIT};
-Settings g_settings{};
-core::path::Buffer g_path{};
-bool g_pathResolved{};
-
-/** @param settings Candidate configuration. @return True when every lane is in range. */
-[[nodiscard]] bool valid(const Settings& settings) noexcept {
-    return std::all_of(
-        settings.timeouts.begin(), settings.timeouts.end(), [](std::uint32_t value) noexcept {
-            return value >= kMinimumTimeoutMs && value <= kMaximumTimeoutMs;
-        });
-}
-
-/** @param reason Key naming the step that failed. */
-void report_fail(const char* reason) noexcept {
-    std::array<char, 96> line{};
-    const int written = std::snprintf(
-        line.data(), line.size(), "ev=inactivity stage=store 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)});
-    }
-}
-
-/**
- * Finds one key's raw scalar text.
- * @param text Whole document.
- * @param key Quoted key to locate.
- * @param output Receives the text between the colon and the next separator.
- * @return True when the key exists and carries a non-empty value.
- */
-[[nodiscard]] bool
-scalar_for(std::string_view text, std::string_view key, std::string_view& output) noexcept {
-    const std::size_t at = text.find(key);
-    if (at == std::string_view::npos) {
-        return false;
-    }
-    const std::size_t colon = text.find(':', at + key.size());
-    if (colon == std::string_view::npos) {
-        return false;
-    }
-    std::size_t begin = colon + 1;
-    while (begin < text.size() && (text[begin] == ' ' || text[begin] == '\t')) {
-        ++begin;
-    }
-    std::size_t end = begin;
-    while (end < text.size() && text[end] != ',' && text[end] != '}' && text[end] != '\n'
-           && text[end] != '\r') {
-        ++end;
-    }
-    output = text.substr(begin, end - begin);
-    return !output.empty();
-}
-
-/**
- * Copies one scalar into null-terminated storage the C conversions require.
- * @param value Scalar text taken from the document.
- * @param output Receives the terminated copy.
- * @return True when the scalar fits.
- */
-[[nodiscard]] bool terminated(std::string_view value,
-                              std::array<char, kScalarCapacity>& output) noexcept {
-    if (value.size() >= output.size()) {
-        return false;
-    }
-    for (std::size_t index = 0; index < value.size(); ++index) {
-        output[index] = value[index];
-    }
-    output[value.size()] = '\0';
-    return true;
-}
-
-/**
- * Layers one document over the current defaults. A missing or malformed key keeps its default,
- * so a hand-edited file cannot stop the module loading.
- * @param text Whole document.
- * @param output Receives the parsed configuration.
- */
-void parse(std::string_view text, Settings& output) noexcept {
-    std::string_view scalar;
-    if (scalar_for(text, "\"enabled\"", scalar)) {
-        output.enabled = scalar.starts_with("true");
-    }
-    if (scalar_for(text, "\"custom\"", scalar)) {
-        output.custom = scalar.starts_with("true");
-    }
-    std::array<char, kScalarCapacity> buffer{};
-    for (std::size_t index = 0; index < kActivityCount; ++index) {
-        std::array<char, 64> quoted{};
-        const int written = std::snprintf(quoted.data(),
-                                          quoted.size(),
-                                          "\"%.*s\"",
-                                          static_cast<int>(kActivities[index].key.size()),
-                                          kActivities[index].key.data());
-        if (written <= 0
-            || !scalar_for(
-                text, std::string_view(quoted.data(), static_cast<std::size_t>(written)), scalar)
-            || !terminated(scalar, buffer)) {
-            continue;
-        }
-        // Clamped, not refused. One out-of-range lane must not drop every other saved value.
-        output.timeouts[index] =
-            std::clamp(static_cast<std::uint32_t>(std::strtoul(buffer.data(), nullptr, 0)),
-                       kMinimumTimeoutMs,
-                       kMaximumTimeoutMs);
-    }
-    // A hand-edited file can carry both exclusive switches. Removing every timeout wins.
-    if (output.enabled) {
-        output.custom = false;
-    }
-}
-
-/**
- * Writes the whole document. It is small enough that a complete rewrite is the simplest correct
- * save, which the shared settings file is not.
- * @param settings Configuration to store.
- * @return True when every byte reached the file.
- */
-[[nodiscard]] bool store(const Settings& settings) noexcept {
-    if (!g_pathResolved) {
-        return false;
-    }
-    std::array<char, kFileCapacity> document{};
-    int size = std::snprintf(document.data(),
-                             document.size(),
-                             "{\n  \"enabled\": %s,\n  \"custom\": %s",
-                             settings.enabled ? "true" : "false",
-                             settings.custom ? "true" : "false");
-    if (size <= 0) {
-        return false;
-    }
-    for (std::size_t index = 0; index < kActivityCount; ++index) {
-        const int written = std::snprintf(document.data() + size,
-                                          document.size() - static_cast<std::size_t>(size),
-                                          ",\n  \"%.*s\": %u",
-                                          static_cast<int>(kActivities[index].key.size()),
-                                          kActivities[index].key.data(),
-                                          static_cast<unsigned>(settings.timeouts[index]));
-        if (written <= 0 || static_cast<std::size_t>(size + written) >= document.size()) {
-            return false;
-        }
-        size += written;
-    }
-    const int tail = std::snprintf(
-        document.data() + size, document.size() - static_cast<std::size_t>(size), "\n}\n");
-    if (tail <= 0 || static_cast<std::size_t>(size + tail) >= document.size()) {
-        return false;
-    }
-    size += tail;
-    const HANDLE file = CreateFileW(g_path.chars.data(),
-                                    GENERIC_WRITE,
-                                    0,
-                                    nullptr,
-                                    CREATE_ALWAYS,
-                                    FILE_ATTRIBUTE_NORMAL,
-                                    nullptr);
-    if (file == INVALID_HANDLE_VALUE) {
-        return false;
-    }
-    DWORD written = 0;
-    bool complete =
-        WriteFile(file, document.data(), static_cast<DWORD>(size), &written, nullptr) != FALSE
-        && written == static_cast<DWORD>(size);
-    complete = CloseHandle(file) != FALSE && complete;
-    return complete;
-}
-
-/** Reads the configuration file into the active settings when one exists. */
-void load() noexcept {
-    const HANDLE file = CreateFileW(g_path.chars.data(),
-                                    GENERIC_READ,
-                                    FILE_SHARE_READ,
-                                    nullptr,
-                                    OPEN_EXISTING,
-                                    FILE_ATTRIBUTE_NORMAL,
-                                    nullptr);
-    if (file == INVALID_HANDLE_VALUE) {
-        return;
-    }
-    std::array<char, kFileCapacity> buffer{};
-    DWORD read = 0;
-    const bool readOk =
-        ReadFile(file, buffer.data(), static_cast<DWORD>(buffer.size() - 1), &read, nullptr)
-        != FALSE;
-    (void)CloseHandle(file);
-    if (!readOk || read == 0) {
-        return;
-    }
-    Settings parsed{};
-    parse(std::string_view(buffer.data(), read), parsed);
-    g_settings = parsed;
-}
-
-} // namespace
-
-/** Resolves the configuration file and loads it when one exists. */
-void initialize(void* module) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
-    g_settings = Settings{};
-    g_pathResolved =
-        core::path::artifact_directory(module, g_path) && core::path::append(g_path, kFileSuffix);
-    if (g_pathResolved) {
-        load();
-    } else {
-        report_fail("path");
-    }
-    ReleaseSRWLockExclusive(&g_lock);
-}
-
-/** Drops the runtime configuration and the resolved file path. */
-void shutdown() noexcept {
-    AcquireSRWLockExclusive(&g_lock);
-    g_settings = Settings{};
-    g_path = core::path::Buffer{};
-    g_pathResolved = false;
-    ReleaseSRWLockExclusive(&g_lock);
-}
-
-/** @return One lock-consistent copy of the current configuration. */
-Settings get() noexcept {
-    AcquireSRWLockShared(&g_lock);
-    const Settings snapshot = g_settings;
-    ReleaseSRWLockShared(&g_lock);
-    return snapshot;
-}
-
-/** Publishes one configuration and writes it straight to disk. */
-bool publish(const Settings& settings) noexcept {
-    if (!valid(settings)) {
-        return false;
-    }
-    AcquireSRWLockExclusive(&g_lock);
-    g_settings = settings;
-    const bool stored = store(settings);
-    ReleaseSRWLockExclusive(&g_lock);
-    if (!stored) {
-        report_fail("write");
-    }
-    return true;
-}
-
-} // namespace sunrise::client::inactivity

+ 0 - 99
Sunrise/src/client/inactivity/inactivity_settings_store.h

@@ -1,99 +0,0 @@
-#pragma once
-
-#include <array>
-#include <cstddef>
-#include <cstdint>
-#include <string_view>
-
-namespace sunrise::client::inactivity {
-
-/** Activity lanes the Client keeps a separate inactivity timeout for. */
-inline constexpr std::size_t kActivityCount = 14;
-
-/**
- * Shortest timeout offered, in milliseconds. A file carrying less is clamped up, not refused.
- * The matchmade lanes cannot fire until the session outlives the grace, about a minute here, so
- * a shorter value would only look like a hold that is not working.
- */
-inline constexpr std::uint32_t kMinimumTimeoutMs = 60000;
-/** Longest timeout offered, in milliseconds. A day outlasts any session. */
-inline constexpr std::uint32_t kMaximumTimeoutMs = 86400000;
-
-/**
- * The orbit lane. It is the one lane the Client reads without the grace gate, so a short value
- * fires at once, drops the session and leaves a marionberry error the process cannot recover
- * from. Held at its longest whenever the hold is on, and no field or file value reaches it.
- */
-inline constexpr std::size_t kOrbitLane = 13;
-
-/** One activity lane's names and its key in the stored document. */
-struct ActivityInfo {
-    /** Full name, shown where there is room for it. */
-    std::string_view name;
-    /** Column heading, short enough for fourteen lanes across one grid. */
-    std::string_view column;
-    /** Key this lane is stored under. */
-    std::string_view key;
-};
-
-/** Every lane, in block order. */
-inline constexpr std::array<ActivityInfo, kActivityCount> kActivities{{
-    {"PvE", "PvE", "pve"},
-    {"PvE (guided)", "PvE gd", "pve_guided"},
-    {"PvE (matchmade, multiple fireteams)", "PvE mm+", "pve_mm_multiple"},
-    {"PvE (matchmade, single fireteam)", "PvE mm", "pve_mm_single"},
-    {"PvE (special)", "PvE sp", "pve_special"},
-    {"PvP", "PvP", "pvp"},
-    {"PvP (guided)", "PvP gd", "pvp_guided"},
-    {"PvP (matchmade, multiple fireteams)", "PvP mm+", "pvp_mm_multiple"},
-    {"PvP (matchmade, single fireteam)", "PvP mm", "pvp_mm_single"},
-    {"PvP (special)", "PvP sp", "pvp_special"},
-    {"PvP (private)", "Private", "pvp_private"},
-    {"PvP (Trials)", "Trials", "pvp_trials"},
-    {"Social", "Social", "social"},
-    {"Orbit", "Orbit", "orbit"},
-}};
-
-/** @return Every lane at its longest. */
-[[nodiscard]] consteval std::array<std::uint32_t, kActivityCount> longest_timeouts() noexcept {
-    std::array<std::uint32_t, kActivityCount> values{};
-    values.fill(kMaximumTimeoutMs);
-    return values;
-}
-
-/** Compiled lanes a fresh install holds. */
-inline constexpr std::array<std::uint32_t, kActivityCount> kDefaultTimeouts = longest_timeouts();
-
-/**
- * Runtime inactivity configuration. This module owns it; Core settings do not carry it.
- * The two switches are exclusive: one removes every timeout, the other replaces each with a
- * chosen one. Neither leaves the Client's own timeouts in place.
- */
-struct Settings {
-    /** Milliseconds per lane, in block order. Held only while custom is set. */
-    std::array<std::uint32_t, kActivityCount> timeouts{kDefaultTimeouts};
-    bool enabled{false};
-    /** Never set alongside enabled; the two ask for opposite things. */
-    bool custom{false};
-};
-
-/**
- * Resolves the configuration file and loads it when one exists.
- * @param module Loaded DLL used to resolve the owned artifact directory.
- */
-void initialize(void* module) noexcept;
-
-/** Drops the runtime configuration and the resolved file path. */
-void shutdown() noexcept;
-
-/** @return One lock-consistent copy of the current configuration. */
-[[nodiscard]] Settings get() noexcept;
-
-/**
- * Publishes one configuration and writes it straight to disk.
- * @param settings Candidate configuration, refused when a lane is out of range.
- * @return True when the value was published. A failed write is logged, not returned.
- */
-bool publish(const Settings& settings) noexcept;
-
-} // namespace sunrise::client::inactivity

+ 6 - 3
Sunrise/src/client/player/player_settings_store.cpp

@@ -70,6 +70,7 @@ void boolean_for(std::string_view text, std::string_view key, bool& output) noex
  */
 void parse(std::string_view text, Settings& output) noexcept {
     boolean_for(text, "\"infinite_ammo_enabled\"", output.infiniteAmmoEnabled);
+    boolean_for(text, "\"anti_afk_enabled\"", output.antiAfkEnabled);
 }
 
 /**
@@ -85,9 +86,11 @@ void parse(std::string_view text, Settings& output) noexcept {
     std::array<char, kFileCapacity> document{};
     const int size = std::snprintf(document.data(),
                                    document.size(),
-                                   "{\n  \"infinite_ammo_enabled\": %s\n}\n",
-                                   settings.infiniteAmmoEnabled ? "true" : "false");
-    if (size <= 0) {
+                                   "{\n  \"infinite_ammo_enabled\": %s,\n"
+                                   "  \"anti_afk_enabled\": %s\n}\n",
+                                   settings.infiniteAmmoEnabled ? "true" : "false",
+                                   settings.antiAfkEnabled ? "true" : "false");
+    if (size <= 0 || static_cast<std::size_t>(size) >= document.size()) {
         return false;
     }
     const HANDLE file = CreateFileW(g_path.chars.data(),

+ 2 - 0
Sunrise/src/client/player/player_settings_store.h

@@ -5,6 +5,8 @@ namespace sunrise::client::player {
 /** Runtime player configuration. This module owns it; Core settings do not carry it. */
 struct Settings {
     bool infiniteAmmoEnabled{false};
+    /** Holds every activity inactivity timeout at its longest. */
+    bool antiAfkEnabled{false};
 };
 
 /**

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

@@ -23,7 +23,6 @@
 #include "../hooks/retail_log/retail_log_lifecycle.h"
 #include "../hooks/teleport/runtime.h"
 #include "../hooks/world_objects/world_object_registry.h"
-#include "../inactivity/inactivity_settings_store.h"
 #include "../movement/movement_settings_store.h"
 #include "../player/player_settings_store.h"
 #include "../targets/game.h"
@@ -37,14 +36,11 @@ namespace sunrise::client {
 
 /** Initializes Client-owned process state without installing hooks. */
 bool initialize(void* module) noexcept {
-    const core::settings::ActivitySdkGenerationSettings& generation =
-        core::settings::get().activitySdkGeneration;
-    content::activity::sdk_generation::initialize(module,
-                                                  {generation.enabled, generation.luaDeclarations});
+    content::activity::sdk_generation::initialize(
+        module, {core::settings::get().activitySdkGeneration.luaDeclarations});
     // Loaded before the pages register, so each page draws saved values on its first frame.
     movement::initialize(module);
     player::initialize(module);
-    inactivity::initialize(module);
     ui::activity::authored_placement_marker::initialize(module);
     return ui::runtime::initialize();
 }
@@ -146,7 +142,6 @@ bool shutdown() noexcept {
     ui::runtime::shutdown();
     // The reverse of the order the stores initialize in.
     ui::activity::authored_placement_marker::shutdown();
-    inactivity::shutdown();
     player::shutdown();
     movement::shutdown();
     core::log::write(core::log::Channel::client, core::log::Level::info, "ev=shutdown result=ok");

+ 11 - 156
Sunrise/src/client/ui/player/player_panel.cpp

@@ -2,181 +2,36 @@
 
 #include "player_panel.h"
 
-#include <algorithm>
-#include <cstddef>
-#include <cstdint>
 #include <imgui.h>
 
 #include "../../../core/ui/components/toggle/ui_toggle_component.h"
-#include "../../hooks/inactivity/inactivity_override.h"
-#include "../../inactivity/inactivity_settings_store.h"
 #include "../../player/player_settings_store.h"
 
 namespace sunrise::client::ui::player {
-namespace {
-
-namespace inactivity = client::inactivity;
-namespace toggle = core::ui::components::toggle;
-
-/** Grid width. Seven columns lays the fourteen lanes out in two rows. */
-constexpr int kLaneColumns = 7;
-
-/**
- * Draws one lane's field and, under it, the milliseconds the Client is holding in that lane now.
- * @param index Lane in block order.
- * @param configured Configuration updated on an edit.
- * @param status What the override reached, for the live figure.
- * @return True when this lane changed.
- */
-[[nodiscard]] bool draw_lane(std::size_t index,
-                             inactivity::Settings& configured,
-                             const hooks::inactivity::Status& status) noexcept {
-    const bool orbit = index == inactivity::kOrbitLane;
-    bool changed = false;
-    ImGui::PushID(static_cast<int>(index));
-    ImGui::BeginDisabled(orbit);
-    ImGui::TextUnformatted(inactivity::kActivities[index].column.data());
-    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
-        ImGui::SetTooltip("%s", inactivity::kActivities[index].name.data());
-    }
-    // The live figure is the one the Client is timing by, so it reads as active and the set value
-    // is dimmed. A lane held at its longest is timing nothing, so neither is active.
-    const std::uint32_t live = status.liveValid ? status.live[index] : 0;
-    const bool liveActive = status.liveValid && live != inactivity::kMaximumTimeoutMs;
-
-    ImGui::SetNextItemWidth(-FLT_MIN);
-    std::uint32_t milliseconds = configured.timeouts[index];
-    if (liveActive) {
-        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]);
-    }
-    ImGui::InputScalar("##lane",
-                       ImGuiDataType_U32,
-                       &milliseconds,
-                       nullptr,
-                       nullptr,
-                       "%u",
-                       ImGuiInputTextFlags_CharsDecimal);
-    if (liveActive) {
-        ImGui::PopStyleColor();
-    }
-    if (ImGui::IsItemDeactivatedAfterEdit()) {
-        configured.timeouts[index] =
-            std::clamp(milliseconds, inactivity::kMinimumTimeoutMs, inactivity::kMaximumTimeoutMs);
-        changed = true;
-    }
-    ImGui::EndDisabled();
-    // Outside the disabled block: the live value reads the same whether or not the field can be
-    // edited.
-    if (!status.liveValid) {
-        ImGui::TextDisabled("-");
-    } else if (liveActive) {
-        ImGui::Text("%u", live);
-    } else {
-        ImGui::TextDisabled("%u", live);
-    }
-    ImGui::PopID();
-    return changed;
-}
-
-/** @param status What the override reached, for the live grace. */
-void draw_inactivity_clocks(const hooks::inactivity::Status& status) noexcept {
-    // Read from the draw rather than the hold, so the Client is only asked while this section is
-    // on screen to answer to.
-    const hooks::inactivity::Timers timers = hooks::inactivity::timers();
-    if (timers.idleValid) {
-        ImGui::Text("Idle %.1f s", static_cast<double>(timers.idleMs) / 1000.0);
-    } else {
-        ImGui::TextDisabled("Idle -");
-    }
-    ImGui::SameLine();
-    if (timers.sessionValid) {
-        ImGui::Text("Session %.1f s", static_cast<double>(timers.sessionMs) / 1000.0);
-    } else {
-        ImGui::TextDisabled("Session -");
-    }
-    if (!status.liveGraceValid) {
-        return;
-    }
-    const bool passed =
-        status.liveGraceMs == 0 || (timers.sessionValid && timers.sessionMs > status.liveGraceMs);
-    ImGui::SameLine();
-    // Reported, never written.
-    const double grace = static_cast<double>(status.liveGraceMs) / 1000.0;
-    if (passed) {
-        ImGui::TextDisabled("Grace %.1f s (passed)", grace);
-    } else {
-        ImGui::Text("Grace %.1f s (no kick until then)", grace);
-    }
-}
-
-/** Draws the inactivity section: the main switch, the lane grid and the Client's clocks. */
-void draw_inactivity() noexcept {
-    inactivity::Settings configured = inactivity::get();
-    // Taken once, so every line below and the grid all describe the same poll.
-    const hooks::inactivity::Status status = hooks::inactivity::status();
-
-    ImGui::TextUnformatted("Inactivity");
-    ImGui::Separator();
-    ImGui::TextWrapped("Disable AFK timeouts from activities kicking to orbit and the title "
-                       "screen.");
-    ImGui::Spacing();
-
-    // The two switches are exclusive, so turning this one on drops the set timeouts.
-    bool changed = toggle::control("Enabled##inactivity", configured.enabled);
-    if (changed && configured.enabled) {
-        configured.custom = false;
-    }
-
-    if (ImGui::CollapsingHeader("Advanced##inactivity")) {
-        // A per-lane timeout has nothing to act on once every lane is already removed.
-        ImGui::BeginDisabled(configured.enabled);
-        if (toggle::control("Use set timeouts##inactivity_custom", configured.custom)) {
-            if (configured.custom) {
-                configured.enabled = false;
-            }
-            changed = true;
-        }
-        ImGui::EndDisabled();
-        ImGui::TextDisabled("In milliseconds");
-        ImGui::Spacing();
-        ImGui::BeginDisabled(configured.enabled || !configured.custom);
-        if (ImGui::BeginTable("lanes", kLaneColumns, ImGuiTableFlags_SizingStretchSame)) {
-            for (std::size_t index = 0; index < inactivity::kActivityCount; ++index) {
-                ImGui::TableNextColumn();
-                changed = draw_lane(index, configured, status) || changed;
-            }
-            ImGui::EndTable();
-        }
-        ImGui::EndDisabled();
-        ImGui::Spacing();
-        draw_inactivity_clocks(status);
-    }
-
-    if (changed) {
-        (void)inactivity::publish(configured);
-    }
-}
-
-} // namespace
 
 /** Draws the player module inside the active Core UI frame. */
 void draw() noexcept {
+    namespace toggle = core::ui::components::toggle;
     client::player::Settings settings = client::player::get();
 
     ImGui::TextUnformatted("Infinite Ammo");
     ImGui::Separator();
     ImGui::TextWrapped("Keep every weapon's reserves full.");
     ImGui::Spacing();
+    bool changed = toggle::control("Enabled##infinite_ammo", settings.infiniteAmmoEnabled);
+
+    ImGui::Spacing();
+    ImGui::Spacing();
+    ImGui::TextUnformatted("Anti AFK");
+    ImGui::Separator();
+    ImGui::TextWrapped("Disable AFK timeouts from activities kicking to orbit and the title "
+                       "screen.");
+    ImGui::Spacing();
+    changed = toggle::control("Enabled##anti_afk", settings.antiAfkEnabled) || changed;
 
-    const bool changed = core::ui::components::toggle::control("Enabled##infinite_ammo",
-                                                               settings.infiniteAmmoEnabled);
     if (changed) {
         (void)client::player::publish(settings);
     }
-
-    ImGui::Spacing();
-    ImGui::Spacing();
-    draw_inactivity();
 }
 
 } // namespace sunrise::client::ui::player

+ 2 - 4
Sunrise/src/core/settings/settings.h

@@ -13,11 +13,9 @@
 
 namespace sunrise::core::settings {
 
-/** Boot policy for generating the activity SDK required by host roster construction. */
+/** Activity SDK generator policy. Generation has no switch, because the host needs the SDK. */
 struct ActivitySdkGenerationSettings final {
-    /** On by default like the bundled file, so a file without this block still generates. */
-    bool enabled{true};
-    /** Writes the sdk/lua declaration tree. On by default, and only runs when generation does. */
+    /** Writes the sdk/lua declaration tree. */
     bool luaDeclarations{true};
 };
 

+ 2 - 8
Sunrise/src/core/settings/settings_parser.cpp

@@ -111,7 +111,7 @@ bool Parser::core(Settings& output) noexcept {
     }
 }
 
-/** Parses the Core-owned activity SDK generation gate. Omitted members keep their defaults. */
+/** Parses the activity SDK generation block. Omitted or unknown members keep the defaults. */
 bool Parser::activity_sdk_generation_settings(ActivitySdkGenerationSettings& output) noexcept {
     if (!consume('{')) {
         return false;
@@ -121,19 +121,13 @@ bool Parser::activity_sdk_generation_settings(ActivitySdkGenerationSettings& out
         output = candidate;
         return true;
     }
-    bool hasEnabled = false;
     bool hasLuaDeclarations = false;
     for (;;) {
         std::string_view key;
         if (!string(key) || !consume(':')) {
             return false;
         }
-        if (key == "enabled") {
-            if (hasEnabled || !boolean(candidate.enabled)) {
-                return false;
-            }
-            hasEnabled = true;
-        } else if (key == "lua_declarations") {
+        if (key == "lua_declarations") {
             if (hasLuaDeclarations || !boolean(candidate.luaDeclarations)) {
                 return false;
             }

+ 3 - 2
Sunrise/src/core/settings/settings_upgrade.cpp

@@ -45,8 +45,9 @@ constexpr std::array<ReplacedMember, 10> kReplacedMembers{{
     // Version 15 seeded the lore book unlock slots, so both banks take the new default.
     {"\"character_flags\"", 15},
     {"\"objective_values\"", 15},
-    // Version 16 turned generation on. The whole block is replaced, because "enabled" is not
-    // unique in the document. The block also carries the lua_declarations default of version 13.
+    // Version 16 dropped the generation switch. The whole block is replaced, because "enabled" is
+    // not unique in the document. The block also carries the lua_declarations default of
+    // version 13.
     {"\"activity_sdk_generation\"", 16},
 }};
 

+ 2 - 4
Sunrise/src/server/bap/encrypted/push/activity/activity_roster_push.cpp

@@ -291,10 +291,8 @@ bool append_roster_notification(
     const bool lifetimePending =
         hasScriptablePending && singleScriptableLink
         && scriptablePending.kind == server::activity::host::ScriptableOverrideKind::lifetime;
-    // The loading lifetime is the presentation, not the spawn hold: state 4 shows the loading
-    // screen and refuses the native spawn gate on its own, so it is released once the region is
-    // instantiated. `awaiting_client_sync` carries the hold on to the client's arrival report.
-    // An explicit lifetime request still wins.
+    // State 4 shows the loading screen and releases once the region is instantiated. The spawn
+    // hold is `awaiting_client_sync`, not the lifetime. An explicit lifetime request still wins.
     const bool clientLoading = !client_region_ready(session, refresh);
     const bool bodyPending = hasScriptablePending && singleScriptableLink && !lifetimePending;
     // Bodies committed behind the head share its push, so they are installed on this same body.

+ 3 - 5
Sunrise/src/server/bap/encrypted/push/activity/activity_roster_snapshot.cpp

@@ -100,8 +100,7 @@ bool client_region_ready(const Session& session, const RefreshReport* refresh) n
 
 /** Tests whether the client has reported arrival in its instantiated region. */
 bool client_in_world(const Session& session, const RefreshReport* refresh) noexcept {
-    // WS-702 world-state 8 follows the bootflow's arrival, independently of the player spawn.
-    // Holding a region alone can precede that report and the world-transition fade's final arm.
+    // ws-702 world state 8 is the arrival report. It does not depend on the player spawn.
     const state::activity::membership::ClientPlacement placement =
         client_placement(session, refresh);
     return placement.entered && client_region_ready(session, refresh);
@@ -608,9 +607,8 @@ build_roster_snapshot(Session& session,
     // carries matches nothing.
     snapshot.playerKey = published_player_key(session);
     snapshot.lifetime = lifetimeState;
-    // Hold the native spawn gate until the client's arrival report, WS-702 world-state 8. A
-    // region can be loaded before the bootflow arms its fade; a spawn before the arm releases an
-    // inactive fade and leaves the screen black. Arrival does not depend on the spawn.
+    // Hold the native spawn gate until the ws-702 world state reads 8. A spawn before the fade
+    // arms leaves the screen black.
     snapshot.awaitClientSync = !client_in_world(session, refresh);
     // Player_BindComponents walks every type-13 reference and the player datum can name any one of
     // them. So every participation record carries the same player key. Selecting the first slot

+ 1 - 1
Sunrise/src/server/bap/encrypted/push/activity/internal.h

@@ -173,7 +173,7 @@ struct RefreshReport final {
 client_placement(const Session& session, const RefreshReport* refresh) noexcept;
 
 /**
- * Tests whether the client has reported arrival in its instantiated region: its WS-702 world
+ * Tests whether the client has reported arrival in its instantiated region: its ws-702 world
  * state reached 8 while it holds the region it reported and no host move is waiting. This is
  * the report that releases the native spawn gate.
  * @param session Connection whose activity session the client reports on.

+ 2 - 4
Sunrise/src/server/gameplay/peer/peer_out_of_band.cpp

@@ -31,15 +31,13 @@ constexpr std::uint16_t kFirstPacketSequence = 1;
 
 /**
  * Fills the address blob that names this host on the direct path.
- * @param receivingPort Host pool port the request arrived on. Zero names the primary port, as it
- * does on the transport's send path.
+ * @param receivingPort Host pool port the request arrived on.
  * @param output Receives the direct-path address blob.
  */
 void local_address(std::uint16_t receivingPort,
                    std::array<std::byte, wire::kAddressBlobSize>& output) noexcept {
-    const gp::Endpoint advertised = endpoint::advertised();
     middleware::gameplay::descriptor::write_direct_net_addr(
-        advertised.address, receivingPort != 0 ? receivingPort : advertised.port, output);
+        endpoint::advertised().address, receivingPort, output);
 }
 
 /** @return A random 32-bit sequence, or zero when Windows refused. */

+ 0 - 1
Sunrise/src/state/build_data/cache/records/format.h

@@ -34,7 +34,6 @@ inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', '
  * Current build-data cache format. Any other version on disk is rebuilt rather than read.
  * Bump it when a stored shape changes or when the extraction filling it changes what it writes,
  * because a cached row survives a code change and a corrected walk keeps publishing old rows.
- * 63 added the catalyst completion flags' account bank indices to the catalyst record.
  */
 inline constexpr std::uint32_t kCacheFormatVersion = 63;
 /** Signed -1 on disk means there is no equipment slot. */

+ 0 - 6
Sunrise/src/state/build_data/items/catalysts/definition.h

@@ -115,12 +115,6 @@ enum class Error : std::uint8_t {
     return "unknown";
 }
 
-/** Native account acquired-flag row feeding an evaluated unlock slot. */
-struct AccountFlagMapping {
-    std::uint16_t slot{};
-    std::uint16_t accountIndex{};
-};
-
 /** One build-derived exotic weapon catalyst relation. */
 struct Definition {
     std::uint32_t itemDefinitionHash{};

+ 36 - 34
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.cpp

@@ -4,6 +4,7 @@
 #include <array>
 #include <cstddef>
 #include <optional>
+#include <vector>
 
 #include "../../../unlocks/definition.h"
 
@@ -512,23 +513,22 @@ bool derive(const Source& source,
         definition.acquisitionDefinitionIndex = completed->acquisitionDefinitionIndex;
         definition.completion = completed->completion;
         for (std::size_t flag = 0; flag < definition.completion.flagCount; ++flag) {
-            const auto mapping =
-                std::find_if(source.accountFlagMappings.begin(),
-                             source.accountFlagMappings.end(),
-                             [&](const AccountFlagMapping& row) {
-                                 return row.slot == definition.completion.flags[flag];
-                             });
-            if (mapping != source.accountFlagMappings.end()) {
-                if (mapping->accountIndex >= state::unlocks::kAccountFlagCapacity) {
-                    return fail(output,
-                                count,
-                                report,
-                                Error::invalidCompletion,
-                                item->definitionHash,
-                                completed->socketLane);
-                }
-                definition.completionAccountFlagIndices[flag] = mapping->accountIndex;
+            const std::uint16_t slot = definition.completion.flags[flag];
+            const std::uint16_t mapped = slot < source.accountFlagSlotMap.size()
+                                             ? source.accountFlagSlotMap[slot]
+                                             : kUnavailableCompletionFlagIndex;
+            if (mapped == kUnavailableCompletionFlagIndex) {
+                continue;
+            }
+            if (mapped >= state::unlocks::kAccountFlagCapacity) {
+                return fail(output,
+                            count,
+                            report,
+                            Error::invalidCompletion,
+                            item->definitionHash,
+                            completed->socketLane);
             }
+            definition.completionAccountFlagIndices[flag] = mapped;
         }
         definition.objective = completed->objective;
         definition.socketLane = completed->socketLane;
@@ -588,8 +588,6 @@ bool matches_cached(const Source& source,
     std::array<CompletionCondition, 2 * kDefinitionCapacity> completionConditions{};
     std::array<AcquisitionGate, kDefinitionCapacity> acquisitionGates{};
     std::array<std::int32_t, state::unlocks::kObjectiveValueCapacity> objectiveValues{};
-    std::array<AccountFlagMapping, kDefinitionCapacity * kCompletionFlagCapacity> accountMappings{};
-    std::size_t mappingCount = 0;
     std::size_t completionCount = 0;
     std::size_t acquisitionCount = 0;
     std::size_t objectiveCount = 0;
@@ -615,30 +613,34 @@ bool matches_cached(const Source& source,
         return true;
     };
 
+    // The slot map covers the highest mapped slot, so it is sized before it is filled.
+    std::size_t slotMapSize = 0;
+    for (const Definition& definition : definitions) {
+        for (std::size_t flag = 0; flag < definition.completion.flagCount; ++flag) {
+            if (definition.completionAccountFlagIndices[flag] != kUnavailableCompletionFlagIndex) {
+                slotMapSize =
+                    (std::max)(slotMapSize,
+                               static_cast<std::size_t>(definition.completion.flags[flag]) + 1);
+            }
+        }
+    }
+    std::vector<std::uint16_t> accountFlagSlotMap(slotMapSize, kUnavailableCompletionFlagIndex);
+
     for (const Definition& definition : definitions) {
         if (definition.availability == Availability::unsupported) {
             continue;
         }
-        for (std::size_t flag = 0; flag < definition.completionAccountFlagIndices.size(); ++flag) {
-            const auto mapped = definition.completionAccountFlagIndices[flag];
+        for (std::size_t flag = 0; flag < definition.completion.flagCount; ++flag) {
+            const std::uint16_t mapped = definition.completionAccountFlagIndices[flag];
             if (mapped == kUnavailableCompletionFlagIndex) {
                 continue;
             }
-            if (flag >= definition.completion.flagCount
-                || mapped >= state::unlocks::kAccountFlagCapacity
-                || mappingCount >= accountMappings.size()) {
+            // One slot maps to one account row. A stored row that disagrees fails closed.
+            std::uint16_t& entry = accountFlagSlotMap[definition.completion.flags[flag]];
+            if (entry != kUnavailableCompletionFlagIndex && entry != mapped) {
                 return false;
             }
-            const auto slot = definition.completion.flags[flag];
-            for (std::size_t prior = 0; prior < mappingCount; ++prior) {
-                if ((accountMappings[prior].slot == slot
-                     && accountMappings[prior].accountIndex != mapped)
-                    || (accountMappings[prior].accountIndex == mapped
-                        && accountMappings[prior].slot != slot)) {
-                    return false;
-                }
-            }
-            accountMappings[mappingCount++] = {slot, mapped};
+            entry = mapped;
         }
         const details::Definition* detail =
             find_detail(source.details, definition.itemDefinitionIndex);
@@ -730,7 +732,7 @@ bool matches_cached(const Source& source,
                   return first.socketType < second.socketType;
               });
     Source rebuilt = source;
-    rebuilt.accountFlagMappings = std::span(accountMappings).first(mappingCount);
+    rebuilt.accountFlagSlotMap = accountFlagSlotMap;
     rebuilt.completionConditions = std::span(completionConditions).first(completionCount);
     rebuilt.acquisitionGates = std::span(acquisitionGates).first(acquisitionCount);
     rebuilt.objectiveCompletionValues = std::span(objectiveValues).first(objectiveCount);

+ 2 - 2
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_builder.h

@@ -34,8 +34,8 @@ struct Source {
     std::span<const AcquisitionGate> acquisitionGates;
     /** Dense objective-indexed completion values read from the installed objective table. */
     std::span<const std::int32_t> objectiveCompletionValues;
-    /** Package account-flag mapping; slot identities are not account array indices. */
-    std::span<const AccountFlagMapping> accountFlagMappings;
+    /** Account flag bank index per unlock slot. An unmapped slot holds the unavailable index. */
+    std::span<const std::uint16_t> accountFlagSlotMap;
 };
 
 /** @return The generated facts pinned to Destiny 2 build 86657.20.08.23. */

+ 16 - 13
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.cpp

@@ -2,6 +2,7 @@
 
 #include <algorithm>
 #include <atomic>
+#include <cstddef>
 #include <mutex>
 #include <shared_mutex>
 
@@ -36,8 +37,15 @@ std::atomic<bool> g_completionEnabled{true};
                : nullptr;
 }
 
-/** Sets one flag slot and removes duplicate authored rows for that slot. */
-[[nodiscard]] bool upsert_flag(state::Family5State& family, std::uint16_t slot) noexcept {
+/**
+ * Sets one flag slot and removes duplicate authored rows for that slot.
+ * @param family Candidate Family-5 state.
+ * @param slot Flag slot to set.
+ * @param appendMissing False leaves a slot with no authored row absent.
+ * @return False when the slot is new, wanted, and the list is full.
+ */
+[[nodiscard]] bool
+upsert_flag(state::Family5State& family, std::uint16_t slot, bool appendMissing) noexcept {
     const std::size_t oldCount = family.flagCount;
     std::size_t write = 0;
     bool found = false;
@@ -52,7 +60,7 @@ std::atomic<bool> g_completionEnabled{true};
         }
         family.flags[write++] = row;
     }
-    if (!found) {
+    if (!found && appendMissing) {
         if (write >= family.flags.size()) {
             return false;
         }
@@ -306,19 +314,14 @@ bool append_investment_overrides(state::Family5State& family) noexcept {
         if (definition.availability != Availability::released) {
             continue;
         }
-        if (!upsert_flag(candidate, definition.acquisitionDefinitionIndex)) {
+        if (!upsert_flag(candidate, definition.acquisitionDefinitionIndex, true)) {
             return false;
         }
         for (std::size_t flag = 0; flag < definition.completion.flagCount; ++flag) {
-            const auto slot = definition.completion.flags[flag];
-            // A mapped completion rides in the account bank, so it takes a Family-5 row only when
-            // the state already carries one for that slot, which is then raised to the set value.
-            const bool present = std::any_of(candidate.flags.begin(),
-                                             candidate.flags.begin() + candidate.flagCount,
-                                             [slot](const auto& row) { return row.slot == slot; });
-            if ((definition.completionAccountFlagIndices[flag] == kUnavailableCompletionFlagIndex
-                 || present)
-                && !upsert_flag(candidate, slot)) {
+            // A mapped completion lives in the account bank; only an authored row for it is raised.
+            const bool unmapped =
+                definition.completionAccountFlagIndices[flag] == kUnavailableCompletionFlagIndex;
+            if (!upsert_flag(candidate, definition.completion.flags[flag], unmapped)) {
                 return false;
             }
         }

+ 3 - 4
Sunrise/src/state/build_data/items/catalysts/exotic_catalyst_catalog.h

@@ -70,10 +70,9 @@ void set_completion_enabled(bool enabled) noexcept;
                                           std::span<std::optional<std::uint16_t>> plugs) noexcept;
 
 /**
- * Adds acquired-state gates, completion flags without an account mapping, and completion values
- * for released catalysts. Existing authored rows with the same slot are raised to the required
- * value, mapped or not. The input stays unchanged when either fixed override bank cannot hold
- * the complete deduplicated result.
+ * Adds the acquisition gates, unmapped completion flags and completion values of released
+ * catalysts. An authored row for any of these slots, mapped or not, is raised to the required
+ * value. The input stays unchanged when either fixed override bank cannot hold the result.
  * @param family Candidate Family-5 state.
  * @return True when completion is disabled or every released override fits atomically.
  */

+ 3 - 0
Sunrise/src/state/runtime/state_progression_runtime.cpp

@@ -153,6 +153,9 @@ upsert_value(Family5State& family, std::uint16_t slot, std::int32_t value) noexc
 /**
  * Removes the family-5 flag rows that name artifact sale slots.
  * The character bank carries ownership, and a family-5 copy would mask it and cost 25 rows.
+ * @param family Global override object, mutated in place.
+ * @param rows Artifact sale rows.
+ * @param count Rows in use.
  */
 void strip_artifact_flags_locked(Family5State& family,
                                  const SaleRows& rows,