package_subclass_build.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. #include <array>
  2. #include "../../../../state/account/account_state.h"
  3. #include "../../../../state/build_data/runtime.h"
  4. #include "../../../../state/runtime/runtime.h"
  5. #include "internal.h"
  6. namespace sunrise::client::content::items::packages {
  7. namespace {
  8. namespace domain = state::build_data::abilities;
  9. /** The authored equipment slot that holds the subclass. */
  10. constexpr std::size_t kSubclassSlot =
  11. static_cast<std::size_t>(state::account::inventory::EquipmentSlot::subclass);
  12. /**
  13. * Finds the socket entry list that carries one character's subclass abilities.
  14. * @param character Authored character.
  15. * @param socketEntryListIndex Receives the subclass's socket-entry-list index.
  16. * @return True when the character equips a subclass whose detail is published.
  17. */
  18. [[nodiscard]] bool subclass_list(const state::CharacterState& character,
  19. std::uint16_t& socketEntryListIndex,
  20. const char*& reason) noexcept {
  21. const auto& slot = character.equipment.slots[kSubclassSlot];
  22. state::build_data::items::Definition item{};
  23. state::build_data::items::details::Definition detail{};
  24. reason = "subclass_slot";
  25. if (!slot.has_value()) {
  26. return false;
  27. }
  28. reason = "subclass_item";
  29. if (!state::build_data::find_item_definition_hash(slot->definitionHash, item)) {
  30. return false;
  31. }
  32. reason = "subclass_detail";
  33. if (!state::build_data::find_configured_item_detail(item.definitionIndex, detail)) {
  34. return false;
  35. }
  36. socketEntryListIndex = detail.socketEntryListIndex;
  37. return true;
  38. }
  39. /**
  40. * @param rows Rows built so far.
  41. * @param row Candidate whose key is tested against them.
  42. * @return True when the candidate's key is already held.
  43. */
  44. [[nodiscard]] bool held(std::span<const domain::Definition> rows,
  45. const domain::Definition& row) noexcept {
  46. for (const domain::Definition& existing : rows) {
  47. if (existing.socketEntryListIndex == row.socketEntryListIndex
  48. && existing.selection == row.selection) {
  49. return true;
  50. }
  51. }
  52. return false;
  53. }
  54. /** @param rows Rows built so far. @return True when this list already has a resolved row. */
  55. [[nodiscard]] bool held(std::span<const state::build_data::socket_entry_buckets::Definition> rows,
  56. std::uint16_t socketEntryListIndex) noexcept {
  57. for (const auto& existing : rows) {
  58. if (existing.socketEntryListIndex == socketEntryListIndex) {
  59. return true;
  60. }
  61. }
  62. return false;
  63. }
  64. /** @param item Authored subclass item. @return Its 5 selected socket entries. */
  65. [[nodiscard]] domain::Selection selection_of(const state::account::inventory::Item& item) noexcept {
  66. return {item.movementAbilityEntry,
  67. item.grenadeAbilityEntry,
  68. item.superAbilityEntry,
  69. item.meleeAbilityEntry,
  70. item.classAbilityEntry};
  71. }
  72. } // namespace
  73. /** Builds one ability bucket row per distinct subclass and ability selection in use. */
  74. bool build_character_abilities(
  75. const reader::Source& source,
  76. reader::Scratch& scratch,
  77. std::span<const std::byte> root,
  78. std::vector<std::byte>& table,
  79. std::vector<std::byte>& definition,
  80. std::vector<std::byte>& blob,
  81. std::span<state::build_data::abilities::Definition> output,
  82. std::size_t& count,
  83. std::span<state::build_data::socket_entry_buckets::Definition> entryBucketOutput,
  84. std::size_t& entryBucketCount) noexcept {
  85. count = 0;
  86. entryBucketCount = 0;
  87. std::uint32_t tableTag = 0;
  88. tables::Array rows{};
  89. if (!tables::slot_tag(root, tables::kSocketEntryListTableSlot, tableTag) || tableTag == 0) {
  90. report_ability_failure("table_slot", 0, root.size(), tableTag);
  91. return false;
  92. }
  93. if (!reader::read_tag(source, scratch, tableTag, table)) {
  94. report_ability_failure("table_read", 0, tableTag, 0);
  95. return false;
  96. }
  97. if (!tables::find_array_at(
  98. std::span<const std::byte>{table}, tables::kTableArrayDescriptor, rows)) {
  99. report_ability_failure("table_array", 0, table.size(), 0);
  100. return false;
  101. }
  102. const state::AccountState account = state::account_snapshot();
  103. // First-option defaults every shipped subclass starts at. Equipping one resets its picks to
  104. // these, so every owned subclass publishes a row at this selection. The row must exist
  105. // synchronously: the equip response is built inline with the commit.
  106. const domain::Selection defaultSelection{state::kDefaultMovementAbilityEntry,
  107. state::kDefaultGrenadeAbilityEntry,
  108. state::kDefaultSuperAbilityEntry,
  109. state::kDefaultMeleeAbilityEntry,
  110. state::kDefaultClassAbilityEntry};
  111. // Builds and stores one row, skipping a key already held. Best-effort: a failure here still
  112. // lets the other rows in this pass publish.
  113. const auto publish = [&](std::size_t character,
  114. std::uint16_t socketEntryListIndex,
  115. const domain::Selection& selection) noexcept {
  116. if (count >= output.size()) {
  117. // A dropped row leaves that subclass and selection with no published abilities.
  118. report_ability_failure("capacity", character, socketEntryListIndex, output.size());
  119. return;
  120. }
  121. domain::Definition row{};
  122. row.socketEntryListIndex = socketEntryListIndex;
  123. row.selection = selection;
  124. if (held(output.first(count), row)) {
  125. return;
  126. }
  127. tables::IndexRow indexRow{};
  128. if (!tables::index_row(
  129. std::span<const std::byte>{table}, rows, socketEntryListIndex, indexRow)
  130. || indexRow.targetTag == 0) {
  131. report_ability_failure("index_row", character, socketEntryListIndex, rows.count);
  132. return;
  133. }
  134. if (!reader::read_tag(source, scratch, indexRow.targetTag, definition)) {
  135. report_ability_failure(
  136. "definition_read", character, socketEntryListIndex, indexRow.targetTag);
  137. return;
  138. }
  139. // A bundled group can freely mix which ability slot each of its members fills (an
  140. // Attunement's melee and super swap can sit in either position), so this is resolved once
  141. // per list here, independent of any selection, rather than assumed from table position.
  142. if (entryBucketCount < entryBucketOutput.size()
  143. && !held(entryBucketOutput.first(entryBucketCount), socketEntryListIndex)) {
  144. state::build_data::socket_entry_buckets::Definition entryBuckets{};
  145. entryBuckets.socketEntryListIndex = socketEntryListIndex;
  146. if (resolve_entry_buckets(source,
  147. scratch,
  148. std::span<const std::byte>{definition},
  149. blob,
  150. entryBuckets.buckets)) {
  151. entryBucketOutput[entryBucketCount++] = entryBuckets;
  152. }
  153. }
  154. if (!build_ability_buckets(
  155. source, scratch, std::span<const std::byte>{definition}, blob, selection, row)) {
  156. const std::size_t packedSelection =
  157. selection.movementEntry | (selection.grenadeEntry << 8U)
  158. | (selection.superEntry << 16U) | (selection.meleeEntry << 24U);
  159. report_ability_failure(
  160. "bucket_build", character, socketEntryListIndex, packedSelection);
  161. return;
  162. }
  163. output[count++] = row;
  164. };
  165. for (std::size_t character = 0; character < account.characterCount && count < output.size();
  166. ++character) {
  167. std::uint16_t equippedSocketEntryListIndex = 0;
  168. const char* subclassReason = "subclass";
  169. if (!subclass_list(
  170. account.characters[character], equippedSocketEntryListIndex, subclassReason)) {
  171. const auto& subclass = account.characters[character].equipment.slots[kSubclassSlot];
  172. report_ability_failure(
  173. subclassReason, character, subclass.has_value() ? subclass->definitionHash : 0, 0);
  174. continue;
  175. }
  176. const auto& equippedSlot = account.characters[character].equipment.slots[kSubclassSlot];
  177. state::build_data::items::Definition equippedItem{};
  178. if (!state::build_data::find_item_definition_hash(equippedSlot->definitionHash,
  179. equippedItem)) {
  180. continue;
  181. }
  182. // Every subclass the character owns publishes a row, not just the equipped one, so a
  183. // later equip swap lands on an already-built row instead of racing the next refresh
  184. // slice. When the group cannot be resolved, the equipped one still publishes.
  185. std::array<std::uint16_t, state::build_data::kSubclassGroupSize> group{};
  186. std::array<std::uint16_t, state::build_data::kSubclassGroupSize> members{};
  187. std::size_t memberCount = 1;
  188. members[0] = equippedItem.definitionIndex;
  189. if (state::build_data::find_subclass_group(equippedItem.definitionIndex, group)) {
  190. members = group;
  191. memberCount = group.size();
  192. }
  193. for (std::size_t member = 0; member < memberCount && count < output.size(); ++member) {
  194. const std::uint16_t memberDefinitionIndex = members[member];
  195. state::build_data::items::details::Definition memberDetail{};
  196. if (!state::build_data::find_configured_item_detail(memberDefinitionIndex,
  197. memberDetail)) {
  198. continue;
  199. }
  200. publish(character, memberDetail.socketEntryListIndex, defaultSelection);
  201. // Each owned subclass remembers its own picks, so every member is checked for a
  202. // non-default selection to publish on top of its default row. A fresh boot needs the
  203. // actual selection for subclasses configured before this boot.
  204. const domain::Selection* memberSelection = nullptr;
  205. domain::Selection resolvedSelection{};
  206. if (memberDefinitionIndex == equippedItem.definitionIndex) {
  207. resolvedSelection = selection_of(*equippedSlot);
  208. memberSelection = &resolvedSelection;
  209. } else {
  210. state::build_data::items::Definition memberItemDefinition{};
  211. if (state::build_data::find_item_definition_index(memberDefinitionIndex,
  212. memberItemDefinition)) {
  213. const auto& inventory = account.characters[character].inventory;
  214. for (std::size_t itemIndex = 0; itemIndex < inventory.count; ++itemIndex) {
  215. if (inventory.values[itemIndex].definitionHash
  216. == memberItemDefinition.definitionHash) {
  217. resolvedSelection = selection_of(inventory.values[itemIndex]);
  218. memberSelection = &resolvedSelection;
  219. break;
  220. }
  221. }
  222. }
  223. }
  224. if (memberSelection != nullptr && !(*memberSelection == defaultSelection)) {
  225. publish(character, memberDetail.socketEntryListIndex, *memberSelection);
  226. }
  227. }
  228. }
  229. if (count == 0) {
  230. report_ability_failure("empty", account.characterCount, rows.count, output.size());
  231. }
  232. return true;
  233. }
  234. } // namespace sunrise::client::content::items::packages