state_account_runtime.cpp 34 KB

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