state_account_acquisition_runtime.cpp 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <array>
  4. #include <cstddef>
  5. #include <cstdint>
  6. #include <limits>
  7. #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
  8. #include "../build_data/runtime.h"
  9. #include "runtime.h"
  10. #include "state_account_transaction_helpers.h"
  11. #include "storage/internal.h"
  12. namespace sunrise::state {
  13. using namespace runtime::detail;
  14. namespace authored_inventory = account::inventory;
  15. namespace item_details = build_data::items::details;
  16. namespace inventory_buckets = build_data::inventory::buckets;
  17. namespace family4_loadout = middleware::datagen::family4::loadout;
  18. /** Prepares one native-row-checked selected-character inventory insertion. */
  19. bool prepare_item_acquisition(std::uint16_t collectibleIndex,
  20. std::uint32_t definitionHash,
  21. PendingItemAcquisition& mutation) noexcept {
  22. mutation = {};
  23. const AccountState account = account_snapshot();
  24. build_data::collectibles::Definition collectible{};
  25. build_data::items::Definition grantedDefinition{};
  26. // A vendor purchase names an item, not a collectible, so the collectible steps are skipped
  27. // rather than faked. The item is still validated, just by its own hash.
  28. const bool hasCollectible = collectibleIndex != build_data::collectibles::kNoCollectibleIndex;
  29. if (definitionHash == authored_inventory::kNoDefinitionHash || !account::valid(account)
  30. || !valid_profile_inventory(account)) {
  31. report_acquisition("prepare", "fail", "input", definitionHash, 0, 0, 0, 0, 0, 0);
  32. return false;
  33. }
  34. if (hasCollectible) {
  35. if (!build_data::find_collectible_definition(collectibleIndex, collectible)
  36. || collectible.itemDefinitionIndex
  37. == build_data::collectibles::kUnavailableItemDefinitionIndex
  38. || !build_data::find_item_definition_index(collectible.itemDefinitionIndex,
  39. grantedDefinition)
  40. || grantedDefinition.definitionHash != definitionHash) {
  41. report_acquisition("prepare", "fail", "input", definitionHash, 0, 0, 0, 0, 0, 0);
  42. return false;
  43. }
  44. } else if (!build_data::find_item_definition_hash(definitionHash, grantedDefinition)
  45. || grantedDefinition.definitionHash != definitionHash) {
  46. report_acquisition("prepare", "fail", "item", definitionHash, 0, 0, 0, 0, 0, 0);
  47. return false;
  48. }
  49. AccountState chargedAccount = account;
  50. bool profileChanged = false;
  51. // Nothing is charged without a collectible: the cost lives on the collectible's material
  52. // requirements, and a sale row's own cost fields are still role-open.
  53. if (hasCollectible
  54. && !apply_collection_materials(account, collectible, chargedAccount, profileChanged)) {
  55. report_acquisition("prepare", "fail", "materials", definitionHash, 0, 0, 0, 0, 0, 0);
  56. return false;
  57. }
  58. std::size_t characterIndex = account.characterCount;
  59. for (std::size_t index = 0; index < account.characterCount; ++index) {
  60. if (account.characters[index].selected) {
  61. characterIndex = index;
  62. break;
  63. }
  64. }
  65. if (characterIndex == account.characterCount) {
  66. report_acquisition("prepare", "fail", "selection", definitionHash, 0, 0, 0, 0, 0, 0);
  67. return false;
  68. }
  69. const CharacterState& before = account.characters[characterIndex];
  70. if (before.inventory.count >= before.inventory.values.size()
  71. || before.nextInventorySerial
  72. >= static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())) {
  73. report_acquisition("prepare",
  74. "fail",
  75. "state_capacity",
  76. definitionHash,
  77. before.soid,
  78. 0,
  79. before.inventory.count,
  80. 0,
  81. 0,
  82. before.nextInventorySerial);
  83. return false;
  84. }
  85. std::uint64_t instanceSoid = 0;
  86. if (!next_item_instance_soid(account, instanceSoid)) {
  87. report_acquisition("prepare",
  88. "fail",
  89. "soid",
  90. definitionHash,
  91. before.soid,
  92. 0,
  93. before.inventory.count,
  94. 0,
  95. 0,
  96. before.nextInventorySerial);
  97. return false;
  98. }
  99. CharacterState after = before;
  100. const std::size_t inventoryIndex = after.inventory.count;
  101. authored_inventory::Item acquired{};
  102. acquired.instanceSoid = instanceSoid;
  103. acquired.definitionHash = definitionHash;
  104. acquired.level = acquisition_level(before);
  105. acquired.quantity = 1;
  106. acquired.mutationSerial = static_cast<std::int32_t>(after.nextInventorySerial++);
  107. acquired.sockets.policy = authored_inventory::SocketPolicy::nativeDefaults;
  108. after.inventory.values[inventoryIndex] = acquired;
  109. ++after.inventory.count;
  110. AccountState candidate = chargedAccount;
  111. candidate.characters[characterIndex] = after;
  112. family4_loadout::ResolvedLoadout resolved{};
  113. std::uint16_t inventoryRow = 0;
  114. std::uint8_t equipmentSlot = 0;
  115. if (!account::valid(candidate) || identity_uses_soid(candidate, instanceSoid)
  116. || !family4_loadout::resolve(candidate, characterIndex, resolved)
  117. || !find_acquired_row(resolved, instanceSoid, inventoryRow, equipmentSlot)) {
  118. report_acquisition("prepare",
  119. "fail",
  120. "resolve_or_bucket_full",
  121. definitionHash,
  122. before.soid,
  123. instanceSoid,
  124. inventoryIndex,
  125. 0,
  126. 0,
  127. after.nextInventorySerial);
  128. return false;
  129. }
  130. mutation.beforeCharacter = before;
  131. mutation.afterCharacter = after;
  132. mutation.beforeProfileItems = account.profileItems;
  133. mutation.afterProfileItems = chargedAccount.profileItems;
  134. mutation.accountSoid = account.primarySoid;
  135. mutation.characterSoid = before.soid;
  136. mutation.acquiredInstanceSoid = instanceSoid;
  137. mutation.acquiredDefinitionHash = definitionHash;
  138. mutation.materialRequirementSetHash = collectible.materialRequirementSetHash;
  139. mutation.expectedNextInventorySerial = before.nextInventorySerial;
  140. mutation.characterIndex = characterIndex;
  141. mutation.expectedInventoryCount = before.inventory.count;
  142. mutation.expectedProfileItemCount = account.profileItemCount;
  143. mutation.afterProfileItemCount = chargedAccount.profileItemCount;
  144. mutation.inventoryIndex = inventoryIndex;
  145. mutation.collectibleIndex = collectibleIndex;
  146. mutation.inventoryRow = inventoryRow;
  147. mutation.equipmentSlot = equipmentSlot;
  148. mutation.materialRequirementCount = collectible.materialRequirementCount;
  149. mutation.profileChanged = profileChanged;
  150. mutation.prepared = true;
  151. report_acquisition("prepare",
  152. "ok",
  153. "ready",
  154. definitionHash,
  155. before.soid,
  156. instanceSoid,
  157. inventoryIndex,
  158. inventoryRow,
  159. equipmentSlot,
  160. after.nextInventorySerial);
  161. return true;
  162. }
  163. /** Produces the full account after-image while a prepared character pull remains current. */
  164. bool preview_item_acquisition(const PendingItemAcquisition& mutation,
  165. AccountState& after) noexcept {
  166. after = {};
  167. if (!mutation.prepared || mutation.accountSoid == 0 || mutation.characterSoid == 0
  168. || mutation.acquiredInstanceSoid == 0 || mutation.characterIndex >= kCharacterCapacity
  169. || mutation.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
  170. || mutation.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
  171. return false;
  172. }
  173. const AccountState current = account_snapshot();
  174. if (mutation.characterIndex >= current.characterCount
  175. || current.primarySoid != mutation.accountSoid
  176. || !same_character(current.characters[mutation.characterIndex], mutation.beforeCharacter)
  177. || !same_profile_inventory(
  178. current, mutation.beforeProfileItems, mutation.expectedProfileItemCount)) {
  179. return false;
  180. }
  181. after = current;
  182. after.profileItems = mutation.afterProfileItems;
  183. after.profileItemCount = mutation.afterProfileItemCount;
  184. after.characters[mutation.characterIndex] = mutation.afterCharacter;
  185. family4_loadout::ResolvedLoadout resolved{};
  186. std::uint16_t checkedRow = 0;
  187. std::uint8_t checkedSlot = 0;
  188. return account::valid(after) && valid_profile_inventory(after)
  189. && family4_loadout::resolve(after, mutation.characterIndex, resolved)
  190. && find_acquired_row(resolved, mutation.acquiredInstanceSoid, checkedRow, checkedSlot)
  191. && checkedRow == mutation.inventoryRow && checkedSlot == mutation.equipmentSlot;
  192. }
  193. /** Commits one prepared insertion only while its prepare-time loadout remains current. */
  194. bool commit_item_acquisition(PendingItemAcquisition& mutation) noexcept {
  195. const PendingItemAcquisition prepared = mutation;
  196. mutation = {};
  197. const auto fail = [&prepared](std::string_view reason) noexcept {
  198. report_acquisition("commit",
  199. "fail",
  200. reason,
  201. prepared.acquiredDefinitionHash,
  202. prepared.characterSoid,
  203. prepared.acquiredInstanceSoid,
  204. prepared.inventoryIndex,
  205. prepared.inventoryRow,
  206. prepared.equipmentSlot,
  207. prepared.afterCharacter.nextInventorySerial);
  208. return false;
  209. };
  210. if (!prepared.prepared || prepared.characterSoid == 0 || prepared.acquiredInstanceSoid == 0
  211. || prepared.accountSoid == 0
  212. || prepared.acquiredDefinitionHash == authored_inventory::kNoDefinitionHash
  213. || prepared.characterIndex >= kCharacterCapacity
  214. || prepared.expectedInventoryCount >= authored_inventory::kCharacterItemCapacity
  215. || prepared.inventoryIndex != prepared.expectedInventoryCount
  216. || prepared.afterCharacter.inventory.count != prepared.expectedInventoryCount + 1U
  217. || prepared.inventoryIndex >= prepared.afterCharacter.inventory.count
  218. || prepared.afterCharacter.inventory.values[prepared.inventoryIndex].instanceSoid
  219. != prepared.acquiredInstanceSoid
  220. || prepared.afterCharacter.inventory.values[prepared.inventoryIndex].definitionHash
  221. != prepared.acquiredDefinitionHash
  222. || prepared.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
  223. || prepared.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
  224. return fail("mutation");
  225. }
  226. // The guard is that prepare and commit agree. Without a collectible they agree on there being
  227. // none, which means both cost fields must still be clear.
  228. if (prepared.collectibleIndex == build_data::collectibles::kNoCollectibleIndex) {
  229. if (prepared.materialRequirementSetHash != 0 || prepared.materialRequirementCount != 0) {
  230. return fail("collectible");
  231. }
  232. } else {
  233. build_data::collectibles::Definition collectible{};
  234. if (!build_data::find_collectible_definition(prepared.collectibleIndex, collectible)
  235. || collectible.itemDefinitionIndex
  236. == build_data::collectibles::kUnavailableItemDefinitionIndex
  237. || collectible.materialRequirementSetHash != prepared.materialRequirementSetHash
  238. || collectible.materialRequirementCount != prepared.materialRequirementCount) {
  239. return fail("collectible");
  240. }
  241. }
  242. report_acquisition("commit_begin",
  243. "ok",
  244. "ready",
  245. prepared.acquiredDefinitionHash,
  246. prepared.characterSoid,
  247. prepared.acquiredInstanceSoid,
  248. prepared.inventoryIndex,
  249. prepared.inventoryRow,
  250. prepared.equipmentSlot,
  251. prepared.afterCharacter.nextInventorySerial);
  252. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  253. AccountState candidate = runtime::storage::g_state.account;
  254. if (prepared.characterIndex >= candidate.characterCount
  255. || candidate.primarySoid != prepared.accountSoid
  256. || !same_profile_inventory(
  257. candidate, prepared.beforeProfileItems, prepared.expectedProfileItemCount)) {
  258. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  259. return fail("account");
  260. }
  261. CharacterState& character = candidate.characters[prepared.characterIndex];
  262. if (!character.selected || character.soid != prepared.characterSoid
  263. || !same_character(character, prepared.beforeCharacter)) {
  264. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  265. return fail("stale");
  266. }
  267. candidate.profileItems = prepared.afterProfileItems;
  268. candidate.profileItemCount = prepared.afterProfileItemCount;
  269. character = prepared.afterCharacter;
  270. family4_loadout::ResolvedLoadout resolved{};
  271. std::uint16_t checkedRow = 0;
  272. std::uint8_t checkedSlot = 0;
  273. if (!account::valid(candidate) || !valid_profile_inventory(candidate)
  274. || identity_uses_soid(candidate, prepared.acquiredInstanceSoid)
  275. || !family4_loadout::resolve(candidate, prepared.characterIndex, resolved)
  276. || !find_acquired_row(resolved, prepared.acquiredInstanceSoid, checkedRow, checkedSlot)
  277. || checkedRow != prepared.inventoryRow || checkedSlot != prepared.equipmentSlot) {
  278. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  279. return fail("resolve");
  280. }
  281. runtime::storage::g_state.account = candidate;
  282. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  283. report_acquisition("commit_end",
  284. "ok",
  285. "published",
  286. prepared.acquiredDefinitionHash,
  287. prepared.characterSoid,
  288. prepared.acquiredInstanceSoid,
  289. prepared.inventoryIndex,
  290. prepared.inventoryRow,
  291. prepared.equipmentSlot,
  292. prepared.afterCharacter.nextInventorySerial);
  293. return true;
  294. }
  295. /** Prepares one checked profile-stack increment or append for a Collections pull. */
  296. bool prepare_profile_item_acquisition(std::uint16_t collectibleIndex,
  297. std::uint32_t definitionHash,
  298. PendingProfileItemAcquisition& mutation) noexcept {
  299. mutation = {};
  300. const AccountState account = account_snapshot();
  301. build_data::collectibles::Definition collectible{};
  302. build_data::items::Definition item{};
  303. item_details::Definition detail{};
  304. inventory_buckets::Descriptor bucket{};
  305. if (definitionHash == authored_inventory::kNoDefinitionHash || !account::valid(account)
  306. || !valid_profile_inventory(account)
  307. || !build_data::find_item_definition_hash(definitionHash, item)
  308. || item.definitionHash != definitionHash
  309. // The item resolves first, because the collectible cross-check reads it. With no
  310. // collectible the item's own hash is the whole check.
  311. || (collectibleIndex != build_data::collectibles::kNoCollectibleIndex
  312. && (!build_data::find_collectible_definition(collectibleIndex, collectible)
  313. || collectible.itemDefinitionIndex
  314. == build_data::collectibles::kUnavailableItemDefinitionIndex
  315. || item.definitionIndex != collectible.itemDefinitionIndex))
  316. || !build_data::find_configured_item_detail(item.definitionIndex, detail)
  317. || detail.definitionIndex != item.definitionIndex || detail.definitionHash != definitionHash
  318. || detail.bucketId != item.bucketId
  319. || detail.instancedDefinitionState != item_details::InstancedDefinitionState::stackable
  320. || detail.maxStackSize <= 0
  321. || !build_data::find_inventory_bucket_descriptor(detail.bucketId, bucket)
  322. || bucket.arraySelector != inventory_buckets::ArraySelector::profile) {
  323. report_profile_acquisition("prepare",
  324. "fail",
  325. "definition_or_profile",
  326. definitionHash,
  327. account.primarySoid,
  328. 0,
  329. 0,
  330. 0,
  331. account.profileItemCount,
  332. 0,
  333. 0,
  334. false);
  335. return false;
  336. }
  337. AccountState chargedAccount{};
  338. bool materialsChanged = false;
  339. if (!apply_collection_materials(account, collectible, chargedAccount, materialsChanged)) {
  340. report_profile_acquisition("prepare",
  341. "fail",
  342. "materials",
  343. definitionHash,
  344. account.primarySoid,
  345. 0,
  346. detail.bucketId,
  347. 0,
  348. account.profileItemCount,
  349. 0,
  350. 0,
  351. false);
  352. return false;
  353. }
  354. (void)materialsChanged;
  355. const bool actionSource =
  356. build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
  357. std::size_t profileIndex = chargedAccount.profileItemCount;
  358. std::int32_t previousQuantity = 0;
  359. std::int32_t previousMutationSerial = 0;
  360. std::int32_t greatestMutationSerial = 0;
  361. bool appended = true;
  362. for (std::size_t index = 0; index < chargedAccount.profileItemCount; ++index) {
  363. greatestMutationSerial =
  364. (std::max)(greatestMutationSerial, chargedAccount.profileItems[index].mutationSerial);
  365. }
  366. for (std::size_t index = 0; index < chargedAccount.profileItemCount; ++index) {
  367. const authored_inventory::ProfileItem& existing = chargedAccount.profileItems[index];
  368. if (existing.definitionHash != definitionHash) {
  369. continue;
  370. }
  371. if (existing.quantity > detail.maxStackSize) {
  372. report_profile_acquisition("prepare",
  373. "fail",
  374. "existing_quantity",
  375. definitionHash,
  376. account.primarySoid,
  377. existing.instanceSoid,
  378. detail.bucketId,
  379. index,
  380. chargedAccount.profileItemCount,
  381. existing.quantity,
  382. existing.quantity,
  383. false);
  384. return false;
  385. }
  386. if (appended && existing.quantity < detail.maxStackSize) {
  387. profileIndex = index;
  388. previousQuantity = existing.quantity;
  389. previousMutationSerial = existing.mutationSerial;
  390. appended = false;
  391. }
  392. }
  393. if (greatestMutationSerial == (std::numeric_limits<std::int32_t>::max)()
  394. || (appended && chargedAccount.profileItemCount >= chargedAccount.profileItems.size())) {
  395. report_profile_acquisition(
  396. "prepare",
  397. "fail",
  398. "state_capacity",
  399. definitionHash,
  400. account.primarySoid,
  401. appended ? 0 : chargedAccount.profileItems[profileIndex].instanceSoid,
  402. detail.bucketId,
  403. profileIndex,
  404. chargedAccount.profileItemCount,
  405. 0,
  406. 0,
  407. true);
  408. return false;
  409. }
  410. std::uint64_t acquiredInstanceSoid =
  411. appended ? 0 : chargedAccount.profileItems[profileIndex].instanceSoid;
  412. if (actionSource != (acquiredInstanceSoid != 0) && !appended) {
  413. report_profile_acquisition("prepare",
  414. "fail",
  415. "identity_policy",
  416. definitionHash,
  417. account.primarySoid,
  418. acquiredInstanceSoid,
  419. detail.bucketId,
  420. profileIndex,
  421. chargedAccount.profileItemCount,
  422. previousQuantity,
  423. previousQuantity,
  424. false);
  425. return false;
  426. }
  427. if (appended && actionSource
  428. && !next_profile_item_instance_soid(chargedAccount, acquiredInstanceSoid)) {
  429. report_profile_acquisition("prepare",
  430. "fail",
  431. "identity_capacity",
  432. definitionHash,
  433. account.primarySoid,
  434. acquiredInstanceSoid,
  435. detail.bucketId,
  436. profileIndex,
  437. chargedAccount.profileItemCount,
  438. 0,
  439. 0,
  440. true);
  441. return false;
  442. }
  443. AccountState after = chargedAccount;
  444. const std::int32_t acquiredMutationSerial = greatestMutationSerial + 1;
  445. if (appended) {
  446. after.profileItems[profileIndex] = {
  447. acquiredInstanceSoid, definitionHash, 1, acquiredMutationSerial};
  448. ++after.profileItemCount;
  449. } else {
  450. ++after.profileItems[profileIndex].quantity;
  451. after.profileItems[profileIndex].mutationSerial = acquiredMutationSerial;
  452. }
  453. const std::int32_t acquiredQuantity = after.profileItems[profileIndex].quantity;
  454. if (after.profileItems[profileIndex].instanceSoid != acquiredInstanceSoid
  455. || acquiredQuantity <= previousQuantity || acquiredQuantity > detail.maxStackSize
  456. || !account::valid(after) || !valid_profile_inventory(after)) {
  457. report_profile_acquisition("prepare",
  458. "fail",
  459. "bucket_full_or_quantity",
  460. definitionHash,
  461. account.primarySoid,
  462. acquiredInstanceSoid,
  463. detail.bucketId,
  464. profileIndex,
  465. after.profileItemCount,
  466. previousQuantity,
  467. acquiredQuantity,
  468. appended);
  469. return false;
  470. }
  471. mutation.beforeItems = account.profileItems;
  472. mutation.afterItems = after.profileItems;
  473. mutation.accountSoid = account.primarySoid;
  474. mutation.acquiredInstanceSoid = acquiredInstanceSoid;
  475. mutation.acquiredDefinitionHash = definitionHash;
  476. mutation.materialRequirementSetHash = collectible.materialRequirementSetHash;
  477. mutation.expectedItemCount = account.profileItemCount;
  478. mutation.afterItemCount = after.profileItemCount;
  479. mutation.profileIndex = profileIndex;
  480. mutation.previousQuantity = previousQuantity;
  481. mutation.acquiredQuantity = acquiredQuantity;
  482. mutation.previousMutationSerial = previousMutationSerial;
  483. mutation.acquiredMutationSerial = acquiredMutationSerial;
  484. mutation.collectibleIndex = collectibleIndex;
  485. mutation.bucketId = detail.bucketId;
  486. mutation.materialRequirementCount = collectible.materialRequirementCount;
  487. mutation.actionSource = actionSource;
  488. mutation.appended = appended;
  489. mutation.prepared = true;
  490. if (!valid_profile_mutation_shape(mutation)) {
  491. mutation = {};
  492. report_profile_acquisition("prepare",
  493. "fail",
  494. "mutation",
  495. definitionHash,
  496. account.primarySoid,
  497. acquiredInstanceSoid,
  498. detail.bucketId,
  499. profileIndex,
  500. after.profileItemCount,
  501. previousQuantity,
  502. acquiredQuantity,
  503. appended);
  504. return false;
  505. }
  506. report_profile_acquisition("prepare",
  507. "ok",
  508. "ready",
  509. definitionHash,
  510. account.primarySoid,
  511. acquiredInstanceSoid,
  512. detail.bucketId,
  513. profileIndex,
  514. after.profileItemCount,
  515. previousQuantity,
  516. acquiredQuantity,
  517. appended);
  518. return true;
  519. }
  520. /** Produces the exact account after-image while the captured profile view is still current. */
  521. bool preview_profile_item_acquisition(const PendingProfileItemAcquisition& mutation,
  522. AccountState& after) noexcept {
  523. after = {};
  524. const AccountState current = account_snapshot();
  525. const bool ready = materialize_profile_acquisition(current, mutation, after);
  526. report_profile_acquisition("preview",
  527. ready ? "ok" : "fail",
  528. ready ? "ready" : "stale_or_invalid",
  529. mutation.acquiredDefinitionHash,
  530. mutation.accountSoid,
  531. mutation.acquiredInstanceSoid,
  532. mutation.bucketId,
  533. mutation.profileIndex,
  534. mutation.afterItemCount,
  535. mutation.previousQuantity,
  536. mutation.acquiredQuantity,
  537. mutation.appended);
  538. return ready;
  539. }
  540. /** Commits one profile-stack after-image only while its exact prepare-time view remains current. */
  541. bool commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcept {
  542. const PendingProfileItemAcquisition prepared = mutation;
  543. mutation = {};
  544. if (!valid_profile_mutation_shape(prepared)) {
  545. report_profile_acquisition("commit",
  546. "fail",
  547. "mutation",
  548. prepared.acquiredDefinitionHash,
  549. prepared.accountSoid,
  550. prepared.acquiredInstanceSoid,
  551. prepared.bucketId,
  552. prepared.profileIndex,
  553. prepared.afterItemCount,
  554. prepared.previousQuantity,
  555. prepared.acquiredQuantity,
  556. prepared.appended);
  557. return false;
  558. }
  559. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  560. AccountState candidate{};
  561. const bool ready =
  562. materialize_profile_acquisition(runtime::storage::g_state.account, prepared, candidate);
  563. if (ready) {
  564. runtime::storage::g_state.account = candidate;
  565. }
  566. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  567. report_profile_acquisition("commit",
  568. ready ? "ok" : "fail",
  569. ready ? "published" : "stale_or_invalid",
  570. prepared.acquiredDefinitionHash,
  571. prepared.accountSoid,
  572. prepared.acquiredInstanceSoid,
  573. prepared.bucketId,
  574. prepared.profileIndex,
  575. prepared.afterItemCount,
  576. prepared.previousQuantity,
  577. prepared.acquiredQuantity,
  578. prepared.appended);
  579. return ready;
  580. }
  581. namespace {
  582. /**
  583. * Default plug hashes the wheel seeds when granting or repairing the collection item's sockets.
  584. * All four are universal Common emotes from Bright Engrams, with no class or race restriction:
  585. * "Yes" (3184938442), "Nope" (48790291), "Casual Sit" (383973261), "Cheer" (2834933816).
  586. * Lane order follows the client's own wheel layout, confirmed empirically in-game:
  587. * lane 0 = top, lane 1 = bottom, lane 2 = left, lane 3 = right.
  588. */
  589. constexpr std::uint32_t kYesEmoteDefinitionHash = 3184938442U;
  590. constexpr std::uint32_t kNopeEmoteDefinitionHash = 48790291U;
  591. constexpr std::uint32_t kCasualSitEmoteDefinitionHash = 383973261U;
  592. constexpr std::uint32_t kCheerEmoteDefinitionHash = 2834933816U;
  593. constexpr std::array<std::uint32_t, authored_inventory::kEmoteCollectionSocketLaneCount>
  594. kEmoteCollectionDefaultPlugHashes{
  595. kCheerEmoteDefinitionHash, // lane 0 -- top
  596. kCasualSitEmoteDefinitionHash, // lane 1 -- bottom
  597. kYesEmoteDefinitionHash, // lane 2 -- left
  598. kNopeEmoteDefinitionHash, // lane 3 -- right
  599. };
  600. /**
  601. * Resolves and cross-checks the "Emotes" collection item's own configured content. The detail row
  602. * is only read to validate the definition, so it stays local rather than reaching the caller.
  603. * @param definition Receives the matching native item-definition row.
  604. * @return True only when both rows agree with each other, carry no native equipment slot (the one
  605. * trait that singles this item out among every character-scoped item), and declare exactly
  606. * the expected 4 ordinary socket lanes.
  607. */
  608. [[nodiscard]] bool
  609. resolve_emote_collection_definition(build_data::items::Definition& definition) noexcept {
  610. item_details::Definition detail{};
  611. return build_data::find_item_definition_hash(authored_inventory::kEmoteCollectionDefinitionHash,
  612. definition)
  613. && definition.definitionHash == authored_inventory::kEmoteCollectionDefinitionHash
  614. && build_data::find_configured_item_detail(definition.definitionIndex, detail)
  615. && detail.definitionIndex == definition.definitionIndex
  616. && detail.definitionHash == authored_inventory::kEmoteCollectionDefinitionHash
  617. && detail.bucketId == definition.bucketId && !detail.equipmentSlot.has_value()
  618. && detail.ordinarySocketState == item_details::OrdinarySocketState::present
  619. && detail.ordinarySocketCount == authored_inventory::kEmoteCollectionSocketLaneCount;
  620. }
  621. /**
  622. * Checks that every one of the collection item's real plug pool candidates is actually installed
  623. * and allowed in its intended lane, so a granted item can never carry a plug the client rejects.
  624. */
  625. [[nodiscard]] bool default_plugs_valid(std::uint16_t collectionDefinitionIndex) noexcept {
  626. for (std::size_t lane = 0; lane < kEmoteCollectionDefaultPlugHashes.size(); ++lane) {
  627. build_data::items::Definition plugDefinition{};
  628. if (!build_data::find_item_definition_hash(kEmoteCollectionDefaultPlugHashes[lane],
  629. plugDefinition)
  630. || !build_data::is_socket_plug_allowed(collectionDefinitionIndex,
  631. static_cast<std::uint8_t>(lane),
  632. plugDefinition.definitionIndex)) {
  633. return false;
  634. }
  635. }
  636. return true;
  637. }
  638. /**
  639. * Checks an already-equipped collection item's own socket state, so a corrupted or stale set of
  640. * plugs is repaired instead of trusted just because the definition hash already matches.
  641. */
  642. [[nodiscard]] bool socket_state_sound(const authored_inventory::Item& item,
  643. std::uint16_t collectionDefinitionIndex) noexcept {
  644. if (item.sockets.policy != authored_inventory::SocketPolicy::authored
  645. || item.sockets.plugCount != authored_inventory::kEmoteCollectionSocketLaneCount) {
  646. return false;
  647. }
  648. for (std::size_t lane = 0; lane < authored_inventory::kEmoteCollectionSocketLaneCount; ++lane) {
  649. const std::optional<std::uint32_t>& plugHash = item.sockets.plugs[lane];
  650. build_data::items::Definition plugDefinition{};
  651. if (!plugHash.has_value()
  652. || !build_data::find_item_definition_hash(*plugHash, plugDefinition)
  653. || !build_data::is_socket_plug_allowed(collectionDefinitionIndex,
  654. static_cast<std::uint8_t>(lane),
  655. plugDefinition.definitionIndex)) {
  656. return false;
  657. }
  658. }
  659. return true;
  660. }
  661. } // namespace
  662. /**
  663. * Equips each character with the "Emotes" collection item in the real emote slot, in place of an
  664. * individual emote. Unlike every other character-scoped item, its real content carries no native
  665. * equipment-slot mapping at all, so the resolvers this depends on (loadout resolution, light,
  666. * appearance refresh) fall back to authored_inventory::kEmoteCollectionNativeEquipmentSlot for it
  667. * specifically, gated to its exact definition hash (state::account::inventory::
  668. * resolve_native_equipment_slot). Its 4 ordinary sockets carry no native default plug, so 4 hashes
  669. * from its real reusable plug pool seed a default wheel; the client's own generic socket-plug
  670. * request (opcode 1901) lets the player reassign them afterward, the same mechanism it already uses
  671. * for weapon mods and shaders.
  672. */
  673. EmoteCollectionOutcome ensure_character_emote_collection() noexcept {
  674. constexpr std::size_t kEmoteCollectionSlot =
  675. static_cast<std::size_t>(authored_inventory::EquipmentSlot::emote);
  676. // The domains every check below reads have to be published first. Until they are, nothing can
  677. // be concluded about the installed content, so this is a retry rather than a verdict.
  678. if (!build_data::item_definitions_ready() || !build_data::configured_item_details_ready()
  679. || !build_data::socket_plug_rules_ready()) {
  680. return EmoteCollectionOutcome::notReady;
  681. }
  682. // With those published, an item that still does not resolve this way is a build that cannot
  683. // carry the wheel at all. Retrying that within this process would never change the answer.
  684. build_data::items::Definition collectionDefinition{};
  685. if (!resolve_emote_collection_definition(collectionDefinition)
  686. || !default_plugs_valid(collectionDefinition.definitionIndex)) {
  687. return EmoteCollectionOutcome::unsupported;
  688. }
  689. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  690. AccountState candidate = runtime::storage::g_state.account;
  691. if (!account::valid(candidate)) {
  692. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  693. return EmoteCollectionOutcome::notReady;
  694. }
  695. bool changed = false;
  696. bool failed = false;
  697. for (std::size_t characterIndex = 0; characterIndex < candidate.characterCount && !failed;
  698. ++characterIndex) {
  699. CharacterState& character = candidate.characters[characterIndex];
  700. auto& collectionSlot = character.equipment.slots[kEmoteCollectionSlot];
  701. const bool present =
  702. collectionSlot.has_value()
  703. && collectionSlot->definitionHash == authored_inventory::kEmoteCollectionDefinitionHash;
  704. if (present && socket_state_sound(*collectionSlot, collectionDefinition.definitionIndex)) {
  705. continue;
  706. }
  707. if (character.nextInventorySerial
  708. >= static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())) {
  709. failed = true;
  710. break;
  711. }
  712. // A repair owns only the definition, the sockets and the serial. Everything else the item
  713. // already carries, the accumulated item-state flags above all, belongs to the player and
  714. // survives. The account was checked whole on entry, so a present item's remaining scalars
  715. // are already known good and need no normalizing here.
  716. authored_inventory::Item granted = present ? *collectionSlot : authored_inventory::Item{};
  717. if (!present) {
  718. std::uint64_t instanceSoid = 0;
  719. if (!next_item_instance_soid(candidate, instanceSoid)) {
  720. failed = true;
  721. break;
  722. }
  723. granted.instanceSoid = instanceSoid;
  724. granted.level = 0;
  725. granted.quantity = 1;
  726. }
  727. granted.definitionHash = authored_inventory::kEmoteCollectionDefinitionHash;
  728. granted.mutationSerial = static_cast<std::int32_t>(character.nextInventorySerial++);
  729. // Replaced whole rather than edited: the lanes past the used prefix have to be empty for
  730. // the socket block to validate, whatever the malformed copy left behind.
  731. granted.sockets = authored_inventory::Sockets{};
  732. granted.sockets.policy = authored_inventory::SocketPolicy::authored;
  733. granted.sockets.plugCount = kEmoteCollectionDefaultPlugHashes.size();
  734. for (std::size_t lane = 0; lane < kEmoteCollectionDefaultPlugHashes.size(); ++lane) {
  735. granted.sockets.plugs[lane] = kEmoteCollectionDefaultPlugHashes[lane];
  736. }
  737. collectionSlot = granted;
  738. changed = true;
  739. }
  740. if (failed) {
  741. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  742. return EmoteCollectionOutcome::failed;
  743. }
  744. if (!changed) {
  745. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  746. return EmoteCollectionOutcome::ready;
  747. }
  748. if (!account::valid(candidate)) {
  749. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  750. return EmoteCollectionOutcome::failed;
  751. }
  752. runtime::storage::g_state.account = candidate;
  753. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  754. return EmoteCollectionOutcome::ready;
  755. }
  756. } // namespace sunrise::state