state_account_runtime.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <array>
  4. #include <cstddef>
  5. #include <cstdint>
  6. #include <utility>
  7. #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
  8. #include "../build_data/runtime.h"
  9. #include "runtime.h"
  10. #include "state.h"
  11. #include "state_account_transaction_helpers.h"
  12. #include "storage/internal.h"
  13. namespace sunrise::state {
  14. namespace runtime::detail {
  15. namespace authored_inventory = account::inventory;
  16. namespace item_details = build_data::items::details;
  17. namespace inventory_buckets = build_data::inventory::buckets;
  18. namespace family4_loadout = middleware::datagen::family4::loadout;
  19. namespace socket_lists = build_data::socket_entry_lists;
  20. /**
  21. * Prepares a subclass ability-entry transition without publishing account State.
  22. * The requested entry must share a socket-entry group with exactly one of the character's 5
  23. * authored picks; that pick is updated, mirroring how `resolve_socket_states` reads a selection.
  24. */
  25. [[nodiscard]] bool stage_subclass_selection(const AccountState& snapshot,
  26. std::size_t characterIndex,
  27. std::uint64_t subclassInstanceSoid,
  28. std::uint8_t requestedEntry,
  29. PendingSubclassSelection& mutation) noexcept {
  30. mutation = {};
  31. if (!account::valid(snapshot) || characterIndex >= snapshot.characterCount
  32. || subclassInstanceSoid == 0 || requestedEntry >= socket_lists::kEntryCapacity) {
  33. return false;
  34. }
  35. const CharacterState& before = snapshot.characters[characterIndex];
  36. if (!before.selected || before.soid == 0) {
  37. return false;
  38. }
  39. // Index of the subclass slot in the authored equipment array.
  40. constexpr std::size_t kSubclassSlot =
  41. static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass);
  42. const auto& subclass = before.equipment.slots[kSubclassSlot];
  43. build_data::items::Definition subclassDefinition{};
  44. item_details::Definition detail{};
  45. socket_lists::EntryTable entries{};
  46. if (!subclass.has_value() || subclass->instanceSoid != subclassInstanceSoid
  47. || !build_data::find_item_definition_hash(subclass->definitionHash, subclassDefinition)
  48. || !build_data::find_configured_item_detail(subclassDefinition.definitionIndex, detail)
  49. || !build_data::find_socket_entry_table(detail.socketEntryListIndex, entries)
  50. || requestedEntry >= entries.entries.size()) {
  51. return false;
  52. }
  53. const socket_lists::Entry& requested = entries.entries[requestedEntry];
  54. if (requested.plugSource == socket_lists::kNoPlugSource
  55. || requested.group == socket_lists::kNoEntryGroup) {
  56. return false;
  57. }
  58. // A clicked entry's table position does not say which ability slot it fills; only its resolved
  59. // destination bucket does. A bundled pick can mix members across slots, so every member of the
  60. // clicked entry's bundle is checked, not just the one clicked.
  61. CharacterState after = before;
  62. // The picks belong to the equipped subclass item itself, not the character. So each owned
  63. // subclass remembers its own selection, instead of sharing one set across all of them.
  64. auto& afterSubclassSlot = after.equipment.slots[kSubclassSlot];
  65. if (!afterSubclassSlot.has_value()) {
  66. return false;
  67. }
  68. auto& afterSubclass = *afterSubclassSlot;
  69. struct Route {
  70. std::uint8_t bucket;
  71. std::uint8_t* field;
  72. std::uint8_t defaultEntry;
  73. };
  74. const std::array<Route, 5> routes{{
  75. {kMovementAbilityBucket, &afterSubclass.movementAbilityEntry, kDefaultMovementAbilityEntry},
  76. {kGrenadeAbilityBucket, &afterSubclass.grenadeAbilityEntry, kDefaultGrenadeAbilityEntry},
  77. {kSuperAbilityBucket, &afterSubclass.superAbilityEntry, kDefaultSuperAbilityEntry},
  78. {kMeleeAbilityBucket, &afterSubclass.meleeAbilityEntry, kDefaultMeleeAbilityEntry},
  79. {class_ability_bucket(after.characterClass),
  80. &afterSubclass.classAbilityEntry,
  81. kDefaultClassAbilityEntry},
  82. }};
  83. const auto bucket_of = [&](std::uint8_t entryIndex) noexcept {
  84. std::uint8_t bucket = build_data::socket_entry_buckets::kNoDestinationBucket;
  85. (void)build_data::find_socket_entry_bucket(detail.socketEntryListIndex, entryIndex, bucket);
  86. return bucket;
  87. };
  88. const auto route_entry = [&](std::uint8_t entryIndex) noexcept {
  89. const std::uint8_t bucket = bucket_of(entryIndex);
  90. for (const Route& route : routes) {
  91. if (route.bucket == bucket) {
  92. *route.field = entryIndex;
  93. return;
  94. }
  95. }
  96. };
  97. // A click can land on any member of a bundle, not only the routable one: the other quadrants
  98. // are passive nodes with no destination bucket. The bundle's start is found by scanning
  99. // backward, then every member is routed from there, and only while the group is wide.
  100. std::size_t groupPopulation = 0;
  101. for (std::size_t index = 0; index < entries.entries.size(); ++index) {
  102. if (entries.entries[index].group == requested.group) {
  103. ++groupPopulation;
  104. }
  105. }
  106. if (groupPopulation <= kMaxAttunementBundleSize) {
  107. route_entry(requestedEntry);
  108. } else {
  109. // A wide group is several same-sized bundles competing for one pick, so only one bundle's
  110. // fields stay set. A bundle that does not touch every field the group reaches must not
  111. // leave an earlier bundle's value behind, so every bucket is reset before the pick writes.
  112. for (std::size_t index = 0; index < entries.entries.size(); ++index) {
  113. if (entries.entries[index].group != requested.group) {
  114. continue;
  115. }
  116. const std::uint8_t bucket = bucket_of(static_cast<std::uint8_t>(index));
  117. for (const Route& route : routes) {
  118. if (route.bucket == bucket) {
  119. *route.field = route.defaultEntry;
  120. }
  121. }
  122. }
  123. std::uint8_t blockStart = requestedEntry;
  124. while (blockStart > 0 && requestedEntry - blockStart < kMaxAttunementBundleSize - 1
  125. && entries.entries[blockStart - 1].group == requested.group) {
  126. --blockStart;
  127. }
  128. for (std::size_t offset = 0;
  129. offset < kMaxAttunementBundleSize && blockStart + offset < entries.entries.size()
  130. && entries.entries[blockStart + offset].group == requested.group;
  131. ++offset) {
  132. route_entry(static_cast<std::uint8_t>(blockStart + offset));
  133. }
  134. }
  135. if (same_character(before, after)) {
  136. return false;
  137. }
  138. mutation.beforeCharacter = before;
  139. mutation.afterCharacter = after;
  140. mutation.accountSoid = snapshot.primarySoid;
  141. mutation.characterSoid = before.soid;
  142. mutation.subclassInstanceSoid = subclassInstanceSoid;
  143. mutation.subclassDefinitionHash = subclassDefinition.definitionHash;
  144. mutation.characterIndex = characterIndex;
  145. mutation.subclassDefinitionIndex = subclassDefinition.definitionIndex;
  146. mutation.socketEntryListIndex = detail.socketEntryListIndex;
  147. mutation.requestedEntry = requestedEntry;
  148. mutation.prepared = true;
  149. return true;
  150. }
  151. } // namespace runtime::detail
  152. using namespace runtime::detail;
  153. /** Stores the active account key without publishing an incomplete account. */
  154. bool set_primary_soid(std::uint64_t primarySoid) noexcept {
  155. if (primarySoid == 0) {
  156. return false;
  157. }
  158. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  159. AccountState candidate = runtime::storage::g_state.account;
  160. candidate.primarySoid = primarySoid;
  161. // Characters belong to the account key the Client uses. The reference account and its
  162. // characters differ only in the low byte, so the authored rows are rebased onto that key.
  163. for (std::size_t index = 0; index < candidate.characterCount; ++index) {
  164. candidate.characters[index].soid = primarySoid + 1U + index;
  165. }
  166. if (!account::valid(candidate)) {
  167. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  168. return false;
  169. }
  170. // Publish only after the settings and identity rules hold together.
  171. runtime::storage::g_state.account = candidate;
  172. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  173. return true;
  174. }
  175. /** Closes the account's one-time profile-setup gate. */
  176. bool complete_profile_setup() noexcept {
  177. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  178. AccountState& accountState = runtime::storage::g_state.account;
  179. if (accountState.primarySoid == 0 || !account::valid(accountState)) {
  180. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  181. return false;
  182. }
  183. const bool changed = !accountState.profileSetupCompleted;
  184. accountState.profileSetupCompleted = true;
  185. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  186. if (changed) {
  187. core::log::write(core::log::Channel::state,
  188. core::log::Level::info,
  189. "ev=profile_setup stage=complete result=ok");
  190. }
  191. return true;
  192. }
  193. /** Moves the selection to one authored character. */
  194. bool set_selected_character(std::uint64_t characterSoid, bool& changed) noexcept {
  195. changed = false;
  196. if (characterSoid == 0) {
  197. return false;
  198. }
  199. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  200. AccountState candidate = runtime::storage::g_state.account;
  201. std::size_t picked = candidate.characterCount;
  202. for (std::size_t index = 0; index < candidate.characterCount; ++index) {
  203. if (candidate.characters[index].soid == characterSoid) {
  204. picked = index;
  205. }
  206. }
  207. if (picked == candidate.characterCount) {
  208. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  209. return false;
  210. }
  211. const bool alreadySelected = candidate.characters[picked].selected;
  212. for (CharacterState& character : candidate.characters) {
  213. character.selected = false;
  214. }
  215. candidate.characters[picked].selected = true;
  216. if (!account::valid(candidate)) {
  217. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  218. return false;
  219. }
  220. // Publish only after the whole account still meets its identity rules.
  221. runtime::storage::g_state.account = candidate;
  222. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  223. changed = !alreadySelected;
  224. return true;
  225. }
  226. /** Stores the selected character's equipped native title row. */
  227. bool set_selected_title(std::uint16_t recordIndex,
  228. std::uint64_t& characterSoid,
  229. bool& changed) noexcept {
  230. characterSoid = 0;
  231. changed = false;
  232. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  233. AccountState candidate = runtime::storage::g_state.account;
  234. std::size_t selectedIndex = candidate.characterCount;
  235. for (std::size_t index = 0; index < candidate.characterCount; ++index) {
  236. if (candidate.characters[index].selected) {
  237. selectedIndex = index;
  238. break;
  239. }
  240. }
  241. if (selectedIndex == candidate.characterCount) {
  242. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  243. return false;
  244. }
  245. CharacterState& character = candidate.characters[selectedIndex];
  246. characterSoid = character.soid;
  247. changed = character.equippedTitleRecordIndex != recordIndex;
  248. character.equippedTitleRecordIndex = recordIndex;
  249. if (!account::valid(candidate)) {
  250. characterSoid = 0;
  251. changed = false;
  252. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  253. return false;
  254. }
  255. runtime::storage::g_state.account = candidate;
  256. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  257. return true;
  258. }
  259. /** Prepares the selected character's current activity without changing account State. */
  260. bool prepare_current_activity(std::uint16_t activityIndex,
  261. PendingCurrentActivity& mutation) noexcept {
  262. mutation = {};
  263. const AccountState snapshot = account_snapshot();
  264. if (!account::valid(snapshot)) {
  265. return false;
  266. }
  267. for (std::size_t index = 0; index < snapshot.characterCount; ++index) {
  268. const CharacterState& character = snapshot.characters[index];
  269. if (!character.selected) {
  270. continue;
  271. }
  272. if (character.currentActivityIndex == activityIndex) {
  273. return false;
  274. }
  275. mutation.beforeCharacter = character;
  276. mutation.afterCharacter = character;
  277. mutation.afterCharacter.currentActivityIndex = activityIndex;
  278. mutation.characterSoid = character.soid;
  279. mutation.characterIndex = index;
  280. mutation.activityIndex = activityIndex;
  281. mutation.prepared = true;
  282. return true;
  283. }
  284. return false;
  285. }
  286. /** Commits one prepared current-activity change behind an exact character staleness guard. */
  287. bool commit_current_activity(PendingCurrentActivity& mutation) noexcept {
  288. const PendingCurrentActivity prepared = mutation;
  289. mutation = {};
  290. if (!prepared.prepared || prepared.characterSoid == 0
  291. || prepared.characterIndex >= kCharacterCapacity
  292. || prepared.beforeCharacter.soid != prepared.characterSoid
  293. || prepared.afterCharacter.soid != prepared.characterSoid) {
  294. return false;
  295. }
  296. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  297. AccountState candidate = runtime::storage::g_state.account;
  298. if (prepared.characterIndex >= candidate.characterCount
  299. || !same_character(candidate.characters[prepared.characterIndex],
  300. prepared.beforeCharacter)) {
  301. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  302. return false;
  303. }
  304. candidate.characters[prepared.characterIndex] = prepared.afterCharacter;
  305. if (!account::valid(candidate)) {
  306. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  307. return false;
  308. }
  309. runtime::storage::g_state.account = candidate;
  310. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  311. return true;
  312. }
  313. /** Prepares one checked equip transition without changing account State. */
  314. bool prepare_equipment_swap(std::uint64_t requestedInstanceSoid,
  315. PendingEquipmentSwap& mutation) noexcept {
  316. mutation = {};
  317. const AccountState account = account_snapshot();
  318. if (requestedInstanceSoid == 0 || !account::valid(account)) {
  319. return false;
  320. }
  321. std::size_t characterIndex = account.characterCount;
  322. for (std::size_t index = 0; index < account.characterCount; ++index) {
  323. if (account.characters[index].selected) {
  324. characterIndex = index;
  325. break;
  326. }
  327. }
  328. if (characterIndex == account.characterCount) {
  329. return false;
  330. }
  331. const CharacterState& before = account.characters[characterIndex];
  332. family4_loadout::ResolvedLoadout beforeLoadout{};
  333. if (!family4_loadout::resolve(account, characterIndex, beforeLoadout)) {
  334. return false;
  335. }
  336. std::size_t inventoryIndex = before.inventory.count;
  337. for (std::size_t index = 0; index < before.inventory.count; ++index) {
  338. if (before.inventory.values[index].instanceSoid != requestedInstanceSoid) {
  339. continue;
  340. }
  341. if (inventoryIndex != before.inventory.count) {
  342. return false;
  343. }
  344. inventoryIndex = index;
  345. }
  346. if (inventoryIndex == before.inventory.count) {
  347. return false;
  348. }
  349. const authored_inventory::Item& requested = before.inventory.values[inventoryIndex];
  350. std::uint8_t requestedNativeSlot = 0;
  351. std::size_t equipmentSlotIndex = authored_inventory::kEquipmentSlotCount;
  352. ResolvedPosition requestedPosition{};
  353. if (!native_equipment_slot(requested, requestedNativeSlot)
  354. || !semantic_equipment_slot(requestedNativeSlot, equipmentSlotIndex)
  355. || !find_resolved_position(beforeLoadout, requestedInstanceSoid, requestedPosition)
  356. || requestedPosition.equipped || requestedPosition.equipmentSlot != requestedNativeSlot) {
  357. return false;
  358. }
  359. CharacterState after = before;
  360. auto& equipped = after.equipment.slots[equipmentSlotIndex];
  361. std::uint64_t previousInstanceSoid = 0;
  362. if (equipped.has_value()) {
  363. std::uint8_t previousNativeSlot = 0;
  364. ResolvedPosition previousPosition{};
  365. if (!native_equipment_slot(*equipped, previousNativeSlot)
  366. || previousNativeSlot != requestedNativeSlot
  367. || !find_resolved_position(beforeLoadout, equipped->instanceSoid, previousPosition)
  368. || !previousPosition.equipped
  369. || previousPosition.equipmentSlot != requestedNativeSlot) {
  370. return false;
  371. }
  372. previousInstanceSoid = equipped->instanceSoid;
  373. std::swap(*equipped, after.inventory.values[inventoryIndex]);
  374. } else {
  375. equipped = after.inventory.values[inventoryIndex];
  376. for (std::size_t index = inventoryIndex; index + 1U < after.inventory.count; ++index) {
  377. after.inventory.values[index] = after.inventory.values[index + 1U];
  378. }
  379. --after.inventory.count;
  380. after.inventory.values[after.inventory.count] = {};
  381. }
  382. std::size_t movedItemCount = 0;
  383. if (!finalize_equipment_transition(account,
  384. characterIndex,
  385. requestedInstanceSoid,
  386. EquipmentMutationKind::equip,
  387. requestedNativeSlot,
  388. beforeLoadout,
  389. after,
  390. movedItemCount)) {
  391. return false;
  392. }
  393. if (previousInstanceSoid != 0) {
  394. // The serial on an unequipped row is also the Client's ordering token for that bucket. A
  395. // fresh greatest serial would move the displaced item to the first cell, so transfer the
  396. // selected row's prior token instead and it keeps the cell the player clicked.
  397. authored_inventory::Item& displaced = after.inventory.values[inventoryIndex];
  398. if (displaced.instanceSoid != previousInstanceSoid) {
  399. return false;
  400. }
  401. displaced.mutationSerial = requestedPosition.mutationSerial;
  402. AccountState checkedAccount = account;
  403. checkedAccount.characters[characterIndex] = after;
  404. family4_loadout::ResolvedLoadout checkedLoadout{};
  405. ResolvedPosition displacedPosition{};
  406. if (!account::valid(checkedAccount)
  407. || !family4_loadout::resolve(checkedAccount, characterIndex, checkedLoadout)
  408. || !find_resolved_position(checkedLoadout, previousInstanceSoid, displacedPosition)
  409. || displacedPosition.equipped || displacedPosition.equipmentSlot != requestedNativeSlot
  410. || displacedPosition.inventoryRow != requestedPosition.inventoryRow
  411. || displacedPosition.mutationSerial != requestedPosition.mutationSerial) {
  412. return false;
  413. }
  414. }
  415. mutation.beforeCharacter = before;
  416. mutation.afterCharacter = after;
  417. mutation.characterSoid = before.soid;
  418. mutation.requestedInstanceSoid = requestedInstanceSoid;
  419. mutation.previousInstanceSoid = previousInstanceSoid;
  420. mutation.characterIndex = characterIndex;
  421. mutation.equipmentSlotIndex = equipmentSlotIndex;
  422. mutation.inventoryIndex = inventoryIndex;
  423. mutation.movedItemCount = movedItemCount;
  424. mutation.nativeEquipmentSlot = requestedNativeSlot;
  425. mutation.kind = EquipmentMutationKind::equip;
  426. mutation.prepared = true;
  427. return true;
  428. }
  429. /** Prepares one checked equipped-to-inventory transition without changing account State. */
  430. bool prepare_equipment_unequip(std::uint64_t requestedInstanceSoid,
  431. PendingEquipmentSwap& mutation) noexcept {
  432. mutation = {};
  433. const AccountState account = account_snapshot();
  434. if (requestedInstanceSoid == 0 || !account::valid(account)) {
  435. return false;
  436. }
  437. std::size_t characterIndex = account.characterCount;
  438. for (std::size_t index = 0; index < account.characterCount; ++index) {
  439. if (account.characters[index].selected) {
  440. characterIndex = index;
  441. break;
  442. }
  443. }
  444. if (characterIndex == account.characterCount) {
  445. return false;
  446. }
  447. const CharacterState& before = account.characters[characterIndex];
  448. if (before.inventory.count >= before.inventory.values.size()) {
  449. return false;
  450. }
  451. family4_loadout::ResolvedLoadout beforeLoadout{};
  452. if (!family4_loadout::resolve(account, characterIndex, beforeLoadout)) {
  453. return false;
  454. }
  455. std::size_t equipmentSlotIndex = before.equipment.slots.size();
  456. for (std::size_t index = 0; index < before.equipment.slots.size(); ++index) {
  457. const auto& item = before.equipment.slots[index];
  458. if (!item.has_value() || item->instanceSoid != requestedInstanceSoid) {
  459. continue;
  460. }
  461. if (equipmentSlotIndex != before.equipment.slots.size()) {
  462. return false;
  463. }
  464. equipmentSlotIndex = index;
  465. }
  466. if (equipmentSlotIndex == before.equipment.slots.size()) {
  467. return false;
  468. }
  469. const authored_inventory::Item& requested = *before.equipment.slots[equipmentSlotIndex];
  470. std::uint8_t requestedNativeSlot = 0;
  471. std::uint8_t requestedBucketId = 0;
  472. std::size_t expectedSemanticIndex = authored_inventory::kEquipmentSlotCount;
  473. ResolvedPosition requestedPosition{};
  474. if (!native_equipment_slot(requested, requestedNativeSlot)
  475. || !inventory_bucket_id(requested, requestedBucketId)
  476. || !semantic_equipment_slot(requestedNativeSlot, expectedSemanticIndex)
  477. || expectedSemanticIndex != equipmentSlotIndex
  478. || !find_resolved_position(beforeLoadout, requestedInstanceSoid, requestedPosition)
  479. || !requestedPosition.equipped || requestedPosition.equipmentSlot != requestedNativeSlot) {
  480. return false;
  481. }
  482. std::size_t inventoryIndex = before.inventory.count;
  483. for (std::size_t index = 0; index < before.inventory.count; ++index) {
  484. std::uint8_t inventoryBucketId = 0;
  485. if (!inventory_bucket_id(before.inventory.values[index], inventoryBucketId)) {
  486. return false;
  487. }
  488. if (inventoryBucketId == requestedBucketId) {
  489. inventoryIndex = index;
  490. break;
  491. }
  492. }
  493. CharacterState after = before;
  494. const authored_inventory::Item unequipped = *after.equipment.slots[equipmentSlotIndex];
  495. for (std::size_t index = after.inventory.count; index > inventoryIndex; --index) {
  496. after.inventory.values[index] = after.inventory.values[index - 1U];
  497. }
  498. after.inventory.values[inventoryIndex] = unequipped;
  499. ++after.inventory.count;
  500. after.equipment.slots[equipmentSlotIndex].reset();
  501. std::size_t movedItemCount = 0;
  502. if (!finalize_equipment_transition(account,
  503. characterIndex,
  504. requestedInstanceSoid,
  505. EquipmentMutationKind::unequip,
  506. requestedNativeSlot,
  507. beforeLoadout,
  508. after,
  509. movedItemCount)) {
  510. return false;
  511. }
  512. mutation.beforeCharacter = before;
  513. mutation.afterCharacter = after;
  514. mutation.characterSoid = before.soid;
  515. mutation.requestedInstanceSoid = requestedInstanceSoid;
  516. mutation.characterIndex = characterIndex;
  517. mutation.equipmentSlotIndex = equipmentSlotIndex;
  518. mutation.inventoryIndex = inventoryIndex;
  519. mutation.movedItemCount = movedItemCount;
  520. mutation.nativeEquipmentSlot = requestedNativeSlot;
  521. mutation.kind = EquipmentMutationKind::unequip;
  522. mutation.prepared = true;
  523. return true;
  524. }
  525. /** Commits one prepared equipment after-image behind an exact character staleness guard. */
  526. bool commit_equipment_swap(PendingEquipmentSwap& mutation) noexcept {
  527. const PendingEquipmentSwap& prepared = mutation;
  528. const PendingConsumption consume{mutation};
  529. if (!prepared.prepared || prepared.characterSoid == 0 || prepared.requestedInstanceSoid == 0
  530. || (prepared.kind != EquipmentMutationKind::equip
  531. && prepared.kind != EquipmentMutationKind::unequip)
  532. || prepared.characterIndex >= kCharacterCapacity
  533. || prepared.equipmentSlotIndex >= authored_inventory::kEquipmentSlotCount
  534. || prepared.inventoryIndex >= authored_inventory::kCharacterItemCapacity
  535. || prepared.nativeEquipmentSlot >= item_details::kEquipmentSlotCount
  536. || prepared.beforeCharacter.soid != prepared.characterSoid
  537. || prepared.afterCharacter.soid != prepared.characterSoid) {
  538. return false;
  539. }
  540. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  541. AccountState candidate = runtime::storage::g_state.account;
  542. if (prepared.characterIndex >= candidate.characterCount
  543. || !same_character(candidate.characters[prepared.characterIndex],
  544. prepared.beforeCharacter)) {
  545. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  546. return false;
  547. }
  548. candidate.characters[prepared.characterIndex] = prepared.afterCharacter;
  549. family4_loadout::ResolvedLoadout checkedAfter{};
  550. if (!account::valid(candidate)
  551. || !family4_loadout::resolve(candidate, prepared.characterIndex, checkedAfter)) {
  552. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  553. return false;
  554. }
  555. ResolvedPosition requestedPosition{};
  556. const bool expectedEquipped = prepared.kind == EquipmentMutationKind::equip;
  557. if (!find_resolved_position(checkedAfter, prepared.requestedInstanceSoid, requestedPosition)
  558. || requestedPosition.equipmentSlot != prepared.nativeEquipmentSlot
  559. || requestedPosition.equipped != expectedEquipped) {
  560. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  561. return false;
  562. }
  563. runtime::storage::g_state.account = candidate;
  564. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  565. // The published ability buckets resolve against whichever subclass is equipped, so swapping
  566. // that item away makes the domain stale the same way an ability-entry pick does. It needs the
  567. // same invalidation or the character screen keeps showing the previous resolution.
  568. if (prepared.equipmentSlotIndex
  569. == static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass)) {
  570. build_data::invalidate_ability_buckets();
  571. }
  572. return true;
  573. }
  574. /** @return A copy of the active account state, read under the lock. */
  575. AccountState account_snapshot() noexcept {
  576. AcquireSRWLockShared(&runtime::storage::g_stateLock);
  577. const AccountState snapshot = runtime::storage::g_state.account;
  578. ReleaseSRWLockShared(&runtime::storage::g_stateLock);
  579. return snapshot;
  580. }
  581. /** Grants each character the other 2 subclasses of its equipped subclass's class. */
  582. bool ensure_character_subclasses() noexcept {
  583. // Index of the subclass slot in the authored equipment array.
  584. constexpr std::size_t kSubclassSlot =
  585. static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass);
  586. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  587. AccountState candidate = runtime::storage::g_state.account;
  588. if (!account::valid(candidate)) {
  589. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  590. return true;
  591. }
  592. std::uint64_t nextSoid = 0;
  593. bool haveNextSoid = false;
  594. bool changed = false;
  595. bool failed = false;
  596. for (std::size_t characterIndex = 0; characterIndex < candidate.characterCount && !failed;
  597. ++characterIndex) {
  598. CharacterState& character = candidate.characters[characterIndex];
  599. const std::optional<authored_inventory::Item>& equipped =
  600. character.equipment.slots[kSubclassSlot];
  601. if (!equipped.has_value()) {
  602. continue;
  603. }
  604. build_data::items::Definition equippedDefinition{};
  605. std::array<std::uint16_t, build_data::kSubclassGroupSize> group{};
  606. if (!build_data::find_item_definition_hash(equipped->definitionHash, equippedDefinition)
  607. || !build_data::find_subclass_group(equippedDefinition.definitionIndex, group)) {
  608. continue;
  609. }
  610. for (const std::uint16_t memberIndex : group) {
  611. if (memberIndex == equippedDefinition.definitionIndex) {
  612. continue;
  613. }
  614. build_data::items::Definition memberDefinition{};
  615. if (!build_data::find_item_definition_index(memberIndex, memberDefinition)
  616. || memberDefinition.definitionIndex != memberIndex) {
  617. continue;
  618. }
  619. bool present = false;
  620. for (std::size_t itemIndex = 0; itemIndex < character.inventory.count; ++itemIndex) {
  621. if (character.inventory.values[itemIndex].definitionHash
  622. == memberDefinition.definitionHash) {
  623. present = true;
  624. break;
  625. }
  626. }
  627. if (present || character.inventory.count >= character.inventory.values.size()) {
  628. continue;
  629. }
  630. if (!haveNextSoid) {
  631. if (!next_item_instance_soid(candidate, nextSoid)) {
  632. failed = true;
  633. break;
  634. }
  635. haveNextSoid = true;
  636. }
  637. authored_inventory::Item granted{};
  638. granted.instanceSoid = nextSoid++;
  639. granted.definitionHash = memberDefinition.definitionHash;
  640. granted.level = 0;
  641. granted.quantity = 1;
  642. // Every resolved item's serial must stay behind the character's own counter (checked
  643. // by the character encoder, not by account::valid), so claim the next one here too.
  644. granted.mutationSerial = static_cast<std::int32_t>(character.nextInventorySerial++);
  645. granted.sockets.policy = authored_inventory::SocketPolicy::nativeDefaults;
  646. character.inventory.values[character.inventory.count++] = granted;
  647. changed = true;
  648. }
  649. }
  650. if (failed || !changed || !account::valid(candidate)) {
  651. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  652. return !failed;
  653. }
  654. runtime::storage::g_state.account = candidate;
  655. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  656. return true;
  657. }
  658. } // namespace sunrise::state