state_account_runtime.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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. /** Writes one exhaustive equipment-transaction checkpoint to the persistent diagnostic log. */
  24. void report_equipment(std::string_view stage,
  25. std::string_view result,
  26. EquipmentMutationKind kind,
  27. std::uint64_t characterSoid,
  28. std::uint64_t previousSoid,
  29. std::uint64_t requestedSoid,
  30. std::size_t equipmentIndex,
  31. std::size_t inventoryIndex,
  32. std::uint8_t nativeSlot,
  33. std::size_t movedItemCount,
  34. std::uint32_t previousHash,
  35. std::uint32_t requestedHash) noexcept {
  36. const std::string_view operation = kind == EquipmentMutationKind::unequip ? "unequip" : "equip";
  37. std::array<char, core::log::kLineCapacity> line{};
  38. const int count = std::snprintf(
  39. line.data(),
  40. line.size(),
  41. "ev=equip operation=%.*s stage=%.*s result=%.*s character=0x%llX previous=0x%llX "
  42. "requested=0x%llX equipment_index=%zu inventory_index=%zu native_slot=%u "
  43. "moved_items=%zu previous_hash=0x%08X requested_hash=0x%08X",
  44. static_cast<int>(operation.size()),
  45. operation.data(),
  46. static_cast<int>(stage.size()),
  47. stage.data(),
  48. static_cast<int>(result.size()),
  49. result.data(),
  50. static_cast<unsigned long long>(characterSoid),
  51. static_cast<unsigned long long>(previousSoid),
  52. static_cast<unsigned long long>(requestedSoid),
  53. equipmentIndex,
  54. inventoryIndex,
  55. static_cast<unsigned>(nativeSlot),
  56. movedItemCount,
  57. previousHash,
  58. requestedHash);
  59. if (count > 0) {
  60. core::log::write(core::log::Channel::state,
  61. result == "ok" ? core::log::Level::debug : core::log::Level::warn,
  62. {line.data(), static_cast<std::size_t>(count)});
  63. }
  64. }
  65. /** Writes one exhaustive item-acquisition transaction checkpoint. */
  66. void report_acquisition(std::string_view stage,
  67. std::string_view result,
  68. std::string_view reason,
  69. std::uint32_t definitionHash,
  70. std::uint64_t characterSoid,
  71. std::uint64_t instanceSoid,
  72. std::size_t inventoryIndex,
  73. std::uint16_t inventoryRow,
  74. std::uint8_t equipmentSlot,
  75. std::uint32_t nextInventorySerial) noexcept {
  76. std::array<char, core::log::kLineCapacity> line{};
  77. const int count = std::snprintf(
  78. line.data(),
  79. line.size(),
  80. "ev=acquire stage=%.*s result=%.*s reason=%.*s definition_hash=0x%08X character=0x%llX "
  81. "instance=0x%llX inventory_index=%zu inventory_row=%u equipment_slot=%u next_serial=%u",
  82. static_cast<int>(stage.size()),
  83. stage.data(),
  84. static_cast<int>(result.size()),
  85. result.data(),
  86. static_cast<int>(reason.size()),
  87. reason.data(),
  88. definitionHash,
  89. static_cast<unsigned long long>(characterSoid),
  90. static_cast<unsigned long long>(instanceSoid),
  91. inventoryIndex,
  92. static_cast<unsigned>(inventoryRow),
  93. static_cast<unsigned>(equipmentSlot),
  94. nextInventorySerial);
  95. if (count > 0) {
  96. core::log::write(core::log::Channel::state,
  97. result == "ok" ? core::log::Level::debug : core::log::Level::warn,
  98. {line.data(), static_cast<std::size_t>(count)});
  99. }
  100. }
  101. } // namespace runtime::detail
  102. using namespace runtime::detail;
  103. /** Stores the active account key without publishing an incomplete account. */
  104. bool set_primary_soid(std::uint64_t primarySoid) noexcept {
  105. if (primarySoid == 0) {
  106. return false;
  107. }
  108. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  109. AccountState candidate = runtime::storage::g_state.account;
  110. candidate.primarySoid = primarySoid;
  111. // Characters belong to the account key the Client uses. The reference account and its
  112. // characters differ only in the low byte, so the authored rows are rebased onto that key.
  113. for (std::size_t index = 0; index < candidate.characterCount; ++index) {
  114. candidate.characters[index].soid = primarySoid + 1U + index;
  115. }
  116. if (!account::valid(candidate)) {
  117. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  118. return false;
  119. }
  120. // Publish only after the settings and identity rules hold together.
  121. runtime::storage::g_state.account = candidate;
  122. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  123. return true;
  124. }
  125. /** Moves the selection to one authored character. */
  126. bool set_selected_character(std::uint64_t characterSoid, bool& changed) noexcept {
  127. changed = false;
  128. if (characterSoid == 0) {
  129. return false;
  130. }
  131. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  132. AccountState candidate = runtime::storage::g_state.account;
  133. std::size_t picked = candidate.characterCount;
  134. for (std::size_t index = 0; index < candidate.characterCount; ++index) {
  135. if (candidate.characters[index].soid == characterSoid) {
  136. picked = index;
  137. }
  138. }
  139. if (picked == candidate.characterCount) {
  140. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  141. return false;
  142. }
  143. const bool alreadySelected = candidate.characters[picked].selected;
  144. for (CharacterState& character : candidate.characters) {
  145. character.selected = false;
  146. }
  147. candidate.characters[picked].selected = true;
  148. if (!account::valid(candidate)) {
  149. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  150. return false;
  151. }
  152. // Publish only after the whole account still meets its identity rules.
  153. runtime::storage::g_state.account = candidate;
  154. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  155. changed = !alreadySelected;
  156. return true;
  157. }
  158. /** Prepares one checked equip transition without changing account State. */
  159. bool prepare_equipment_swap(std::uint64_t requestedInstanceSoid,
  160. PendingEquipmentSwap& mutation) noexcept {
  161. mutation = {};
  162. const AccountState account = account_snapshot();
  163. if (requestedInstanceSoid == 0 || !account::valid(account)) {
  164. return false;
  165. }
  166. std::size_t characterIndex = account.characterCount;
  167. for (std::size_t index = 0; index < account.characterCount; ++index) {
  168. if (account.characters[index].selected) {
  169. characterIndex = index;
  170. break;
  171. }
  172. }
  173. if (characterIndex == account.characterCount) {
  174. return false;
  175. }
  176. const CharacterState& before = account.characters[characterIndex];
  177. family4_loadout::ResolvedLoadout beforeLoadout{};
  178. if (!family4_loadout::resolve(account, characterIndex, beforeLoadout)) {
  179. return false;
  180. }
  181. std::size_t inventoryIndex = before.inventory.count;
  182. for (std::size_t index = 0; index < before.inventory.count; ++index) {
  183. if (before.inventory.values[index].instanceSoid != requestedInstanceSoid) {
  184. continue;
  185. }
  186. if (inventoryIndex != before.inventory.count) {
  187. return false;
  188. }
  189. inventoryIndex = index;
  190. }
  191. if (inventoryIndex == before.inventory.count) {
  192. return false;
  193. }
  194. const authored_inventory::Item& requested = before.inventory.values[inventoryIndex];
  195. std::uint8_t requestedNativeSlot = 0;
  196. std::size_t equipmentSlotIndex = authored_inventory::kEquipmentSlotCount;
  197. ResolvedPosition requestedPosition{};
  198. if (!native_equipment_slot(requested, requestedNativeSlot)
  199. || !semantic_equipment_slot(requestedNativeSlot, equipmentSlotIndex)
  200. || !find_resolved_position(beforeLoadout, requestedInstanceSoid, requestedPosition)
  201. || requestedPosition.equipped || requestedPosition.equipmentSlot != requestedNativeSlot) {
  202. return false;
  203. }
  204. CharacterState after = before;
  205. auto& equipped = after.equipment.slots[equipmentSlotIndex];
  206. std::uint64_t previousInstanceSoid = 0;
  207. std::uint32_t previousDefinitionHash = 0;
  208. if (equipped.has_value()) {
  209. std::uint8_t previousNativeSlot = 0;
  210. ResolvedPosition previousPosition{};
  211. if (!native_equipment_slot(*equipped, previousNativeSlot)
  212. || previousNativeSlot != requestedNativeSlot
  213. || !find_resolved_position(beforeLoadout, equipped->instanceSoid, previousPosition)
  214. || !previousPosition.equipped
  215. || previousPosition.equipmentSlot != requestedNativeSlot) {
  216. return false;
  217. }
  218. previousInstanceSoid = equipped->instanceSoid;
  219. previousDefinitionHash = equipped->definitionHash;
  220. std::swap(*equipped, after.inventory.values[inventoryIndex]);
  221. } else {
  222. equipped = after.inventory.values[inventoryIndex];
  223. for (std::size_t index = inventoryIndex; index + 1U < after.inventory.count; ++index) {
  224. after.inventory.values[index] = after.inventory.values[index + 1U];
  225. }
  226. --after.inventory.count;
  227. after.inventory.values[after.inventory.count] = {};
  228. }
  229. std::size_t movedItemCount = 0;
  230. if (!finalize_equipment_transition(account,
  231. characterIndex,
  232. requestedInstanceSoid,
  233. EquipmentMutationKind::equip,
  234. requestedNativeSlot,
  235. beforeLoadout,
  236. after,
  237. movedItemCount)) {
  238. return false;
  239. }
  240. mutation.beforeCharacter = before;
  241. mutation.afterCharacter = after;
  242. mutation.characterSoid = before.soid;
  243. mutation.requestedInstanceSoid = requestedInstanceSoid;
  244. mutation.previousInstanceSoid = previousInstanceSoid;
  245. mutation.characterIndex = characterIndex;
  246. mutation.equipmentSlotIndex = equipmentSlotIndex;
  247. mutation.inventoryIndex = inventoryIndex;
  248. mutation.movedItemCount = movedItemCount;
  249. mutation.nativeEquipmentSlot = requestedNativeSlot;
  250. mutation.kind = EquipmentMutationKind::equip;
  251. mutation.prepared = true;
  252. report_equipment("prepare",
  253. "ok",
  254. mutation.kind,
  255. mutation.characterSoid,
  256. mutation.previousInstanceSoid,
  257. mutation.requestedInstanceSoid,
  258. mutation.equipmentSlotIndex,
  259. mutation.inventoryIndex,
  260. mutation.nativeEquipmentSlot,
  261. mutation.movedItemCount,
  262. previousDefinitionHash,
  263. requested.definitionHash);
  264. return true;
  265. }
  266. /** Prepares one checked equipped-to-inventory transition without changing account State. */
  267. bool prepare_equipment_unequip(std::uint64_t requestedInstanceSoid,
  268. PendingEquipmentSwap& mutation) noexcept {
  269. mutation = {};
  270. const AccountState account = account_snapshot();
  271. if (requestedInstanceSoid == 0 || !account::valid(account)) {
  272. return false;
  273. }
  274. std::size_t characterIndex = account.characterCount;
  275. for (std::size_t index = 0; index < account.characterCount; ++index) {
  276. if (account.characters[index].selected) {
  277. characterIndex = index;
  278. break;
  279. }
  280. }
  281. if (characterIndex == account.characterCount) {
  282. return false;
  283. }
  284. const CharacterState& before = account.characters[characterIndex];
  285. if (before.inventory.count >= before.inventory.values.size()) {
  286. return false;
  287. }
  288. family4_loadout::ResolvedLoadout beforeLoadout{};
  289. if (!family4_loadout::resolve(account, characterIndex, beforeLoadout)) {
  290. return false;
  291. }
  292. std::size_t equipmentSlotIndex = before.equipment.slots.size();
  293. for (std::size_t index = 0; index < before.equipment.slots.size(); ++index) {
  294. const auto& item = before.equipment.slots[index];
  295. if (!item.has_value() || item->instanceSoid != requestedInstanceSoid) {
  296. continue;
  297. }
  298. if (equipmentSlotIndex != before.equipment.slots.size()) {
  299. return false;
  300. }
  301. equipmentSlotIndex = index;
  302. }
  303. if (equipmentSlotIndex == before.equipment.slots.size()) {
  304. return false;
  305. }
  306. const authored_inventory::Item& requested = *before.equipment.slots[equipmentSlotIndex];
  307. std::uint8_t requestedNativeSlot = 0;
  308. std::uint8_t requestedBucketId = 0;
  309. std::size_t expectedSemanticIndex = authored_inventory::kEquipmentSlotCount;
  310. ResolvedPosition requestedPosition{};
  311. if (!native_equipment_slot(requested, requestedNativeSlot)
  312. || !inventory_bucket_id(requested, requestedBucketId)
  313. || !semantic_equipment_slot(requestedNativeSlot, expectedSemanticIndex)
  314. || expectedSemanticIndex != equipmentSlotIndex
  315. || !find_resolved_position(beforeLoadout, requestedInstanceSoid, requestedPosition)
  316. || !requestedPosition.equipped || requestedPosition.equipmentSlot != requestedNativeSlot) {
  317. return false;
  318. }
  319. std::size_t inventoryIndex = before.inventory.count;
  320. for (std::size_t index = 0; index < before.inventory.count; ++index) {
  321. std::uint8_t inventoryBucketId = 0;
  322. if (!inventory_bucket_id(before.inventory.values[index], inventoryBucketId)) {
  323. return false;
  324. }
  325. if (inventoryBucketId == requestedBucketId) {
  326. inventoryIndex = index;
  327. break;
  328. }
  329. }
  330. CharacterState after = before;
  331. const authored_inventory::Item unequipped = *after.equipment.slots[equipmentSlotIndex];
  332. for (std::size_t index = after.inventory.count; index > inventoryIndex; --index) {
  333. after.inventory.values[index] = after.inventory.values[index - 1U];
  334. }
  335. after.inventory.values[inventoryIndex] = unequipped;
  336. ++after.inventory.count;
  337. after.equipment.slots[equipmentSlotIndex].reset();
  338. std::size_t movedItemCount = 0;
  339. if (!finalize_equipment_transition(account,
  340. characterIndex,
  341. requestedInstanceSoid,
  342. EquipmentMutationKind::unequip,
  343. requestedNativeSlot,
  344. beforeLoadout,
  345. after,
  346. movedItemCount)) {
  347. return false;
  348. }
  349. mutation.beforeCharacter = before;
  350. mutation.afterCharacter = after;
  351. mutation.characterSoid = before.soid;
  352. mutation.requestedInstanceSoid = requestedInstanceSoid;
  353. mutation.characterIndex = characterIndex;
  354. mutation.equipmentSlotIndex = equipmentSlotIndex;
  355. mutation.inventoryIndex = inventoryIndex;
  356. mutation.movedItemCount = movedItemCount;
  357. mutation.nativeEquipmentSlot = requestedNativeSlot;
  358. mutation.kind = EquipmentMutationKind::unequip;
  359. mutation.prepared = true;
  360. report_equipment("prepare",
  361. "ok",
  362. mutation.kind,
  363. mutation.characterSoid,
  364. mutation.previousInstanceSoid,
  365. mutation.requestedInstanceSoid,
  366. mutation.equipmentSlotIndex,
  367. mutation.inventoryIndex,
  368. mutation.nativeEquipmentSlot,
  369. mutation.movedItemCount,
  370. 0,
  371. requested.definitionHash);
  372. return true;
  373. }
  374. /** Commits one prepared equipment after-image behind an exact character staleness guard. */
  375. bool commit_equipment_swap(PendingEquipmentSwap& mutation) noexcept {
  376. const PendingEquipmentSwap prepared = mutation;
  377. mutation = {};
  378. if (!prepared.prepared || prepared.characterSoid == 0 || prepared.requestedInstanceSoid == 0
  379. || (prepared.kind != EquipmentMutationKind::equip
  380. && prepared.kind != EquipmentMutationKind::unequip)
  381. || prepared.characterIndex >= kCharacterCapacity
  382. || prepared.equipmentSlotIndex >= authored_inventory::kEquipmentSlotCount
  383. || prepared.inventoryIndex >= authored_inventory::kCharacterItemCapacity
  384. || prepared.nativeEquipmentSlot >= item_details::kEquipmentSlotCount
  385. || prepared.beforeCharacter.soid != prepared.characterSoid
  386. || prepared.afterCharacter.soid != prepared.characterSoid) {
  387. return false;
  388. }
  389. const std::uint32_t previousDefinitionHash =
  390. character_item_definition_hash(prepared.afterCharacter, prepared.previousInstanceSoid);
  391. const std::uint32_t requestedDefinitionHash =
  392. character_item_definition_hash(prepared.afterCharacter, prepared.requestedInstanceSoid);
  393. report_equipment("commit_begin",
  394. "ok",
  395. prepared.kind,
  396. prepared.characterSoid,
  397. prepared.previousInstanceSoid,
  398. prepared.requestedInstanceSoid,
  399. prepared.equipmentSlotIndex,
  400. prepared.inventoryIndex,
  401. prepared.nativeEquipmentSlot,
  402. prepared.movedItemCount,
  403. previousDefinitionHash,
  404. requestedDefinitionHash);
  405. AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
  406. AccountState candidate = runtime::storage::g_state.account;
  407. if (prepared.characterIndex >= candidate.characterCount
  408. || !same_character(candidate.characters[prepared.characterIndex],
  409. prepared.beforeCharacter)) {
  410. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  411. return false;
  412. }
  413. candidate.characters[prepared.characterIndex] = prepared.afterCharacter;
  414. family4_loadout::ResolvedLoadout checkedAfter{};
  415. if (!account::valid(candidate)
  416. || !family4_loadout::resolve(candidate, prepared.characterIndex, checkedAfter)) {
  417. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  418. return false;
  419. }
  420. ResolvedPosition requestedPosition{};
  421. const bool expectedEquipped = prepared.kind == EquipmentMutationKind::equip;
  422. if (!find_resolved_position(checkedAfter, prepared.requestedInstanceSoid, requestedPosition)
  423. || requestedPosition.equipmentSlot != prepared.nativeEquipmentSlot
  424. || requestedPosition.equipped != expectedEquipped) {
  425. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  426. return false;
  427. }
  428. runtime::storage::g_state.account = candidate;
  429. ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
  430. report_equipment("commit_end",
  431. "ok",
  432. prepared.kind,
  433. prepared.characterSoid,
  434. prepared.previousInstanceSoid,
  435. prepared.requestedInstanceSoid,
  436. prepared.equipmentSlotIndex,
  437. prepared.inventoryIndex,
  438. prepared.nativeEquipmentSlot,
  439. prepared.movedItemCount,
  440. previousDefinitionHash,
  441. requestedDefinitionHash);
  442. return true;
  443. }
  444. /** @return A copy of the active account state, read under the lock. */
  445. AccountState account_snapshot() noexcept {
  446. AcquireSRWLockShared(&runtime::storage::g_stateLock);
  447. const AccountState snapshot = runtime::storage::g_state.account;
  448. ReleaseSRWLockShared(&runtime::storage::g_stateLock);
  449. return snapshot;
  450. }
  451. } // namespace sunrise::state