state_account_equipment_runtime.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. /**
  2. * Equipment placement helpers: native and semantic slots, resolved positions, and the
  3. * comparisons one equipment transition is checked against.
  4. */
  5. #include <Windows.h>
  6. #include <algorithm>
  7. #include <array>
  8. #include <cstddef>
  9. #include <cstdint>
  10. #include <cstdio>
  11. #include <limits>
  12. #include <string_view>
  13. #include <utility>
  14. #include "../../core/logging/log.h"
  15. #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
  16. #include "../build_data/runtime.h"
  17. #include "runtime.h"
  18. #include "state.h"
  19. #include "state_account_transaction_helpers.h"
  20. #include "storage/internal.h"
  21. namespace sunrise::state {
  22. namespace runtime::detail {
  23. namespace authored_inventory = account::inventory;
  24. namespace item_details = build_data::items::details;
  25. namespace inventory_buckets = build_data::inventory::buckets;
  26. namespace family4_loadout = middleware::datagen::family4::loadout;
  27. /** Resolves the installed native equipment slot for one configured authored item. */
  28. [[nodiscard]] bool native_equipment_slot(const authored_inventory::Item& item,
  29. std::uint8_t& slot) noexcept {
  30. build_data::items::Definition definition{};
  31. item_details::Definition detail{};
  32. if (!build_data::find_item_definition_hash(item.definitionHash, definition)
  33. || definition.definitionHash != item.definitionHash
  34. || !build_data::find_configured_item_detail(definition.definitionIndex, detail)
  35. || detail.definitionIndex != definition.definitionIndex
  36. || detail.definitionHash != definition.definitionHash
  37. || detail.bucketId != definition.bucketId || !detail.equipmentSlot.has_value()
  38. || *detail.equipmentSlot < 0
  39. || static_cast<std::size_t>(*detail.equipmentSlot) >= item_details::kEquipmentSlotCount) {
  40. return false;
  41. }
  42. slot = static_cast<std::uint8_t>(*detail.equipmentSlot);
  43. return true;
  44. }
  45. /** Resolves the installed physical inventory bucket for one configured authored item. */
  46. [[nodiscard]] bool inventory_bucket_id(const authored_inventory::Item& item,
  47. std::uint8_t& bucketId) noexcept {
  48. build_data::items::Definition definition{};
  49. item_details::Definition detail{};
  50. if (!build_data::find_item_definition_hash(item.definitionHash, definition)
  51. || definition.definitionHash != item.definitionHash
  52. || !build_data::find_configured_item_detail(definition.definitionIndex, detail)
  53. || detail.definitionIndex != definition.definitionIndex
  54. || detail.definitionHash != definition.definitionHash
  55. || detail.bucketId != definition.bucketId) {
  56. return false;
  57. }
  58. bucketId = detail.bucketId;
  59. return true;
  60. }
  61. /** Maps the 16 proven native equipment positions onto their stable authored State slots. */
  62. [[nodiscard]] bool semantic_equipment_slot(std::uint8_t nativeSlot,
  63. std::size_t& semanticIndex) noexcept {
  64. using EquipmentSlot = authored_inventory::EquipmentSlot;
  65. EquipmentSlot semanticSlot = EquipmentSlot::count;
  66. switch (nativeSlot) {
  67. case 0:
  68. semanticSlot = EquipmentSlot::subclass;
  69. break;
  70. case 1:
  71. semanticSlot = EquipmentSlot::helmet;
  72. break;
  73. case 2:
  74. semanticSlot = EquipmentSlot::gauntlets;
  75. break;
  76. case 4:
  77. semanticSlot = EquipmentSlot::chest;
  78. break;
  79. case 5:
  80. semanticSlot = EquipmentSlot::legs;
  81. break;
  82. case 6:
  83. semanticSlot = EquipmentSlot::classItem;
  84. break;
  85. case 7:
  86. semanticSlot = EquipmentSlot::kinetic;
  87. break;
  88. case 8:
  89. semanticSlot = EquipmentSlot::energy;
  90. break;
  91. case 9:
  92. semanticSlot = EquipmentSlot::heavy;
  93. break;
  94. case 10:
  95. semanticSlot = EquipmentSlot::ship;
  96. break;
  97. case 11:
  98. semanticSlot = EquipmentSlot::vehicle;
  99. break;
  100. case 12:
  101. semanticSlot = EquipmentSlot::ghost;
  102. break;
  103. case 13:
  104. semanticSlot = EquipmentSlot::emblem;
  105. break;
  106. case 14:
  107. semanticSlot = EquipmentSlot::emote;
  108. break;
  109. case 15:
  110. semanticSlot = EquipmentSlot::clanBanner;
  111. break;
  112. case 17:
  113. semanticSlot = EquipmentSlot::finisher;
  114. break;
  115. default:
  116. return false;
  117. }
  118. semanticIndex = static_cast<std::size_t>(semanticSlot);
  119. return semanticIndex < authored_inventory::kEquipmentSlotCount;
  120. }
  121. /** Finds one instance exactly once in a checked, row-sorted loadout. */
  122. [[nodiscard]] bool find_resolved_position(const family4_loadout::ResolvedLoadout& loadout,
  123. std::uint64_t instanceSoid,
  124. ResolvedPosition& position) noexcept {
  125. bool found = false;
  126. for (std::size_t index = 0; index < loadout.itemCount; ++index) {
  127. const family4_loadout::ResolvedItem& item = loadout.items[index];
  128. if (item.instance.instanceSoid != instanceSoid) {
  129. continue;
  130. }
  131. if (found) {
  132. return false;
  133. }
  134. found = true;
  135. position.inventoryRow = item.inventoryRow;
  136. position.equipmentSlot = item.equipmentSlot;
  137. position.equipped = item.equipped;
  138. position.mutationSerial = item.mutationSerial;
  139. }
  140. return found;
  141. }
  142. /** @return True when the native placement and equipped marker are unchanged. */
  143. [[nodiscard]] bool same_position(const ResolvedPosition& left,
  144. const ResolvedPosition& right) noexcept {
  145. return left.inventoryRow == right.inventoryRow && left.equipmentSlot == right.equipmentSlot
  146. && left.equipped == right.equipped;
  147. }
  148. /**
  149. * Applies canonical mutation generations after one shape-only equipment transition.
  150. *
  151. * Every surviving instance must preserve its native bucket. A generation advances exactly when
  152. * its published native row or equipped marker changes, and a second resolution proves that the
  153. * stamped after-image retained the staged placement. Callers that need a moved item to keep an
  154. * older grid cell (equip swaps) rewrite that item's serial afterwards and re-validate.
  155. */
  156. [[nodiscard]] bool
  157. finalize_equipment_transition(const AccountState& account,
  158. std::size_t characterIndex,
  159. std::uint64_t requestedInstanceSoid,
  160. EquipmentMutationKind kind,
  161. std::uint8_t expectedNativeSlot,
  162. const family4_loadout::ResolvedLoadout& beforeLoadout,
  163. CharacterState& after,
  164. std::size_t& movedItemCount) noexcept {
  165. movedItemCount = 0;
  166. if (characterIndex >= account.characterCount || kind == EquipmentMutationKind::none) {
  167. return false;
  168. }
  169. AccountState candidate = account;
  170. candidate.characters[characterIndex] = after;
  171. family4_loadout::ResolvedLoadout placedAfter{};
  172. if (!account::valid(candidate)
  173. || !family4_loadout::resolve(candidate, characterIndex, placedAfter)
  174. || placedAfter.itemCount != beforeLoadout.itemCount) {
  175. return false;
  176. }
  177. ResolvedPosition beforeRequested{};
  178. ResolvedPosition afterRequested{};
  179. if (!find_resolved_position(beforeLoadout, requestedInstanceSoid, beforeRequested)
  180. || !find_resolved_position(placedAfter, requestedInstanceSoid, afterRequested)
  181. || beforeRequested.equipmentSlot != expectedNativeSlot
  182. || afterRequested.equipmentSlot != expectedNativeSlot
  183. || (kind == EquipmentMutationKind::equip
  184. && (beforeRequested.equipped || !afterRequested.equipped))
  185. || (kind == EquipmentMutationKind::unequip
  186. && (!beforeRequested.equipped || afterRequested.equipped))) {
  187. return false;
  188. }
  189. const auto count_move = [&](const authored_inventory::Item& item) noexcept {
  190. ResolvedPosition beforePosition{};
  191. ResolvedPosition afterPosition{};
  192. if (!find_resolved_position(beforeLoadout, item.instanceSoid, beforePosition)
  193. || !find_resolved_position(placedAfter, item.instanceSoid, afterPosition)
  194. || beforePosition.equipmentSlot != afterPosition.equipmentSlot) {
  195. return false;
  196. }
  197. movedItemCount += static_cast<std::size_t>(!same_position(beforePosition, afterPosition));
  198. return true;
  199. };
  200. for (const auto& item : after.equipment.slots) {
  201. if (item.has_value() && !count_move(*item)) {
  202. return false;
  203. }
  204. }
  205. for (std::size_t index = 0; index < after.inventory.count; ++index) {
  206. if (!count_move(after.inventory.values[index])) {
  207. return false;
  208. }
  209. }
  210. // The serial is signed on the wire, so it must stay inside the positive int32 range.
  211. constexpr std::uint32_t kMaximumInventorySerial =
  212. static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)());
  213. if (movedItemCount == 0 || after.nextInventorySerial > kMaximumInventorySerial
  214. || movedItemCount > kMaximumInventorySerial - after.nextInventorySerial) {
  215. return false;
  216. }
  217. const auto stamp_move = [&](authored_inventory::Item& item) noexcept {
  218. ResolvedPosition beforePosition{};
  219. ResolvedPosition afterPosition{};
  220. if (!find_resolved_position(beforeLoadout, item.instanceSoid, beforePosition)
  221. || !find_resolved_position(placedAfter, item.instanceSoid, afterPosition)
  222. || beforePosition.equipmentSlot != afterPosition.equipmentSlot) {
  223. return false;
  224. }
  225. if (!same_position(beforePosition, afterPosition)) {
  226. item.mutationSerial = static_cast<std::int32_t>(after.nextInventorySerial++);
  227. }
  228. return true;
  229. };
  230. for (auto& item : after.equipment.slots) {
  231. if (item.has_value() && !stamp_move(*item)) {
  232. return false;
  233. }
  234. }
  235. for (std::size_t index = 0; index < after.inventory.count; ++index) {
  236. if (!stamp_move(after.inventory.values[index])) {
  237. return false;
  238. }
  239. }
  240. candidate.characters[characterIndex] = after;
  241. family4_loadout::ResolvedLoadout checkedAfter{};
  242. if (!account::valid(candidate)
  243. || !family4_loadout::resolve(candidate, characterIndex, checkedAfter)
  244. || checkedAfter.itemCount != placedAfter.itemCount) {
  245. return false;
  246. }
  247. for (const auto& item : after.equipment.slots) {
  248. if (!item.has_value()) {
  249. continue;
  250. }
  251. ResolvedPosition placed{};
  252. ResolvedPosition checked{};
  253. if (!find_resolved_position(placedAfter, item->instanceSoid, placed)
  254. || !find_resolved_position(checkedAfter, item->instanceSoid, checked)
  255. || !same_position(placed, checked) || checked.mutationSerial != item->mutationSerial) {
  256. return false;
  257. }
  258. }
  259. for (std::size_t index = 0; index < after.inventory.count; ++index) {
  260. const authored_inventory::Item& item = after.inventory.values[index];
  261. ResolvedPosition placed{};
  262. ResolvedPosition checked{};
  263. if (!find_resolved_position(placedAfter, item.instanceSoid, placed)
  264. || !find_resolved_position(checkedAfter, item.instanceSoid, checked)
  265. || !same_position(placed, checked) || checked.mutationSerial != item.mutationSerial) {
  266. return false;
  267. }
  268. }
  269. return true;
  270. }
  271. /** @return True when two authored item values are identical, including socket policy and tail. */
  272. [[nodiscard]] bool same_item(const authored_inventory::Item& left,
  273. const authored_inventory::Item& right) noexcept {
  274. return left.instanceSoid == right.instanceSoid && left.definitionHash == right.definitionHash
  275. && left.level == right.level && left.quantity == right.quantity
  276. && left.flags == right.flags && left.sockets.policy == right.sockets.policy
  277. && left.sockets.plugCount == right.sockets.plugCount
  278. && left.sockets.plugs == right.sockets.plugs
  279. && left.movementAbilityEntry == right.movementAbilityEntry
  280. && left.grenadeAbilityEntry == right.grenadeAbilityEntry
  281. && left.superAbilityEntry == right.superAbilityEntry
  282. && left.meleeAbilityEntry == right.meleeAbilityEntry
  283. && left.classAbilityEntry == right.classAbilityEntry;
  284. }
  285. /** Records one checked native item-state transition. */
  286. void report_item_state(std::string_view stage,
  287. std::string_view result,
  288. std::string_view reason,
  289. std::uint64_t characterSoid,
  290. std::uint64_t instanceSoid,
  291. std::uint16_t definitionIndex,
  292. std::uint32_t beforeFlags,
  293. std::uint32_t afterFlags,
  294. bool equipped,
  295. std::size_t itemIndex) noexcept {
  296. std::array<char, core::log::kLineCapacity> line{};
  297. const int count = std::snprintf(
  298. line.data(),
  299. line.size(),
  300. "ev=item_state stage=%.*s result=%.*s reason=%.*s character=0x%llX instance=0x%llX "
  301. "definition=%u flags_before=0x%X flags_after=0x%X equipped=%u item_index=%zu",
  302. static_cast<int>(stage.size()),
  303. stage.data(),
  304. static_cast<int>(result.size()),
  305. result.data(),
  306. static_cast<int>(reason.size()),
  307. reason.data(),
  308. static_cast<unsigned long long>(characterSoid),
  309. static_cast<unsigned long long>(instanceSoid),
  310. static_cast<unsigned>(definitionIndex),
  311. beforeFlags,
  312. afterFlags,
  313. equipped ? 1U : 0U,
  314. itemIndex);
  315. if (count > 0) {
  316. core::log::write(core::log::Channel::state,
  317. result == "ok" ? core::log::Level::debug : core::log::Level::warn,
  318. {line.data(), static_cast<std::size_t>(count)});
  319. }
  320. }
  321. /** Exact stationary-item comparison, including its inventory mutation generation. */
  322. [[nodiscard]] bool same_stationary_item(const authored_inventory::Item& left,
  323. const authored_inventory::Item& right) noexcept {
  324. return same_item(left, right) && left.mutationSerial == right.mutationSerial;
  325. }
  326. /** @return True when every item-bearing field of two character views is identical. */
  327. [[nodiscard]] bool same_loadout(const CharacterState& left, const CharacterState& right) noexcept {
  328. if (left.soid != right.soid || left.selected != right.selected || left.race != right.race
  329. || left.gender != right.gender || left.characterClass != right.characterClass
  330. || left.level != right.level || left.accepted != right.accepted
  331. || left.previewAvailable != right.previewAvailable
  332. || left.appearanceValue != right.appearanceValue
  333. || left.lastOrbitedDestination != right.lastOrbitedDestination
  334. || left.contentBypass != right.contentBypass
  335. || left.nextInventorySerial != right.nextInventorySerial
  336. || left.inventory.count != right.inventory.count) {
  337. return false;
  338. }
  339. for (std::size_t index = 0; index < left.equipment.slots.size(); ++index) {
  340. const auto& leftItem = left.equipment.slots[index];
  341. const auto& rightItem = right.equipment.slots[index];
  342. if (leftItem.has_value() != rightItem.has_value()
  343. || (leftItem.has_value() && !same_stationary_item(*leftItem, *rightItem))) {
  344. return false;
  345. }
  346. }
  347. for (std::size_t index = 0; index < left.inventory.count; ++index) {
  348. if (!same_stationary_item(left.inventory.values[index], right.inventory.values[index])) {
  349. return false;
  350. }
  351. }
  352. return true;
  353. }
  354. /** Exact character comparison, including the canonical unused inventory tail. */
  355. [[nodiscard]] bool same_character(const CharacterState& left,
  356. const CharacterState& right) noexcept {
  357. if (!same_loadout(left, right)) {
  358. return false;
  359. }
  360. for (std::size_t index = left.inventory.count; index < left.inventory.values.size(); ++index) {
  361. if (!same_stationary_item(left.inventory.values[index], right.inventory.values[index])) {
  362. return false;
  363. }
  364. }
  365. return true;
  366. }
  367. /** Finds one item SOID exactly once in the character's equipment and dense inventory. */
  368. [[nodiscard]] bool find_character_item_location(const CharacterState& character,
  369. std::uint64_t instanceSoid,
  370. CharacterItemLocation& location) noexcept {
  371. location = {};
  372. bool found = false;
  373. for (std::size_t index = 0; index < character.equipment.slots.size(); ++index) {
  374. const auto& item = character.equipment.slots[index];
  375. if (!item.has_value() || item->instanceSoid != instanceSoid) {
  376. continue;
  377. }
  378. if (found) {
  379. return false;
  380. }
  381. found = true;
  382. location = {index, true};
  383. }
  384. for (std::size_t index = 0; index < character.inventory.count; ++index) {
  385. if (character.inventory.values[index].instanceSoid != instanceSoid) {
  386. continue;
  387. }
  388. if (found) {
  389. return false;
  390. }
  391. found = true;
  392. location = {index, false};
  393. }
  394. return found;
  395. }
  396. /** Borrows an item at a previously validated authored location. */
  397. [[nodiscard]] const authored_inventory::Item*
  398. character_item_at(const CharacterState& character, const CharacterItemLocation& location) noexcept {
  399. if (location.equipped) {
  400. if (location.index >= character.equipment.slots.size()
  401. || !character.equipment.slots[location.index].has_value()) {
  402. return nullptr;
  403. }
  404. return &*character.equipment.slots[location.index];
  405. }
  406. if (location.index >= character.inventory.count) {
  407. return nullptr;
  408. }
  409. return &character.inventory.values[location.index];
  410. }
  411. /** Borrows a mutable item at a previously validated authored location. */
  412. [[nodiscard]] authored_inventory::Item*
  413. character_item_at(CharacterState& character, const CharacterItemLocation& location) noexcept {
  414. if (location.equipped) {
  415. if (location.index >= character.equipment.slots.size()
  416. || !character.equipment.slots[location.index].has_value()) {
  417. return nullptr;
  418. }
  419. return &*character.equipment.slots[location.index];
  420. }
  421. if (location.index >= character.inventory.count) {
  422. return nullptr;
  423. }
  424. return &character.inventory.values[location.index];
  425. }
  426. } // namespace runtime::detail
  427. } // namespace sunrise::state