account_state.cpp 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. #include "account_state.h"
  2. #include <algorithm>
  3. #include <array>
  4. #include <cmath>
  5. namespace sunrise::state::account {
  6. namespace {
  7. /** Enough fixed storage for every account, character, profile-stack, and character-item key. */
  8. inline constexpr std::size_t kIdentityCapacity =
  9. 1 + inventory::kProfileItemCapacity
  10. + kCharacterCapacity * (1 + inventory::kEquipmentSlotCount + inventory::kCharacterItemCapacity);
  11. /** @return True when a profile row carries no authored or runtime-owned value. */
  12. [[nodiscard]] bool empty_profile_item(const inventory::ProfileItem& item) noexcept {
  13. return item.instanceSoid == 0 && item.definitionHash == 0 && item.quantity == 0
  14. && item.mutationSerial == 0;
  15. }
  16. /** Tier bits 1-5 are the native rarity ladder; bit 0 (no tier) is never a payout target. */
  17. constexpr std::uint8_t kDismantleTierMaskBits = 0b0011'1110U;
  18. constexpr std::uint8_t kDismantleClassMaskBits =
  19. static_cast<std::uint8_t>(DismantleGearClass::weapon)
  20. | static_cast<std::uint8_t>(DismantleGearClass::armor);
  21. /** @return True when one unused dismantle policy row is canonical zero. */
  22. [[nodiscard]] bool empty_dismantle_reward(const DismantleRewardPolicy& reward) noexcept {
  23. return reward.definitionHash == 0 && reward.quantity == 0 && reward.tierMask == 0
  24. && reward.classMask == 0 && reward.masterwork == DismantleMasterworkFilter::any;
  25. }
  26. /** Checks filled policy rows, uniqueness, and the zero tail. */
  27. [[nodiscard]] bool valid_dismantle_rewards(const AccountState& state) noexcept {
  28. if (state.dismantleRewardCount > state.dismantleRewards.size()) {
  29. return false;
  30. }
  31. for (std::size_t index = 0; index < state.dismantleRewardCount; ++index) {
  32. const DismantleRewardPolicy& reward = state.dismantleRewards[index];
  33. if (reward.definitionHash == inventory::kNoDefinitionHash || reward.quantity <= 0
  34. || (reward.tierMask & ~kDismantleTierMaskBits) != 0
  35. || (reward.classMask & ~kDismantleClassMaskBits) != 0
  36. || reward.masterwork > DismantleMasterworkFilter::notMasterworked) {
  37. return false;
  38. }
  39. for (std::size_t prior = 0; prior < index; ++prior) {
  40. if (same_dismantle_policy_key(state.dismantleRewards[prior], reward)) {
  41. return false;
  42. }
  43. }
  44. }
  45. const auto tail =
  46. state.dismantleRewards.cbegin() + static_cast<std::ptrdiff_t>(state.dismantleRewardCount);
  47. return std::all_of(tail, state.dismantleRewards.cend(), empty_dismantle_reward);
  48. }
  49. [[nodiscard]] bool empty_record_reward(const RecordRewardPolicy& reward) noexcept {
  50. return reward.recordIndex == 0 && reward.itemIndex == 0 && reward.quantity == 0;
  51. }
  52. [[nodiscard]] bool valid_record_rewards(const AccountState& state) noexcept {
  53. if (state.recordRewardCount > state.recordRewards.size()) {
  54. return false;
  55. }
  56. for (std::size_t index = 0; index < state.recordRewardCount; ++index) {
  57. const RecordRewardPolicy& reward = state.recordRewards[index];
  58. if (reward.quantity <= 0) {
  59. return false;
  60. }
  61. for (std::size_t prior = 0; prior < index; ++prior) {
  62. if (same_record_reward_key(state.recordRewards[prior], reward)) {
  63. return false;
  64. }
  65. }
  66. }
  67. const auto tail =
  68. state.recordRewards.cbegin() + static_cast<std::ptrdiff_t>(state.recordRewardCount);
  69. return std::all_of(tail, state.recordRewards.cend(), empty_record_reward);
  70. }
  71. /** Adds one nonzero key to the bounded identity buffer. */
  72. [[nodiscard]] bool append_identity(std::array<std::uint64_t, kIdentityCapacity>& identities,
  73. std::size_t& count,
  74. std::uint64_t soid) noexcept {
  75. if (soid == 0 || count >= identities.size()) {
  76. return false;
  77. }
  78. identities[count++] = soid;
  79. return true;
  80. }
  81. /** Checks the complete authored/runtime structure without consulting installed build data. */
  82. [[nodiscard]] bool valid_impl(const AccountState& state) noexcept {
  83. if (state.profileItemCount > state.profileItems.size()
  84. || state.characterCount > state.characters.size()) {
  85. return false;
  86. }
  87. if (state.primarySoid == 0) {
  88. if (state.profileItemCount != 0 || state.characterCount != 0
  89. || state.dismantleRewardCount != 0 || state.recordRewardCount != 0
  90. || state.settings.configured || state.settings.keyBindings.configured) {
  91. return false;
  92. }
  93. return std::all_of(
  94. state.profileItems.cbegin(), state.profileItems.cend(), empty_profile_item)
  95. && std::all_of(state.dismantleRewards.cbegin(),
  96. state.dismantleRewards.cend(),
  97. empty_dismantle_reward)
  98. && std::all_of(
  99. state.recordRewards.cbegin(), state.recordRewards.cend(), empty_record_reward);
  100. }
  101. if (!settings::valid(state.settings) || !valid_dismantle_rewards(state)
  102. || !valid_record_rewards(state)) {
  103. return false;
  104. }
  105. std::array<std::uint64_t, kIdentityCapacity> identities{};
  106. std::size_t identityCount = 0;
  107. if (!append_identity(identities, identityCount, state.primarySoid)) {
  108. return false;
  109. }
  110. for (std::size_t index = 0; index < state.profileItemCount; ++index) {
  111. const inventory::ProfileItem& item = state.profileItems[index];
  112. if (item.definitionHash == inventory::kNoDefinitionHash || item.quantity <= 0
  113. || item.mutationSerial < 0
  114. || (item.instanceSoid != 0
  115. && !append_identity(identities, identityCount, item.instanceSoid))) {
  116. return false;
  117. }
  118. }
  119. const auto profileTail =
  120. state.profileItems.cbegin() + static_cast<std::ptrdiff_t>(state.profileItemCount);
  121. if (!std::all_of(profileTail, state.profileItems.cend(), empty_profile_item)) {
  122. return false;
  123. }
  124. bool selected = false;
  125. for (std::size_t index = 0; index < state.characterCount; ++index) {
  126. const CharacterState& character = state.characters[index];
  127. if (!append_identity(identities, identityCount, character.soid)
  128. || (character.selected && selected) || character.race > CharacterRace::exo
  129. || character.gender > CharacterGender::female
  130. || character.characterClass > CharacterClass::warlock
  131. || !std::isfinite(character.appearanceValue) || !inventory::valid(character.equipment)
  132. || !inventory::valid(character.inventory) || !inventory::valid(character.stacks)) {
  133. return false;
  134. }
  135. selected = selected || character.selected;
  136. for (const std::optional<inventory::Item>& item : character.equipment.slots) {
  137. if (item.has_value()
  138. && !append_identity(identities, identityCount, item->instanceSoid)) {
  139. return false;
  140. }
  141. }
  142. for (std::size_t itemIndex = 0; itemIndex < character.inventory.count; ++itemIndex) {
  143. if (!append_identity(identities,
  144. identityCount,
  145. character.inventory.values[itemIndex].instanceSoid)) {
  146. return false;
  147. }
  148. }
  149. }
  150. auto end = identities.begin() + static_cast<std::ptrdiff_t>(identityCount);
  151. std::sort(identities.begin(), end);
  152. return std::adjacent_find(identities.begin(), end) == end;
  153. }
  154. } // namespace
  155. /**
  156. * Checks bounded ids and whole settings for every nonzero account.
  157. * @param state Account State to check.
  158. * @return True for empty State, or a whole account with unique ids.
  159. */
  160. bool valid(const AccountState& state) noexcept {
  161. return valid_impl(state);
  162. }
  163. /** Checks settings-authored State before runtime-only profile stack identities are seeded. */
  164. bool valid_authored(const AccountState& state) noexcept {
  165. return valid_impl(state);
  166. }
  167. /**
  168. * Finds the selected character without storing a second account-level key.
  169. * @param state Account snapshot read under the lock.
  170. * @return The selected character's nonzero SOID, or zero when none is selected.
  171. */
  172. std::uint64_t selected_character_soid(const AccountState& state) noexcept {
  173. const std::size_t count = (std::min)(state.characterCount, state.characters.size());
  174. for (std::size_t index = 0; index < count; ++index) {
  175. if (state.characters[index].selected) {
  176. return state.characters[index].soid;
  177. }
  178. }
  179. return 0;
  180. }
  181. /**
  182. * Finds the character the family-zero banner pair names.
  183. * The pair goes out before any pick, so it falls back to the first character.
  184. * @param state Account snapshot read under the lock.
  185. * @return The character's nonzero SOID, or zero when the account owns none.
  186. */
  187. std::uint64_t banner_character_soid(const AccountState& state) noexcept {
  188. const std::uint64_t selected = selected_character_soid(state);
  189. if (selected != 0) {
  190. return selected;
  191. }
  192. return state.characterCount == 0 ? 0 : state.characters[0].soid;
  193. }
  194. bool find_record_reward(const AccountState& state,
  195. std::uint16_t recordIndex,
  196. RecordRewardPolicy& reward) noexcept {
  197. const std::size_t count = (std::min)(state.recordRewardCount, state.recordRewards.size());
  198. for (std::size_t index = 0; index < count; ++index) {
  199. if (state.recordRewards[index].recordIndex == recordIndex) {
  200. reward = state.recordRewards[index];
  201. return true;
  202. }
  203. }
  204. return false;
  205. }
  206. } // namespace sunrise::state::account