Selaa lähdekoodia

Make subclasses equippable through the standard inventory swap

Millie 3 viikkoa sitten
vanhempi
commit
bfa37e704c

+ 5 - 4
Sunrise/src/client/content/investment/investment_refresh.cpp

@@ -47,8 +47,9 @@ bool refresh() noexcept {
         // The same lock as the extraction path. A cache write holds its own lock across file
         // calls, so a held thread stopped inside one would deadlock the freeze below.
         AcquireSRWLockExclusive(&g_refreshLock);
-        const bool persisted =
-            state::ensure_profile_item_identities() && state::build_data::persist();
+        const bool persisted = state::ensure_profile_item_identities()
+                                && state::ensure_character_subclasses()
+                                && state::build_data::persist();
         // Nothing reads a package again until the next boot, so the open files and the held
         // tables go back now rather than at process exit.
         middleware::content::packages::reader::release_caches();
@@ -65,8 +66,8 @@ bool refresh() noexcept {
     // The package pass owns the item table and must not wait on runtime content lookups.
     (void)items::packages::build();
     const bool domainsReady = ready();
-    const bool complete =
-        domainsReady && state::ensure_profile_item_identities() && state::build_data::persist();
+    const bool complete = domainsReady && state::ensure_profile_item_identities()
+                          && state::ensure_character_subclasses() && state::build_data::persist();
     // The overlay ends with the work, not with the slice, so it spans every retry the pass needs.
     if (complete) {
         core::ui::busy::end(core::ui::busy::Task::contentExtraction);

+ 80 - 25
Sunrise/src/client/content/items/packages/package_subclass_build.cpp

@@ -95,45 +95,100 @@ bool build_character_abilities(const reader::Source& source,
         return false;
     }
     const state::AccountState account = state::account_snapshot();
-    for (std::size_t character = 0; character < account.characterCount && count < output.size();
-         ++character) {
-        domain::Definition row{};
-        const char* subclassReason = "subclass";
-        if (!subclass_list(
-                account.characters[character], row.socketEntryListIndex, subclassReason)) {
-            const auto& subclass = account.characters[character].equipment.slots[kSubclassSlot];
-            report_ability_failure(
-                subclassReason, character, subclass.has_value() ? subclass->definitionHash : 0, 0);
-            continue;
+    // First-option defaults every shipped subclass starts at (see account_state.h). Equipping a
+    // subclass always resets its picks to these (state_account_runtime.cpp), so every owned
+    // subclass publishes a row at this fixed selection regardless of which one is equipped right
+    // now. That row has to exist synchronously, ahead of time: the equip response that needs it is
+    // built inline with the commit, with no room to wait on a later refresh slice.
+    const domain::Selection defaultSelection{state::kDefaultMovementAbilityEntry,
+                                             state::kDefaultGrenadeAbilityEntry,
+                                             state::kDefaultSuperAbilityEntry,
+                                             state::kDefaultMeleeAbilityEntry,
+                                             state::kDefaultClassAbilityEntry};
+    // Builds and stores one row, skipping a key already held. Best-effort: a failure here still
+    // lets the other rows in this pass publish.
+    const auto publish = [&](std::size_t character,
+                             std::uint16_t socketEntryListIndex,
+                             const domain::Selection& selection) noexcept {
+        if (count >= output.size()) {
+            return;
         }
-        // The selection is held in a local because the row it also keys is the build's output.
-        const domain::Selection selection = selection_of(account.characters[character]);
+        domain::Definition row{};
+        row.socketEntryListIndex = socketEntryListIndex;
         row.selection = selection;
         if (held(output.first(count), row)) {
-            continue;
+            return;
         }
         tables::IndexRow indexRow{};
-        if (!tables::index_row(
-                std::span<const std::byte>{table}, rows, row.socketEntryListIndex, indexRow)
+        if (!tables::index_row(std::span<const std::byte>{table}, rows, socketEntryListIndex, indexRow)
             || indexRow.targetTag == 0) {
-            report_ability_failure("index_row", character, row.socketEntryListIndex, rows.count);
-            continue;
+            report_ability_failure("index_row", character, socketEntryListIndex, rows.count);
+            return;
         }
         if (!reader::read_tag(source, scratch, indexRow.targetTag, definition)) {
-            report_ability_failure(
-                "definition_read", character, row.socketEntryListIndex, indexRow.targetTag);
-            continue;
+            report_ability_failure("definition_read", character, socketEntryListIndex, indexRow.targetTag);
+            return;
         }
         if (!build_ability_buckets(
                 source, scratch, std::span<const std::byte>{definition}, blob, selection, row)) {
-            const std::size_t packedSelection =
-                selection.movementEntry | (selection.grenadeEntry << 8U)
-                | (selection.superEntry << 16U) | (selection.meleeEntry << 24U);
+            const std::size_t packedSelection = selection.movementEntry
+                                                | (selection.grenadeEntry << 8U)
+                                                | (selection.superEntry << 16U)
+                                                | (selection.meleeEntry << 24U);
+            report_ability_failure("bucket_build", character, socketEntryListIndex, packedSelection);
+            return;
+        }
+        output[count++] = row;
+    };
+    for (std::size_t character = 0; character < account.characterCount && count < output.size();
+         ++character) {
+        std::uint16_t equippedSocketEntryListIndex = 0;
+        const char* subclassReason = "subclass";
+        if (!subclass_list(
+                account.characters[character], equippedSocketEntryListIndex, subclassReason)) {
+            const auto& subclass = account.characters[character].equipment.slots[kSubclassSlot];
             report_ability_failure(
-                "bucket_build", character, row.socketEntryListIndex, packedSelection);
+                subclassReason, character, subclass.has_value() ? subclass->definitionHash : 0, 0);
             continue;
         }
-        output[count++] = row;
+
+        const auto& equippedSlot = account.characters[character].equipment.slots[kSubclassSlot];
+        state::build_data::items::Definition equippedItem{};
+        if (!state::build_data::find_item_definition_hash(equippedSlot->definitionHash,
+                                                           equippedItem)) {
+            continue;
+        }
+
+        // Every subclass the character owns publishes a row, not just the equipped one, so a
+        // later equip swap always lands on an already-built row instead of racing the next
+        // refresh slice. When the group cannot be resolved, at least the equipped one still
+        // publishes, matching the prior single-row behaviour.
+        std::array<std::uint16_t, state::build_data::kSubclassGroupSize> group{};
+        std::array<std::uint16_t, state::build_data::kSubclassGroupSize> members{};
+        std::size_t memberCount = 1;
+        members[0] = equippedItem.definitionIndex;
+        if (state::build_data::find_subclass_group(equippedItem.definitionIndex, group)) {
+            members = group;
+            memberCount = group.size();
+        }
+
+        const domain::Selection realSelection = selection_of(account.characters[character]);
+        for (std::size_t member = 0; member < memberCount && count < output.size(); ++member) {
+            const std::uint16_t memberDefinitionIndex = members[member];
+            state::build_data::items::details::Definition memberDetail{};
+            if (!state::build_data::find_configured_item_detail(memberDefinitionIndex,
+                                                                 memberDetail)) {
+                continue;
+            }
+            publish(character, memberDetail.socketEntryListIndex, defaultSelection);
+            // The equipped member also publishes its real current picks, on top of the default
+            // row every member gets: a fresh boot that never swapped needs its actual selection
+            // to resolve, not the shared default.
+            if (memberDefinitionIndex == equippedItem.definitionIndex
+                && !(realSelection == defaultSelection)) {
+                publish(character, memberDetail.socketEntryListIndex, realSelection);
+            }
+        }
     }
     if (count == 0) {
         report_ability_failure("empty", account.characterCount, rows.count, output.size());

+ 8 - 2
Sunrise/src/state/build_data/abilities/definition.h

@@ -12,8 +12,14 @@ inline constexpr std::size_t kBucketCapacity = 12;
 inline constexpr std::size_t kBucketHashCapacity = 16;
 /** The flat overflow bank holds 32 hashes no bucket category claims. */
 inline constexpr std::size_t kOverflowCapacity = 32;
-/** One row per distinct subclass and ability selection the configured characters use. */
-inline constexpr std::size_t kDefinitionCapacity = 8;
+/**
+ * One row per distinct subclass and ability selection the configured characters use. Each
+ * character publishes a row for every subclass it owns at the shared default selection (not just
+ * the equipped one), plus one extra row for the equipped subclass's real picks when those are not
+ * already the default — 3 characters by up to 4 rows (3 subclasses plus the equipped one's real
+ * picks) each.
+ */
+inline constexpr std::size_t kDefinitionCapacity = 12;
 /** All bits set marks a bucket no entry claimed. */
 inline constexpr std::uint8_t kEmptyBucketKind = 0xFF;
 

+ 17 - 0
Sunrise/src/state/build_data/runtime.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include <array>
 #include <cstddef>
 #include <cstdint>
 #include <span>
@@ -317,6 +318,22 @@ publish_socket_entry_lists(std::span<const socket_entry_lists::Definition> defin
 [[nodiscard]] bool find_socket_entry_list(std::uint16_t definitionIndex,
                                           socket_entry_lists::Definition& definition) noexcept;
 
+/** Number of subclass items every character class ships in this build. */
+inline constexpr std::size_t kSubclassGroupSize = 3;
+
+/**
+ * Finds the 2 other subclasses that share one character class with a known member.
+ * The installed manifest lists every subclass item (any item carrying a socket-entry-list)
+ * together as one dense run per class, in native definition-index order, so the run holding a
+ * known member gives every other member with no per-class identity of its own to look up.
+ * @param memberDefinitionIndex Native item-definition index of one subclass in the class.
+ * @param group Receives the 3 member indices, in native definition-index order.
+ * @return True when every subclass item was found and `memberDefinitionIndex` is one of them.
+ */
+[[nodiscard]] bool
+find_subclass_group(std::uint16_t memberDefinitionIndex,
+                    std::array<std::uint16_t, kSubclassGroupSize>& group) noexcept;
+
 /** @return True when a complete destination-layout domain, empty or not, is published. */
 [[nodiscard]] bool scenario_layouts_ready() noexcept;
 

+ 38 - 0
Sunrise/src/state/build_data/runtime/build_data_routing_catalogs.cpp

@@ -1,4 +1,5 @@
 #include "../inventory/buckets/inventory_bucket_catalog.h"
+#include "../items/details/definition.h"
 #include "../items/item_catalog.h"
 #include "../runtime.h"
 #include "../socket_entry_lists/socket_entry_list_catalog.h"
@@ -69,4 +70,41 @@ bool find_socket_entry_list(std::uint16_t definitionIndex,
     return socket_entry_lists_ready() && socket_entry_lists::find(definitionIndex, definition);
 }
 
+/** Finds the 2 other subclasses sharing one character class with a known member. */
+bool find_subclass_group(std::uint16_t memberDefinitionIndex,
+                         std::array<std::uint16_t, kSubclassGroupSize>& group) noexcept {
+    group.fill(0);
+    const std::size_t itemCount = item_definition_count();
+    if (itemCount == 0) {
+        return false;
+    }
+    // Every subclass item (any item carrying a socket-entry-list), in native definition-index
+    // order. The installed manifest lists these as one dense run of kSubclassGroupSize per class.
+    std::array<std::uint16_t, socket_entry_lists::kEntryTableCapacity> subclasses{};
+    std::size_t subclassCount = 0;
+    for (std::size_t index = 0; index < itemCount && subclassCount < subclasses.size(); ++index) {
+        items::details::Definition detail{};
+        socket_entry_lists::EntryTable entries{};
+        if (find_configured_item_detail(static_cast<std::uint16_t>(index), detail)
+            && detail.definitionIndex == index
+            && find_socket_entry_table(detail.socketEntryListIndex, entries)) {
+            subclasses[subclassCount++] = static_cast<std::uint16_t>(index);
+        }
+    }
+    for (std::size_t base = 0; base + kSubclassGroupSize <= subclassCount;
+         base += kSubclassGroupSize) {
+        const bool matches = subclasses[base] == memberDefinitionIndex
+                             || subclasses[base + 1] == memberDefinitionIndex
+                             || subclasses[base + 2] == memberDefinitionIndex;
+        if (!matches) {
+            continue;
+        }
+        group[0] = subclasses[base];
+        group[1] = subclasses[base + 1];
+        group[2] = subclasses[base + 2];
+        return true;
+    }
+    return false;
+}
+
 } // namespace sunrise::state::build_data

+ 9 - 0
Sunrise/src/state/runtime/runtime.h

@@ -15,6 +15,15 @@ namespace sunrise::state {
  */
 [[nodiscard]] bool ensure_profile_item_identities() noexcept;
 
+/**
+ * Grants each character the other 2 subclasses of the class its equipped subclass belongs to,
+ * placing any missing ones into unequipped inventory with native socket defaults. Idempotent:
+ * a subclass already equipped or already sitting in inventory is left alone.
+ * @return True when every character with an equipped subclass ends up holding its whole class,
+ * or there was nothing to check (no account, or no character carries a subclass yet).
+ */
+[[nodiscard]] bool ensure_character_subclasses() noexcept;
+
 /** Direction of one checked character equipment mutation. */
 enum class EquipmentMutationKind : std::uint8_t {
     none,

+ 92 - 0
Sunrise/src/state/runtime/state_account_runtime.cpp

@@ -2117,6 +2117,20 @@ bool prepare_equipment_swap(std::uint64_t requestedInstanceSoid,
         after.inventory.values[after.inventory.count] = {};
     }
 
+    // A subclass swap changes which socket-entry list the character's ability picks index into.
+    // The prior selection is meaningless (and often out of range) for the newly equipped
+    // subclass, so it resets to the universal first-option defaults every shipped subclass
+    // starts at, matching what a fresh character carries before any pick is made.
+    constexpr std::size_t kSubclassSlotIndex =
+        static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass);
+    if (equipmentSlotIndex == kSubclassSlotIndex) {
+        after.movementAbilityEntry = kDefaultMovementAbilityEntry;
+        after.grenadeAbilityEntry = kDefaultGrenadeAbilityEntry;
+        after.superAbilityEntry = kDefaultSuperAbilityEntry;
+        after.meleeAbilityEntry = kDefaultMeleeAbilityEntry;
+        after.classAbilityEntry = kDefaultClassAbilityEntry;
+    }
+
     std::size_t movedItemCount = 0;
     if (!finalize_equipment_transition(account,
                                        characterIndex,
@@ -2355,4 +2369,82 @@ AccountState account_snapshot() noexcept {
     return snapshot;
 }
 
+/** Grants each character the other 2 subclasses of its equipped subclass's class. */
+bool ensure_character_subclasses() noexcept {
+    constexpr std::size_t kSubclassSlot =
+        static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass);
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState candidate = runtime::storage::g_state.account;
+    if (!account::valid(candidate)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return true;
+    }
+    std::uint64_t nextSoid = 0;
+    bool haveNextSoid = false;
+    bool changed = false;
+    bool failed = false;
+    for (std::size_t characterIndex = 0;
+         characterIndex < candidate.characterCount && !failed;
+         ++characterIndex) {
+        CharacterState& character = candidate.characters[characterIndex];
+        const std::optional<authored_inventory::Item>& equipped =
+            character.equipment.slots[kSubclassSlot];
+        if (!equipped.has_value()) {
+            continue;
+        }
+        build_data::items::Definition equippedDefinition{};
+        std::array<std::uint16_t, build_data::kSubclassGroupSize> group{};
+        if (!build_data::find_item_definition_hash(equipped->definitionHash, equippedDefinition)
+            || !build_data::find_subclass_group(equippedDefinition.definitionIndex, group)) {
+            continue;
+        }
+        for (const std::uint16_t memberIndex : group) {
+            if (memberIndex == equippedDefinition.definitionIndex) {
+                continue;
+            }
+            build_data::items::Definition memberDefinition{};
+            if (!build_data::find_item_definition_index(memberIndex, memberDefinition)
+                || memberDefinition.definitionIndex != memberIndex) {
+                continue;
+            }
+            bool present = false;
+            for (std::size_t itemIndex = 0; itemIndex < character.inventory.count; ++itemIndex) {
+                if (character.inventory.values[itemIndex].definitionHash
+                    == memberDefinition.definitionHash) {
+                    present = true;
+                    break;
+                }
+            }
+            if (present || character.inventory.count >= character.inventory.values.size()) {
+                continue;
+            }
+            if (!haveNextSoid) {
+                if (!next_item_instance_soid(candidate, nextSoid)) {
+                    failed = true;
+                    break;
+                }
+                haveNextSoid = true;
+            }
+            authored_inventory::Item granted{};
+            granted.instanceSoid = nextSoid++;
+            granted.definitionHash = memberDefinition.definitionHash;
+            granted.level = 0;
+            granted.quantity = 1;
+            // Every resolved item's serial must stay behind the character's own counter (checked
+            // by the character encoder, not by account::valid), so claim the next one here too.
+            granted.mutationSerial = static_cast<std::int32_t>(character.nextInventorySerial++);
+            granted.sockets.policy = authored_inventory::SocketPolicy::nativeDefaults;
+            character.inventory.values[character.inventory.count++] = granted;
+            changed = true;
+        }
+    }
+    if (failed || !changed || !account::valid(candidate)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return !failed;
+    }
+    runtime::storage::g_state.account = candidate;
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+    return true;
+}
+
 } // namespace sunrise::state