#include #include "../../../../state/account/account_state.h" #include "../../../../state/build_data/runtime.h" #include "../../../../state/runtime/runtime.h" #include "internal.h" namespace sunrise::client::content::items::packages { namespace { namespace domain = state::build_data::abilities; /** The authored equipment slot that holds the subclass. */ constexpr std::size_t kSubclassSlot = static_cast(state::account::inventory::EquipmentSlot::subclass); /** * Finds the socket entry list that carries one character's subclass abilities. * @param character Authored character. * @param socketEntryListIndex Receives the subclass's socket-entry-list index. * @return True when the character equips a subclass whose detail is published. */ [[nodiscard]] bool subclass_list(const state::CharacterState& character, std::uint16_t& socketEntryListIndex, const char*& reason) noexcept { const auto& slot = character.equipment.slots[kSubclassSlot]; state::build_data::items::Definition item{}; state::build_data::items::details::Definition detail{}; reason = "subclass_slot"; if (!slot.has_value()) { return false; } reason = "subclass_item"; if (!state::build_data::find_item_definition_hash(slot->definitionHash, item)) { return false; } reason = "subclass_detail"; if (!state::build_data::find_configured_item_detail(item.definitionIndex, detail)) { return false; } socketEntryListIndex = detail.socketEntryListIndex; return true; } /** * @param rows Rows built so far. * @param row Candidate whose key is tested against them. * @return True when the candidate's key is already held. */ [[nodiscard]] bool held(std::span rows, const domain::Definition& row) noexcept { for (const domain::Definition& existing : rows) { if (existing.socketEntryListIndex == row.socketEntryListIndex && existing.selection == row.selection) { return true; } } return false; } /** @param character Authored character. @return Its 5 selected socket entries. */ [[nodiscard]] domain::Selection selection_of(const state::CharacterState& character) noexcept { return {character.movementAbilityEntry, character.grenadeAbilityEntry, character.superAbilityEntry, character.meleeAbilityEntry, character.classAbilityEntry}; } } // namespace /** Builds one ability bucket row per distinct subclass and ability selection in use. */ bool build_character_abilities(const reader::Source& source, reader::Scratch& scratch, std::span root, std::vector& table, std::vector& definition, std::vector& blob, std::span output, std::size_t& count) noexcept { count = 0; std::uint32_t tableTag = 0; tables::Array rows{}; if (!tables::slot_tag(root, tables::kSocketEntryListTableSlot, tableTag) || tableTag == 0) { report_ability_failure("table_slot", 0, root.size(), tableTag); return false; } if (!reader::read_tag(source, scratch, tableTag, table)) { report_ability_failure("table_read", 0, tableTag, 0); return false; } if (!tables::find_array_at( std::span{table}, tables::kTableArrayDescriptor, rows)) { report_ability_failure("table_array", 0, table.size(), 0); return false; } const state::AccountState account = state::account_snapshot(); // 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; } domain::Definition row{}; row.socketEntryListIndex = socketEntryListIndex; row.selection = selection; if (held(output.first(count), row)) { return; } tables::IndexRow indexRow{}; if (!tables::index_row(std::span{table}, rows, socketEntryListIndex, indexRow) || indexRow.targetTag == 0) { 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, socketEntryListIndex, indexRow.targetTag); return; } if (!build_ability_buckets( source, scratch, std::span{definition}, blob, selection, row)) { 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( subclassReason, character, subclass.has_value() ? subclass->definitionHash : 0, 0); continue; } 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 group{}; std::array 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()); } return true; } } // namespace sunrise::client::content::items::packages