state_account_reward_grant_runtime.cpp 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114
  1. /**
  2. * Grants that no purchase pays for: season pass rewards, record rewards, and the
  3. * default emote collection.
  4. */
  5. #include <array>
  6. #include <cstddef>
  7. #include <cstdint>
  8. #include <limits>
  9. #include "../../middleware/datagen/family4/account/layout.h"
  10. #include "../../middleware/datagen/family4/character/layout.h"
  11. #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
  12. #include "../build_data/runtime.h"
  13. #include "../investment/store_internal.h"
  14. #include "../rewards/reward_resolver.h"
  15. #include "../unlocks/unlocks_records.h"
  16. #include "runtime.h"
  17. #include "state_account_transaction_helpers.h"
  18. #include "storage/internal.h"
  19. namespace sunrise::state {
  20. using namespace runtime::detail;
  21. namespace authored_inventory = account::inventory;
  22. namespace item_details = build_data::items::details;
  23. namespace inventory_buckets = build_data::inventory::buckets;
  24. namespace family4_loadout = middleware::datagen::family4::loadout;
  25. namespace {
  26. /** Prepares inventory rows and consumed-wrapper flags against the same account view. */
  27. [[nodiscard]] RewardPreparation
  28. prepare_reward_grant(std::span<const DirectRecordReward> rewards,
  29. std::uint16_t claimedRecordIndex,
  30. const std::bitset<unlocks::kAccountFlagCapacity>* wrapperFlags,
  31. PendingRecordRewardGrant& mutation,
  32. const char** refusal) noexcept;
  33. [[nodiscard]] bool materialize_record_reward(const AccountState& current,
  34. const PendingRecordRewardGrant& mutation,
  35. AccountState& after) noexcept;
  36. /** Each native change list must fit independently; unlock flags consume neither list. */
  37. [[nodiscard]] const char*
  38. reward_change_refusal(std::span<const PreparedRecordReward> rewards) noexcept {
  39. std::size_t characterChanges = 0;
  40. std::size_t profileChanges = 0;
  41. for (const auto& reward : rewards) {
  42. switch (reward.kind) {
  43. case RecordRewardKind::characterInstance:
  44. case RecordRewardKind::characterStack:
  45. if (++characterChanges
  46. > middleware::datagen::family4::character::layout::kInventoryChangeRecordCapacity) {
  47. return "character_change_capacity";
  48. }
  49. break;
  50. case RecordRewardKind::profileStack:
  51. if (++profileChanges > middleware::datagen::family4::account::layout::
  52. kProfileInventoryChangeRecordCapacity) {
  53. return "profile_change_capacity";
  54. }
  55. break;
  56. case RecordRewardKind::accountUnlock:
  57. break;
  58. default:
  59. return "reward_kind";
  60. }
  61. }
  62. return nullptr;
  63. }
  64. /** A reward overrides socket types, so adding an ordinary lane cannot shift a fixed roll. */
  65. [[nodiscard]] bool
  66. apply_reward_sockets(const item_details::Definition& detail,
  67. std::span<const build_data::rewards::SocketOverride> overrides,
  68. authored_inventory::Sockets& sockets,
  69. const char** refusal = nullptr) noexcept {
  70. const auto fail = [&](const char* reason) noexcept {
  71. if (refusal != nullptr) {
  72. *refusal = reason;
  73. }
  74. return false;
  75. };
  76. if (overrides.empty()) {
  77. return true;
  78. }
  79. if (detail.ordinarySocketCount > sockets.plugs.size()) {
  80. return fail("socket_layout");
  81. }
  82. authored_inventory::Sockets staged{};
  83. staged.policy = authored_inventory::SocketPolicy::authored;
  84. staged.plugCount = detail.ordinarySocketCount;
  85. for (std::size_t lane = 0; lane < staged.plugCount; ++lane) {
  86. const auto index = detail.initialPlugIndices[lane];
  87. if (index == item_details::kUnavailableItemIndex) {
  88. continue;
  89. }
  90. build_data::items::Definition plug{};
  91. if (!build_data::find_item_definition_index(index, plug)) {
  92. return fail("socket_layout");
  93. }
  94. staged.plugs[lane] = plug.definitionHash;
  95. }
  96. std::array<bool, item_details::kInitialPlugCapacity> replaced{};
  97. for (const auto& override : overrides) {
  98. if (override.plugSet != build_data::rewards::kAbsent) {
  99. return fail("socket_plug_set");
  100. }
  101. if (override.rollSet != build_data::rewards::kAbsent) {
  102. return fail("socket_roll_set");
  103. }
  104. // Zero selection preserves the initial plug in every lane of the named type.
  105. const bool preserve =
  106. override.plugItem == build_data::rewards::kAbsent && override.selection == 0;
  107. if (!preserve && override.selection != build_data::rewards::kFixedPlugSelection) {
  108. return fail("socket_selection");
  109. }
  110. build_data::items::Definition plug{};
  111. if (!preserve && !build_data::find_item_definition_index(override.plugItem, plug)) {
  112. return fail("socket_layout");
  113. }
  114. bool found = false;
  115. for (std::size_t lane = 0; lane < staged.plugCount; ++lane) {
  116. if (detail.socketTypes[lane] != override.socketType) {
  117. continue;
  118. }
  119. if (replaced[lane] || (!preserve && found)) {
  120. return fail("socket_layout");
  121. }
  122. replaced[lane] = true;
  123. found = true;
  124. if (!preserve) {
  125. staged.plugs[lane] = plug.definitionHash;
  126. }
  127. }
  128. if (!found) {
  129. return fail("socket_layout");
  130. }
  131. }
  132. if (!authored_inventory::valid(staged)) {
  133. return fail("socket_layout");
  134. }
  135. sockets = staged;
  136. return true;
  137. }
  138. [[nodiscard]] RewardPreparation prepare_resolved_reward(const reward_resolution::Result& resolved,
  139. PendingRecordRewardGrant& mutation,
  140. const char** refusal) noexcept {
  141. std::array<DirectRecordReward, kRecordRewardGrantCapacity> rows{};
  142. if (resolved.count > rows.size()) {
  143. return RewardPreparation::unresolvable;
  144. }
  145. for (std::size_t i = 0; i < resolved.count; ++i) {
  146. const auto& grant = resolved.grants[i];
  147. rows[i] = {grant.itemIndex,
  148. grant.quantity,
  149. std::span(grant.sockets).first(grant.socketCount),
  150. true};
  151. }
  152. return prepare_reward_grant(std::span(rows).first(resolved.count),
  153. kUnclaimedRecordIndex,
  154. &resolved.wrapperFlags,
  155. mutation,
  156. refusal);
  157. }
  158. /** Finds the kept unsocketed grant of this item, or null. */
  159. [[nodiscard]] reward_resolution::Grant* kept_stack(reward_resolution::Result& resolved,
  160. std::size_t kept,
  161. std::uint16_t itemIndex) noexcept {
  162. for (std::size_t prior = 0; prior < kept; ++prior) {
  163. auto& grant = resolved.grants[prior];
  164. if (grant.itemIndex == itemIndex && grant.socketCount == 0) {
  165. return &grant;
  166. }
  167. }
  168. return nullptr;
  169. }
  170. /** Folds repeat draws of a stackable item into its first draw; false when one stack overflows. */
  171. [[nodiscard]] bool consolidate_stacks(reward_resolution::Result& resolved) noexcept {
  172. std::size_t kept = 0;
  173. for (std::size_t index = 0; index < resolved.count; ++index) {
  174. const reward_resolution::Grant grant = resolved.grants[index];
  175. item_details::Definition detail{};
  176. const bool stackable =
  177. grant.socketCount == 0
  178. && build_data::find_configured_item_detail(grant.itemIndex, detail)
  179. && detail.instancedDefinitionState == item_details::InstancedDefinitionState::stackable;
  180. auto* const merged = stackable ? kept_stack(resolved, kept, grant.itemIndex) : nullptr;
  181. if (merged == nullptr) {
  182. resolved.grants[kept++] = grant;
  183. continue;
  184. }
  185. if (static_cast<std::int64_t>(merged->quantity) + grant.quantity > detail.maxStackSize) {
  186. return false;
  187. }
  188. merged->quantity += grant.quantity;
  189. }
  190. resolved.count = kept;
  191. return true;
  192. }
  193. /** Classifies a catalog refusal: a verdict unless a catalog was cleared meanwhile. */
  194. [[nodiscard]] RewardPreparation catalog_refusal(const char*& reason) noexcept {
  195. if (build_data::item_definitions_ready() && build_data::reward_definitions_ready()) {
  196. return RewardPreparation::unresolvable;
  197. }
  198. reason = "not_ready";
  199. return RewardPreparation::deferred;
  200. }
  201. /** Wrappers and direct account perks bypass quest initialization. */
  202. bool uses_reward_definition(const build_data::items::Definition& item) noexcept {
  203. build_data::rewards::Item reward{};
  204. if (!build_data::find_reward_item(item.definitionIndex, reward)) {
  205. return false;
  206. }
  207. return reward.poolIndex != build_data::rewards::kAbsent
  208. || (item.bucketId == inventory_buckets::kPerkBucketId
  209. && reward.acquiredFlag != build_data::rewards::kAbsent);
  210. }
  211. enum class PassResolution { claim, replay };
  212. [[nodiscard]] bool resolve_pass(const build_data::season_pass::Reward& reward,
  213. const AccountState& account,
  214. std::uint64_t seed,
  215. reward_resolution::Result& result,
  216. PassResolution resolution,
  217. const char** reason = nullptr) noexcept {
  218. const auto character = selected_character_index(account);
  219. unlocks::Table flags{};
  220. build_data::rewards::Item source{};
  221. if (character >= account.characterCount
  222. || !investment::store::read_unlocks(flags, static_cast<int>(character))
  223. || !build_data::find_reward_item(reward.itemIndex, source)
  224. || source.definitionHash != reward.itemHash || reward.socketCount > reward.sockets.size()
  225. || reward.conditionCount > reward.condition.size()) {
  226. return false;
  227. }
  228. if (reason != nullptr) {
  229. *reason = "reward_condition";
  230. }
  231. if (resolution == PassResolution::replay && reward.claimFlagIndex < flags.accountFlags.size()) {
  232. flags.accountFlags[reward.claimFlagIndex] = unlocks::kFlagClear;
  233. }
  234. bool enabled = false;
  235. if (!reward_resolution::eligible(
  236. std::span(reward.condition).first(reward.conditionCount),
  237. {flags, account.characters[character].characterClass, seed, reason},
  238. enabled)
  239. || !enabled) {
  240. return false;
  241. }
  242. if (reward_resolution::resolve(
  243. {flags, account.characters[character].characterClass, seed, reason},
  244. reward.itemIndex,
  245. reward.quantity,
  246. result)
  247. != reward_resolution::Resolution::resolved) {
  248. return false;
  249. }
  250. if (!consolidate_stacks(result)) {
  251. if (reason != nullptr) {
  252. *reason = "stack_capacity";
  253. }
  254. return false;
  255. }
  256. if (reward.socketCount != 0) {
  257. if (result.count != 1 || result.grants[0].itemIndex != reward.itemIndex) {
  258. if (reason != nullptr) {
  259. *reason = "socket_owner";
  260. }
  261. return false;
  262. }
  263. result.grants[0].sockets = reward.sockets;
  264. result.grants[0].socketCount = reward.socketCount;
  265. }
  266. return true;
  267. }
  268. [[nodiscard]] bool reward_matches(const build_data::season_pass::Reward& reward,
  269. const PendingSeasonPassReward& mutation) noexcept {
  270. const auto* grant = &mutation.grant;
  271. reward_resolution::Result expected{};
  272. if (!mutation.prepared || mutation.sourceDefinitionHash != reward.itemHash
  273. || !resolve_pass(
  274. reward, account_snapshot(), mutation.seed, expected, PassResolution::replay)
  275. || expected.count != grant->rewardCount
  276. || expected.wrapperFlags.count() != grant->wrapperFlags.size()) {
  277. return false;
  278. }
  279. for (const auto& flag : grant->wrapperFlags) {
  280. if (flag.index >= expected.wrapperFlags.size() || !expected.wrapperFlags[flag.index]) {
  281. return false;
  282. }
  283. }
  284. for (std::size_t i = 0; i < expected.count; ++i) {
  285. const auto& planned = expected.grants[i];
  286. const auto& prepared = grant->rewards[i];
  287. build_data::items::Definition definition{};
  288. build_data::rewards::Item source{};
  289. if (!build_data::find_item_definition_index(planned.itemIndex, definition)
  290. || !build_data::find_reward_item(planned.itemIndex, source)
  291. || prepared.definitionHash != definition.definitionHash
  292. || prepared.quantity != planned.quantity
  293. || prepared.acquiredFlag != source.acquiredFlag) {
  294. return false;
  295. }
  296. if (planned.socketCount != 0) {
  297. item_details::Definition detail{};
  298. authored_inventory::Sockets sockets{};
  299. if (prepared.kind != RecordRewardKind::characterInstance
  300. || prepared.stateIndex >= grant->afterCharacter.inventory.count
  301. || !build_data::find_configured_item_detail(planned.itemIndex, detail)
  302. || !apply_reward_sockets(
  303. detail, std::span(planned.sockets).first(planned.socketCount), sockets)) {
  304. return false;
  305. }
  306. const auto& installed =
  307. grant->afterCharacter.inventory.values[prepared.stateIndex].sockets;
  308. if (installed.policy != sockets.policy || installed.plugCount != sockets.plugCount
  309. || installed.plugs != sockets.plugs) {
  310. return false;
  311. }
  312. }
  313. }
  314. return true;
  315. }
  316. } // namespace
  317. bool prepare_season_pass_reward(std::uint16_t rewardIndex,
  318. std::uint64_t seed,
  319. PendingSeasonPassReward& mutation,
  320. const char** refusal) noexcept {
  321. const char* unused = nullptr;
  322. auto& reason = refusal != nullptr ? *refusal : unused;
  323. reason = "reward_index";
  324. mutation = {};
  325. const std::lock_guard lock(investment::store::g_mutex);
  326. build_data::season_pass::Reward reward{};
  327. if (!build_data::find_season_pass_reward(rewardIndex, reward)) {
  328. return false;
  329. }
  330. reason = "rank";
  331. if (reward.requiredRank > seasonal_rank()) {
  332. return false;
  333. }
  334. reason = "already_claimed";
  335. if (season_pass_reward_claimed(rewardIndex)) {
  336. return false;
  337. }
  338. mutation.seed = seed;
  339. reward_resolution::Result resolved{};
  340. reason = "reward_condition";
  341. if (!resolve_pass(
  342. reward, account_snapshot(), mutation.seed, resolved, PassResolution::claim, &reason)) {
  343. return false;
  344. }
  345. reason = "reward_placement";
  346. if (prepare_resolved_reward(resolved, mutation.grant, &reason) != RewardPreparation::prepared) {
  347. return false;
  348. }
  349. reason = nullptr;
  350. mutation.sourceDefinitionHash = reward.itemHash;
  351. mutation.rewardIndex = rewardIndex;
  352. mutation.prepared = true;
  353. return true;
  354. }
  355. ItemGrantRoute item_grant_route(std::uint16_t itemIndex) noexcept {
  356. build_data::items::Definition item{};
  357. if (!build_data::find_item_definition_index(itemIndex, item)) {
  358. return ItemGrantRoute::unavailable;
  359. }
  360. if (uses_reward_definition(item)) {
  361. return ItemGrantRoute::reward;
  362. }
  363. if (item.questInitialization.scope != build_data::items::QuestInitialization::Scope::none) {
  364. return ItemGrantRoute::quest;
  365. }
  366. item_details::Definition detail{};
  367. inventory_buckets::Descriptor bucket{};
  368. if (!build_data::find_configured_item_detail(itemIndex, detail)
  369. || !build_data::find_inventory_bucket_descriptor(item.bucketId, bucket)) {
  370. return ItemGrantRoute::unavailable;
  371. }
  372. if (bucket.arraySelector == inventory_buckets::ArraySelector::profile
  373. && detail.instancedDefinitionState == item_details::InstancedDefinitionState::stackable) {
  374. return ItemGrantRoute::profile;
  375. }
  376. return bucket.arraySelector == inventory_buckets::ArraySelector::character
  377. ? ItemGrantRoute::reward
  378. : ItemGrantRoute::unavailable;
  379. }
  380. RewardPreparation prepare_item_reward(std::uint16_t itemIndex,
  381. std::uint32_t quantity,
  382. std::uint64_t seed,
  383. PendingRecordRewardGrant& mutation,
  384. const char** refusal) noexcept {
  385. const char* unused = nullptr;
  386. auto& reason = refusal != nullptr ? *refusal : unused;
  387. reason = "not_ready";
  388. mutation = {};
  389. // Unpublished domains decide nothing; retry later.
  390. if (!build_data::item_definitions_ready() || !build_data::reward_definitions_ready()) {
  391. return RewardPreparation::deferred;
  392. }
  393. reason = "item_definition";
  394. const std::lock_guard lock(investment::store::g_mutex);
  395. build_data::items::Definition item{};
  396. if (!build_data::find_item_definition_index(itemIndex, item)) {
  397. return catalog_refusal(reason);
  398. }
  399. reason = "selected_character";
  400. const AccountState account = account_snapshot();
  401. const auto character = selected_character_index(account);
  402. unlocks::Table flags{};
  403. if (character >= account.characterCount
  404. || !investment::store::read_unlocks(flags, static_cast<int>(character))) {
  405. return RewardPreparation::deferred;
  406. }
  407. reward_resolution::Result resolved{};
  408. const auto resolution = reward_resolution::resolve(
  409. {flags, account.characters[character].characterClass, seed, &reason},
  410. itemIndex,
  411. quantity,
  412. resolved);
  413. // Unlocks can change and another draw can fit, so these are retried, not retired.
  414. if (resolution == reward_resolution::Resolution::ineligible
  415. || resolution == reward_resolution::Resolution::exceeded) {
  416. return RewardPreparation::deferred;
  417. }
  418. if (resolution != reward_resolution::Resolution::resolved) {
  419. return catalog_refusal(reason);
  420. }
  421. reason = "stack_capacity";
  422. if (!consolidate_stacks(resolved)) {
  423. return RewardPreparation::deferred;
  424. }
  425. reason = "reward_placement";
  426. return prepare_resolved_reward(resolved, mutation, &reason);
  427. }
  428. bool preview_reward_unlocks(const PendingRecordRewardGrant& mutation,
  429. unlocks::Table& after) noexcept {
  430. if (!mutation.prepared || mutation.rewardCount > mutation.rewards.size()
  431. || !investment::store::read_unlocks(after, static_cast<int>(mutation.characterIndex))) {
  432. return false;
  433. }
  434. for (std::size_t i = 0; i < mutation.rewardCount; ++i) {
  435. const auto& reward = mutation.rewards[i];
  436. if (reward.acquiredFlag == build_data::rewards::kAbsent) {
  437. continue;
  438. }
  439. if (reward.acquiredFlag >= after.accountFlags.size()
  440. || after.accountFlags[reward.acquiredFlag] != reward.previousFlag) {
  441. return false;
  442. }
  443. }
  444. for (std::size_t i = 0; i < mutation.wrapperFlags.size(); ++i) {
  445. const auto& flag = mutation.wrapperFlags[i];
  446. if (flag.index >= after.accountFlags.size() || flag.previousValue > unlocks::kFlagSet
  447. || (i != 0 && mutation.wrapperFlags[i - 1].index >= flag.index)
  448. || after.accountFlags[flag.index] != flag.previousValue) {
  449. return false;
  450. }
  451. }
  452. for (const auto& flag : mutation.wrapperFlags) {
  453. after.accountFlags[flag.index] = unlocks::kFlagSet;
  454. }
  455. for (std::size_t i = 0; i < mutation.rewardCount; ++i) {
  456. const auto flag = mutation.rewards[i].acquiredFlag;
  457. if (flag != build_data::rewards::kAbsent) {
  458. after.accountFlags[flag] = unlocks::kFlagSet;
  459. }
  460. }
  461. return true;
  462. }
  463. /** Revalidates the prepared reward under the lock that commits its inventory and flags. */
  464. bool commit_season_pass_reward(PendingSeasonPassReward& mutation) noexcept {
  465. if (!mutation.prepared) {
  466. return false;
  467. }
  468. const PendingConsumption consume{mutation};
  469. bool ready = false;
  470. {
  471. investment::store::Transaction transaction;
  472. build_data::season_pass::Reward reward{};
  473. ready = transaction.ready()
  474. && build_data::find_season_pass_reward(mutation.rewardIndex, reward)
  475. && season_pass_reward_claimed(mutation.rewardIndex)
  476. && reward.requiredRank <= seasonal_rank() && reward_matches(reward, mutation)
  477. && commit_record_reward(mutation.grant) && transaction.commit();
  478. }
  479. if (!ready) {
  480. revoke_season_pass_reward(mutation.rewardIndex);
  481. }
  482. return ready;
  483. }
  484. namespace {
  485. /**
  486. * Rebuilds the account after-image for one prepared reward set and rejects any drift.
  487. * @param current Live account the mutation was prepared against.
  488. * @param after Receives the after-image; its content is unusable on failure.
  489. * @return False when the account moved or a reward row breaks its own item rules.
  490. */
  491. [[nodiscard]] bool materialize_record_reward(const AccountState& current,
  492. const PendingRecordRewardGrant& mutation,
  493. AccountState& after) noexcept {
  494. if (!mutation.prepared || (mutation.rewardCount == 0 && mutation.wrapperFlags.empty())
  495. || mutation.rewardCount > mutation.rewards.size()
  496. || reward_change_refusal(std::span(mutation.rewards).first(mutation.rewardCount)) != nullptr
  497. || mutation.accountSoid == 0 || mutation.characterSoid == 0
  498. || mutation.characterIndex >= current.characterCount
  499. || current.primarySoid != mutation.accountSoid
  500. || !same_character(current.characters[mutation.characterIndex], mutation.beforeCharacter)
  501. || !same_profile_inventory(
  502. current, mutation.beforeProfileItems, mutation.beforeProfileItemCount)
  503. || !mutation.beforeCharacter.selected
  504. || mutation.beforeCharacter.soid != mutation.characterSoid) {
  505. return false;
  506. }
  507. after = current;
  508. after.characters[mutation.characterIndex] = mutation.afterCharacter;
  509. after.profileItems = mutation.afterProfileItems;
  510. after.profileItemCount = mutation.afterProfileItemCount;
  511. family4_loadout::ResolvedLoadout loadout{};
  512. if (!account::valid(after) || !valid_profile_inventory(after)
  513. || !family4_loadout::resolve(after, mutation.characterIndex, loadout)) {
  514. return false;
  515. }
  516. for (std::size_t index = 0; index < mutation.rewardCount; ++index) {
  517. const PreparedRecordReward& reward = mutation.rewards[index];
  518. build_data::items::Definition item{};
  519. item_details::Definition detail{};
  520. inventory_buckets::Descriptor bucket{};
  521. if (reward.definitionHash == authored_inventory::kNoDefinitionHash || reward.quantity <= 0
  522. || reward.afterQuantity < reward.quantity || reward.mutationSerial < 0
  523. || !build_data::find_item_definition_hash(reward.definitionHash, item)) {
  524. return false;
  525. }
  526. if (reward.kind == RecordRewardKind::accountUnlock) {
  527. build_data::rewards::Item source{};
  528. if (reward.quantity != 1 || reward.afterQuantity != 1 || reward.instanceSoid != 0
  529. || item.bucketId != build_data::inventory::buckets::kPerkBucketId
  530. || !build_data::find_reward_item(item.definitionIndex, source)
  531. || source.acquiredFlag == build_data::rewards::kAbsent
  532. || source.acquiredFlag != reward.acquiredFlag) {
  533. return false;
  534. }
  535. continue;
  536. }
  537. if (!build_data::find_configured_item_detail(item.definitionIndex, detail)
  538. || detail.definitionIndex != item.definitionIndex
  539. || detail.definitionHash != item.definitionHash || detail.bucketId != item.bucketId
  540. || !build_data::find_inventory_bucket_descriptor(item.bucketId, bucket)) {
  541. return false;
  542. }
  543. if (reward.kind == RecordRewardKind::characterInstance) {
  544. if (reward.quantity != 1 || reward.afterQuantity != 1 || reward.instanceSoid == 0
  545. || reward.appendedProfileResident
  546. || detail.instancedDefinitionState
  547. != item_details::InstancedDefinitionState::instanced
  548. || bucket.arraySelector != inventory_buckets::ArraySelector::character
  549. || reward.stateIndex >= mutation.afterCharacter.inventory.count) {
  550. return false;
  551. }
  552. const auto& granted = mutation.afterCharacter.inventory.values[reward.stateIndex];
  553. std::uint16_t row = 0;
  554. std::uint8_t slot = 0;
  555. if (granted.instanceSoid != reward.instanceSoid
  556. || granted.definitionHash != reward.definitionHash || granted.quantity != 1
  557. || granted.mutationSerial != reward.mutationSerial
  558. || !find_unequipped_row(loadout, reward.instanceSoid, row, slot)
  559. || row != reward.inventoryRow) {
  560. return false;
  561. }
  562. } else if (reward.kind == RecordRewardKind::characterStack) {
  563. if (reward.instanceSoid != 0 || reward.appendedProfileResident
  564. || detail.equipmentSlot.has_value()
  565. || detail.instancedDefinitionState
  566. != item_details::InstancedDefinitionState::stackable
  567. || bucket.arraySelector != inventory_buckets::ArraySelector::character
  568. || reward.afterQuantity > detail.maxStackSize
  569. || reward.stateIndex >= mutation.afterCharacter.stacks.count) {
  570. return false;
  571. }
  572. const auto& granted = mutation.afterCharacter.stacks.values[reward.stateIndex];
  573. if (granted.definitionHash != reward.definitionHash
  574. || granted.quantity != reward.afterQuantity
  575. || granted.mutationSerial != reward.mutationSerial) {
  576. return false;
  577. }
  578. } else {
  579. if (reward.kind != RecordRewardKind::profileStack
  580. || detail.instancedDefinitionState
  581. != item_details::InstancedDefinitionState::stackable
  582. || bucket.arraySelector != inventory_buckets::ArraySelector::profile
  583. || reward.afterQuantity > detail.maxStackSize
  584. || reward.stateIndex >= mutation.afterProfileItemCount) {
  585. return false;
  586. }
  587. const auto& granted = mutation.afterProfileItems[reward.stateIndex];
  588. const bool actionSource =
  589. build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
  590. if (granted.instanceSoid != reward.instanceSoid
  591. || granted.definitionHash != reward.definitionHash
  592. || granted.quantity != reward.afterQuantity
  593. || granted.mutationSerial != reward.mutationSerial
  594. || actionSource != (reward.instanceSoid != 0)
  595. || reward.appendedProfileResident
  596. != (actionSource && reward.stateIndex >= mutation.beforeProfileItemCount)) {
  597. return false;
  598. }
  599. }
  600. }
  601. return true;
  602. }
  603. /** Captures only flags carried by consumed wrappers; storage failure remains retryable. */
  604. [[nodiscard]] bool prepare_wrapper_flags(const std::bitset<unlocks::kAccountFlagCapacity>* flags,
  605. PendingRecordRewardGrant& mutation,
  606. const char*& reason) noexcept {
  607. if (flags == nullptr) {
  608. return true;
  609. }
  610. try {
  611. mutation.wrapperFlags.reserve(flags->count());
  612. for (std::size_t index = 0; index < flags->size(); ++index) {
  613. if (!(*flags)[index]) {
  614. continue;
  615. }
  616. reason = "wrapper_acquisition_flag";
  617. std::int32_t before = 0;
  618. if (!investment::store::read_unlock(investment::store::Bank::accountFlags,
  619. static_cast<std::uint16_t>(index),
  620. before)
  621. || before < 0 || before > unlocks::kFlagSet) {
  622. return false;
  623. }
  624. mutation.wrapperFlags.push_back(
  625. {static_cast<std::uint16_t>(index), static_cast<std::uint8_t>(before)});
  626. }
  627. } catch (...) {
  628. reason = "wrapper_flag_storage";
  629. return false;
  630. }
  631. return true;
  632. }
  633. /** Prepares inventory rows and consumed-wrapper flags against the same account view. */
  634. RewardPreparation
  635. prepare_reward_grant(std::span<const DirectRecordReward> rewards,
  636. std::uint16_t claimedRecordIndex,
  637. const std::bitset<unlocks::kAccountFlagCapacity>* wrapperFlags,
  638. PendingRecordRewardGrant& mutation,
  639. const char** refusal) noexcept {
  640. const char* unused = nullptr;
  641. auto& reason = refusal != nullptr ? *refusal : unused;
  642. reason = "reward_count";
  643. mutation = {};
  644. if ((rewards.empty() && (wrapperFlags == nullptr || wrapperFlags->none()))
  645. || rewards.size() > mutation.rewards.size()) {
  646. return RewardPreparation::unresolvable;
  647. }
  648. reason = "not_ready";
  649. // The loadout check needs these domains, so nothing is decided before they publish.
  650. if (!build_data::item_definitions_ready() || !build_data::configured_item_details_ready()
  651. || !build_data::inventory_bucket_descriptors_ready()
  652. || !build_data::socket_entry_lists_ready()) {
  653. return RewardPreparation::deferred;
  654. }
  655. reason = "account_state";
  656. const AccountState account = account_snapshot();
  657. const std::size_t characterIndex = selected_character_index(account);
  658. if (!account::valid(account) || !valid_profile_inventory(account)
  659. || characterIndex >= account.characterCount) {
  660. return RewardPreparation::deferred;
  661. }
  662. if (!prepare_wrapper_flags(wrapperFlags, mutation, reason)) {
  663. return RewardPreparation::deferred;
  664. }
  665. AccountState working = account;
  666. for (std::size_t index = 0; index < rewards.size(); ++index) {
  667. reason = "item_identity";
  668. const DirectRecordReward& requested = rewards[index];
  669. build_data::items::Definition item{};
  670. item_details::Definition detail{};
  671. inventory_buckets::Descriptor bucket{};
  672. if (requested.quantity <= 0
  673. || !build_data::find_item_definition_index(requested.itemDefinitionIndex, item)
  674. || item.questInitialization.scope
  675. != build_data::items::QuestInitialization::Scope::none) {
  676. return RewardPreparation::unresolvable;
  677. }
  678. PreparedRecordReward prepared{};
  679. prepared.definitionHash = item.definitionHash;
  680. prepared.quantity = requested.quantity;
  681. if (requested.acquireUnlock) {
  682. reason = "acquisition_flag";
  683. // Only a resolved reward asks for its flag, so the graph is published.
  684. build_data::rewards::Item source{};
  685. if (!build_data::find_reward_item(item.definitionIndex, source)
  686. || source.definitionHash != item.definitionHash) {
  687. return RewardPreparation::unresolvable;
  688. }
  689. prepared.acquiredFlag = source.acquiredFlag;
  690. if (source.acquiredFlag != build_data::rewards::kAbsent) {
  691. std::int32_t before = 0;
  692. // The saved flag refuses here, not the definition.
  693. if (!investment::store::read_unlock(
  694. investment::store::Bank::accountFlags, source.acquiredFlag, before)
  695. || before < 0 || before > unlocks::kFlagSet) {
  696. return RewardPreparation::deferred;
  697. }
  698. prepared.previousFlag = static_cast<std::uint8_t>(before);
  699. }
  700. }
  701. if (item.bucketId == build_data::inventory::buckets::kPerkBucketId) {
  702. reason = "perk_acquisition";
  703. if (prepared.acquiredFlag == build_data::rewards::kAbsent || requested.quantity != 1
  704. || !requested.sockets.empty()) {
  705. return RewardPreparation::unresolvable;
  706. }
  707. prepared.kind = RecordRewardKind::accountUnlock;
  708. prepared.afterQuantity = 1;
  709. mutation.rewards[index] = prepared;
  710. continue;
  711. }
  712. reason = "item_detail";
  713. if (!build_data::find_configured_item_detail(requested.itemDefinitionIndex, detail)
  714. || detail.definitionIndex != item.definitionIndex
  715. || detail.definitionHash != item.definitionHash || detail.bucketId != item.bucketId
  716. || !build_data::find_inventory_bucket_descriptor(item.bucketId, bucket)) {
  717. return RewardPreparation::unresolvable;
  718. }
  719. reason = "duplicate_stack";
  720. if (detail.instancedDefinitionState == item_details::InstancedDefinitionState::stackable) {
  721. for (std::size_t prior = 0; prior < index; ++prior) {
  722. if (mutation.rewards[prior].definitionHash == item.definitionHash) {
  723. return RewardPreparation::unresolvable;
  724. }
  725. }
  726. }
  727. reason = "socket_owner";
  728. if (!requested.sockets.empty()
  729. && (bucket.arraySelector != inventory_buckets::ArraySelector::character
  730. || detail.instancedDefinitionState
  731. != item_details::InstancedDefinitionState::instanced)) {
  732. return RewardPreparation::unresolvable;
  733. }
  734. reason = "inventory_destination";
  735. if (bucket.arraySelector == inventory_buckets::ArraySelector::profile) {
  736. reason = "profile_capacity";
  737. if (detail.instancedDefinitionState != item_details::InstancedDefinitionState::stackable
  738. || requested.quantity > detail.maxStackSize) {
  739. return RewardPreparation::unresolvable;
  740. }
  741. PendingProfileItemAcquisition staged{};
  742. const bool actionSource =
  743. build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
  744. if (!finalize_profile_item_acquisition(working,
  745. working,
  746. item.definitionHash,
  747. detail,
  748. actionSource,
  749. requested.quantity,
  750. {.direct = true},
  751. staged)) {
  752. return RewardPreparation::deferred;
  753. }
  754. working.profileItems = staged.afterItems;
  755. working.profileItemCount = staged.afterItemCount;
  756. prepared.instanceSoid = staged.acquiredInstanceSoid;
  757. prepared.stateIndex = staged.profileIndex;
  758. prepared.afterQuantity = staged.acquiredQuantity;
  759. prepared.mutationSerial = staged.acquiredMutationSerial;
  760. prepared.kind = RecordRewardKind::profileStack;
  761. prepared.appendedProfileResident = staged.appended && staged.actionSource;
  762. } else if (bucket.arraySelector == inventory_buckets::ArraySelector::character
  763. && detail.instancedDefinitionState
  764. == item_details::InstancedDefinitionState::instanced) {
  765. reason = "instance_quantity";
  766. if (requested.quantity != 1) {
  767. return RewardPreparation::unresolvable;
  768. }
  769. reason = "instance_capacity";
  770. PendingItemAcquisition staged{};
  771. if (!finalize_item_acquisition(
  772. working, working, item.definitionHash, false, {.direct = true}, staged)) {
  773. return RewardPreparation::deferred;
  774. }
  775. if (!apply_reward_sockets(
  776. detail,
  777. requested.sockets,
  778. staged.afterCharacter.inventory.values[staged.inventoryIndex].sockets,
  779. &reason)) {
  780. return RewardPreparation::unresolvable;
  781. }
  782. working.characters[characterIndex] = staged.afterCharacter;
  783. prepared.instanceSoid = staged.acquiredInstanceSoid;
  784. prepared.stateIndex = staged.inventoryIndex;
  785. prepared.afterQuantity = 1;
  786. prepared.mutationSerial =
  787. staged.afterCharacter.inventory.values[staged.inventoryIndex].mutationSerial;
  788. prepared.inventoryRow = staged.inventoryRow;
  789. prepared.kind = RecordRewardKind::characterInstance;
  790. } else if (bucket.arraySelector == inventory_buckets::ArraySelector::character
  791. && detail.instancedDefinitionState
  792. == item_details::InstancedDefinitionState::stackable
  793. && !detail.equipmentSlot.has_value()) {
  794. reason = "character_stack_capacity";
  795. CharacterState& character = working.characters[characterIndex];
  796. if (requested.quantity > detail.maxStackSize) {
  797. return RewardPreparation::unresolvable;
  798. }
  799. if (character.nextInventorySerial
  800. >= static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())) {
  801. return RewardPreparation::deferred;
  802. }
  803. std::size_t stackIndex = character.stacks.count;
  804. for (std::size_t candidate = 0; candidate < character.stacks.count; ++candidate) {
  805. if (character.stacks.values[candidate].definitionHash == item.definitionHash) {
  806. stackIndex = candidate;
  807. break;
  808. }
  809. }
  810. const bool appended = stackIndex == character.stacks.count;
  811. if ((appended && stackIndex >= character.stacks.values.size())
  812. || (!appended
  813. && character.stacks.values[stackIndex].quantity
  814. > detail.maxStackSize - requested.quantity)) {
  815. return RewardPreparation::deferred;
  816. }
  817. auto& stack = character.stacks.values[stackIndex];
  818. if (appended) {
  819. stack.definitionHash = item.definitionHash;
  820. ++character.stacks.count;
  821. }
  822. stack.quantity += requested.quantity;
  823. stack.mutationSerial = static_cast<std::int32_t>(character.nextInventorySerial++);
  824. prepared.stateIndex = stackIndex;
  825. prepared.afterQuantity = stack.quantity;
  826. prepared.mutationSerial = stack.mutationSerial;
  827. prepared.kind = RecordRewardKind::characterStack;
  828. } else {
  829. return RewardPreparation::unresolvable;
  830. }
  831. mutation.rewards[index] = prepared;
  832. }
  833. reason = reward_change_refusal(std::span(mutation.rewards).first(rewards.size()));
  834. if (reason != nullptr) {
  835. // Only a wrapper draw can exceed a list, and its next draw may fit.
  836. return RewardPreparation::deferred;
  837. }
  838. reason = "loadout";
  839. // A full native bucket rejects the whole loadout; retry later.
  840. family4_loadout::ResolvedLoadout loadout{};
  841. if (!account::valid(working) || !valid_profile_inventory(working)
  842. || !family4_loadout::resolve(working, characterIndex, loadout)) {
  843. return RewardPreparation::deferred;
  844. }
  845. mutation.beforeCharacter = account.characters[characterIndex];
  846. mutation.afterCharacter = working.characters[characterIndex];
  847. mutation.beforeProfileItems = account.profileItems;
  848. mutation.afterProfileItems = working.profileItems;
  849. mutation.claimedRecordIndex = claimedRecordIndex;
  850. mutation.accountSoid = account.primarySoid;
  851. mutation.characterSoid = account.characters[characterIndex].soid;
  852. mutation.characterIndex = characterIndex;
  853. mutation.beforeProfileItemCount = account.profileItemCount;
  854. mutation.afterProfileItemCount = working.profileItemCount;
  855. mutation.rewardCount = rewards.size();
  856. reason = nullptr;
  857. mutation.prepared = true;
  858. return RewardPreparation::prepared;
  859. }
  860. } // namespace
  861. /** Prepares every direct reward over one cumulative account view. */
  862. RewardPreparation prepare_record_reward_grant(std::span<const DirectRecordReward> rewards,
  863. std::uint16_t claimedRecordIndex,
  864. PendingRecordRewardGrant& mutation,
  865. const char** refusal) noexcept {
  866. return prepare_reward_grant(rewards, claimedRecordIndex, nullptr, mutation, refusal);
  867. }
  868. bool preview_record_reward_grant(const PendingRecordRewardGrant& mutation,
  869. AccountState& after) noexcept {
  870. after = {};
  871. return materialize_record_reward(account_snapshot(), mutation, after);
  872. }
  873. /** Commits the shared reward after-image and claim together. */
  874. bool commit_record_reward(PendingRecordRewardGrant& mutation) noexcept {
  875. if (!mutation.prepared) {
  876. return false;
  877. }
  878. const PendingConsumption consume{mutation};
  879. bool ready = false;
  880. {
  881. investment::store::Transaction transaction;
  882. AccountState after{};
  883. unlocks::Table flags{};
  884. ready = transaction.ready()
  885. && materialize_record_reward(investment::store::account(), mutation, after)
  886. && preview_reward_unlocks(mutation, flags)
  887. && investment::store::write_account(after);
  888. for (std::size_t i = 0; ready && i < mutation.rewardCount; ++i) {
  889. const auto flag = mutation.rewards[i].acquiredFlag;
  890. if (flag != build_data::rewards::kAbsent) {
  891. ready = investment::store::write_unlock(
  892. investment::store::Bank::accountFlags, flag, unlocks::kFlagSet);
  893. }
  894. }
  895. for (const auto& flag : mutation.wrapperFlags) {
  896. if (!ready) {
  897. break;
  898. }
  899. ready = investment::store::write_unlock(
  900. investment::store::Bank::accountFlags, flag.index, unlocks::kFlagSet);
  901. }
  902. ready = ready && transaction.commit();
  903. }
  904. if (!ready && mutation.claimedRecordIndex != kUnclaimedRecordIndex) {
  905. // The claim was written when the reward was prepared, so a refused install undoes it.
  906. unlocks::records::revoke(mutation.claimedRecordIndex);
  907. }
  908. return ready;
  909. }
  910. namespace {
  911. /**
  912. * Emote definitions seeded into the collection item's wheel lanes.
  913. * All four are universal Common emotes, so no class or race can reject a seeded lane.
  914. */
  915. constexpr std::uint32_t kYesEmoteDefinitionHash = 3184938442U;
  916. constexpr std::uint32_t kNopeEmoteDefinitionHash = 48790291U;
  917. constexpr std::uint32_t kCasualSitEmoteDefinitionHash = 383973261U;
  918. constexpr std::uint32_t kCheerEmoteDefinitionHash = 2834933816U;
  919. /** Lane order is the client's own wheel layout. */
  920. constexpr std::array<std::uint32_t, authored_inventory::kEmoteCollectionSocketLaneCount>
  921. kEmoteCollectionDefaultPlugHashes{
  922. kCheerEmoteDefinitionHash, // lane 0 -- top
  923. kCasualSitEmoteDefinitionHash, // lane 1 -- bottom
  924. kYesEmoteDefinitionHash, // lane 2 -- left
  925. kNopeEmoteDefinitionHash, // lane 3 -- right
  926. };
  927. /**
  928. * Resolves and cross-checks the "Emotes" collection item's own configured content.
  929. * @param definition Receives the matching native item-definition row.
  930. * @return True only when both rows agree, declare no native equipment slot, and carry exactly the
  931. * expected 4 ordinary socket lanes.
  932. */
  933. [[nodiscard]] bool
  934. resolve_emote_collection_definition(build_data::items::Definition& definition) noexcept {
  935. item_details::Definition detail{};
  936. return build_data::find_item_definition_hash(authored_inventory::kEmoteCollectionDefinitionHash,
  937. definition)
  938. && definition.definitionHash == authored_inventory::kEmoteCollectionDefinitionHash
  939. && build_data::find_configured_item_detail(definition.definitionIndex, detail)
  940. && detail.definitionIndex == definition.definitionIndex
  941. && detail.definitionHash == authored_inventory::kEmoteCollectionDefinitionHash
  942. && detail.bucketId == definition.bucketId && !detail.equipmentSlot.has_value()
  943. && detail.ordinarySocketState == item_details::OrdinarySocketState::present
  944. && detail.ordinarySocketCount == authored_inventory::kEmoteCollectionSocketLaneCount;
  945. }
  946. /** Checks every seeded plug is installed and allowed in its lane before an item can carry it. */
  947. [[nodiscard]] bool default_plugs_valid(std::uint16_t collectionDefinitionIndex) noexcept {
  948. for (std::size_t lane = 0; lane < kEmoteCollectionDefaultPlugHashes.size(); ++lane) {
  949. build_data::items::Definition plugDefinition{};
  950. if (!build_data::find_item_definition_hash(kEmoteCollectionDefaultPlugHashes[lane],
  951. plugDefinition)
  952. || !build_data::is_socket_plug_allowed(collectionDefinitionIndex,
  953. static_cast<std::uint8_t>(lane),
  954. plugDefinition.definitionIndex)) {
  955. return false;
  956. }
  957. }
  958. return true;
  959. }
  960. /** Checks an equipped collection item's plugs so a stale set is repaired, not trusted. */
  961. [[nodiscard]] bool socket_state_sound(const authored_inventory::Item& item,
  962. std::uint16_t collectionDefinitionIndex) noexcept {
  963. if (item.sockets.policy != authored_inventory::SocketPolicy::authored
  964. || item.sockets.plugCount != authored_inventory::kEmoteCollectionSocketLaneCount) {
  965. return false;
  966. }
  967. for (std::size_t lane = 0; lane < authored_inventory::kEmoteCollectionSocketLaneCount; ++lane) {
  968. const std::optional<std::uint32_t>& plugHash = item.sockets.plugs[lane];
  969. build_data::items::Definition plugDefinition{};
  970. if (!plugHash.has_value()
  971. || !build_data::find_item_definition_hash(*plugHash, plugDefinition)
  972. || !build_data::is_socket_plug_allowed(collectionDefinitionIndex,
  973. static_cast<std::uint8_t>(lane),
  974. plugDefinition.definitionIndex)) {
  975. return false;
  976. }
  977. }
  978. return true;
  979. }
  980. } // namespace
  981. /**
  982. * Equips each character with the "Emotes" collection item in the emote slot.
  983. * Its content declares no native slot, so every resolver reaches it through
  984. * resolve_native_equipment_slot. Its 4 lanes have no native default and are seeded here.
  985. */
  986. EmoteCollectionOutcome ensure_character_emote_collection() noexcept {
  987. // The wheel occupies the authored emote equipment slot.
  988. constexpr std::size_t kEmoteCollectionSlot =
  989. static_cast<std::size_t>(authored_inventory::EquipmentSlot::emote);
  990. // Nothing can be concluded before these domains publish, so this is a retry, not a verdict.
  991. if (!build_data::item_definitions_ready() || !build_data::configured_item_details_ready()
  992. || !build_data::socket_plug_rules_ready()) {
  993. return EmoteCollectionOutcome::notReady;
  994. }
  995. // Published and still unresolved means the build cannot carry the wheel; a retry never helps.
  996. build_data::items::Definition collectionDefinition{};
  997. if (!resolve_emote_collection_definition(collectionDefinition)
  998. || !default_plugs_valid(collectionDefinition.definitionIndex)) {
  999. return EmoteCollectionOutcome::unsupported;
  1000. }
  1001. investment::store::g_mutex.lock();
  1002. AccountState candidate = investment::store::account();
  1003. if (!account::valid(candidate)) {
  1004. investment::store::g_mutex.unlock();
  1005. return EmoteCollectionOutcome::notReady;
  1006. }
  1007. bool changed = false;
  1008. bool failed = false;
  1009. for (std::size_t characterIndex = 0; characterIndex < candidate.characterCount && !failed;
  1010. ++characterIndex) {
  1011. CharacterState& character = candidate.characters[characterIndex];
  1012. auto& collectionSlot = character.equipment.slots[kEmoteCollectionSlot];
  1013. const bool present =
  1014. collectionSlot.has_value()
  1015. && collectionSlot->definitionHash == authored_inventory::kEmoteCollectionDefinitionHash;
  1016. if (present && socket_state_sound(*collectionSlot, collectionDefinition.definitionIndex)) {
  1017. continue;
  1018. }
  1019. if (character.nextInventorySerial
  1020. >= static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())) {
  1021. failed = true;
  1022. break;
  1023. }
  1024. // A repair owns the definition, the sockets and the serial. Every other field, the
  1025. // item-state flags above all, belongs to the player and survives.
  1026. authored_inventory::Item granted = present ? *collectionSlot : authored_inventory::Item{};
  1027. if (!present) {
  1028. std::uint64_t instanceSoid = 0;
  1029. if (!next_item_instance_soid(candidate, instanceSoid)) {
  1030. failed = true;
  1031. break;
  1032. }
  1033. granted.instanceSoid = instanceSoid;
  1034. granted.level = 0;
  1035. granted.quantity = 1;
  1036. }
  1037. granted.definitionHash = authored_inventory::kEmoteCollectionDefinitionHash;
  1038. granted.mutationSerial = static_cast<std::int32_t>(character.nextInventorySerial++);
  1039. // Replaced whole: lanes past the used prefix must be empty for the block to validate.
  1040. granted.sockets = authored_inventory::Sockets{};
  1041. granted.sockets.policy = authored_inventory::SocketPolicy::authored;
  1042. granted.sockets.plugCount = kEmoteCollectionDefaultPlugHashes.size();
  1043. for (std::size_t lane = 0; lane < kEmoteCollectionDefaultPlugHashes.size(); ++lane) {
  1044. granted.sockets.plugs[lane] = kEmoteCollectionDefaultPlugHashes[lane];
  1045. }
  1046. collectionSlot = granted;
  1047. changed = true;
  1048. }
  1049. if (failed) {
  1050. investment::store::g_mutex.unlock();
  1051. return EmoteCollectionOutcome::failed;
  1052. }
  1053. if (!changed) {
  1054. investment::store::g_mutex.unlock();
  1055. return EmoteCollectionOutcome::ready;
  1056. }
  1057. if (!account::valid(candidate)) {
  1058. investment::store::g_mutex.unlock();
  1059. return EmoteCollectionOutcome::failed;
  1060. }
  1061. if (!investment::store::write_account(candidate)) {
  1062. investment::store::g_mutex.unlock();
  1063. return EmoteCollectionOutcome::failed;
  1064. }
  1065. investment::store::g_mutex.unlock();
  1066. return EmoteCollectionOutcome::ready;
  1067. }
  1068. } // namespace sunrise::state