state_account_socket_runtime.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. /** Socket-plug and item-state staging, which both mutate one character-owned item. */
  2. #include <Windows.h>
  3. #include <algorithm>
  4. #include <array>
  5. #include <cstddef>
  6. #include <cstdint>
  7. #include <cstdio>
  8. #include <limits>
  9. #include <string_view>
  10. #include <utility>
  11. #include "../../core/logging/log.h"
  12. #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
  13. #include "../build_data/runtime.h"
  14. #include "runtime.h"
  15. #include "state.h"
  16. #include "state_account_transaction_helpers.h"
  17. #include "state_rolled_socket_plugs.h"
  18. #include "storage/internal.h"
  19. namespace sunrise::state {
  20. namespace runtime::detail {
  21. namespace authored_inventory = account::inventory;
  22. namespace item_details = build_data::items::details;
  23. namespace inventory_buckets = build_data::inventory::buckets;
  24. namespace family4_loadout = middleware::datagen::family4::loadout;
  25. /** Writes one bounded opcode-903 socket-selection transaction checkpoint. */
  26. void report_socket_plug(std::string_view stage,
  27. std::string_view result,
  28. std::string_view reason,
  29. std::uint64_t characterSoid,
  30. std::uint64_t targetInstanceSoid,
  31. std::uint16_t targetDefinitionIndex,
  32. std::uint8_t socketLane,
  33. std::uint16_t plugDefinitionIndex,
  34. std::uint8_t targetBucketId,
  35. std::uint8_t plugBucketId,
  36. bool targetEquipped,
  37. std::size_t itemIndex) noexcept {
  38. std::array<char, core::log::kLineCapacity> line{};
  39. const int count = std::snprintf(
  40. line.data(),
  41. line.size(),
  42. "ev=socket_plug stage=%.*s result=%.*s reason=%.*s character=0x%llX "
  43. "instance=0x%llX target_definition=%u target_bucket=%u lane=%u plug_definition=%u "
  44. "plug_bucket=%u equipped=%u item_index=%zu",
  45. static_cast<int>(stage.size()),
  46. stage.data(),
  47. static_cast<int>(result.size()),
  48. result.data(),
  49. static_cast<int>(reason.size()),
  50. reason.data(),
  51. static_cast<unsigned long long>(characterSoid),
  52. static_cast<unsigned long long>(targetInstanceSoid),
  53. static_cast<unsigned>(targetDefinitionIndex),
  54. static_cast<unsigned>(targetBucketId),
  55. static_cast<unsigned>(socketLane),
  56. static_cast<unsigned>(plugDefinitionIndex),
  57. static_cast<unsigned>(plugBucketId),
  58. static_cast<unsigned>(targetEquipped),
  59. itemIndex);
  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. /** Materializes native initial plugs as a complete authored socket block. */
  67. [[nodiscard]] bool materialize_native_sockets(const item_details::Definition& detail,
  68. authored_inventory::Sockets& sockets) noexcept {
  69. sockets = {};
  70. if (detail.ordinarySocketState != item_details::OrdinarySocketState::present
  71. || detail.ordinarySocketCount > sockets.plugs.size()) {
  72. return false;
  73. }
  74. sockets.policy = authored_inventory::SocketPolicy::authored;
  75. sockets.plugCount = detail.ordinarySocketCount;
  76. for (std::size_t lane = 0; lane < sockets.plugCount; ++lane) {
  77. const std::uint16_t plugIndex = detail.initialPlugIndices[lane];
  78. if (plugIndex == item_details::kUnavailableItemIndex) {
  79. continue;
  80. }
  81. build_data::items::Definition plug{};
  82. if (!build_data::find_item_definition_index(plugIndex, plug)
  83. || plug.definitionIndex != plugIndex
  84. || plug.definitionHash == authored_inventory::kNoDefinitionHash) {
  85. return false;
  86. }
  87. sockets.plugs[lane] = plug.definitionHash;
  88. }
  89. return authored_inventory::valid(sockets);
  90. }
  91. /** Stages the canonical socket-only after-image over one already validated account snapshot. */
  92. [[nodiscard]] bool stage_socket_plug(const AccountState& snapshot,
  93. std::size_t characterIndex,
  94. std::uint64_t targetInstanceSoid,
  95. std::uint8_t socketLane,
  96. std::uint16_t plugDefinitionIndex,
  97. PendingSocketPlug& mutation,
  98. std::uint32_t pinnedPlugHash) noexcept {
  99. mutation = {};
  100. CharacterItemLocation location{};
  101. build_data::items::Definition targetDefinition{};
  102. build_data::items::Definition plugDefinition{};
  103. const auto fail = [&](std::string_view reason) noexcept {
  104. const std::uint64_t characterSoid =
  105. characterIndex < snapshot.characterCount ? snapshot.characters[characterIndex].soid : 0;
  106. report_socket_plug("stage_internal",
  107. "fail",
  108. reason,
  109. characterSoid,
  110. targetInstanceSoid,
  111. targetDefinition.definitionIndex,
  112. socketLane,
  113. plugDefinitionIndex,
  114. targetDefinition.bucketId,
  115. plugDefinition.bucketId,
  116. location.equipped,
  117. location.index);
  118. mutation = {};
  119. return false;
  120. };
  121. if (!account::valid(snapshot) || characterIndex >= snapshot.characterCount
  122. || targetInstanceSoid == 0 || socketLane >= authored_inventory::kPlugCapacity) {
  123. return fail("request_or_account");
  124. }
  125. const CharacterState& before = snapshot.characters[characterIndex];
  126. if (!before.selected || before.soid == 0) {
  127. return fail("selected_character");
  128. }
  129. family4_loadout::ResolvedLoadout beforeLoadout{};
  130. if (!find_character_item_location(before, targetInstanceSoid, location)
  131. || !family4_loadout::resolve(snapshot, characterIndex, beforeLoadout)) {
  132. return fail("target_or_before_loadout");
  133. }
  134. const authored_inventory::Item* target = character_item_at(before, location);
  135. item_details::Definition detail{};
  136. if (target == nullptr
  137. || !build_data::find_item_definition_hash(target->definitionHash, targetDefinition)
  138. || targetDefinition.definitionHash != target->definitionHash
  139. || !build_data::find_configured_item_detail(targetDefinition.definitionIndex, detail)
  140. || detail.definitionIndex != targetDefinition.definitionIndex
  141. || detail.definitionHash != targetDefinition.definitionHash
  142. || detail.bucketId != targetDefinition.bucketId
  143. || detail.ordinarySocketState != item_details::OrdinarySocketState::present
  144. || socketLane >= detail.ordinarySocketCount
  145. || detail.ordinarySocketCount > authored_inventory::kPlugCapacity
  146. || !build_data::find_item_definition_index(plugDefinitionIndex, plugDefinition)
  147. || plugDefinition.definitionIndex != plugDefinitionIndex
  148. || plugDefinition.definitionHash == authored_inventory::kNoDefinitionHash
  149. || !build_data::is_socket_plug_allowed(
  150. targetDefinition.definitionIndex, socketLane, plugDefinitionIndex)) {
  151. return fail("definition_or_compatibility");
  152. }
  153. if (build_data::is_exotic_catalyst_lane(targetDefinition.definitionIndex, socketLane)) {
  154. return fail("catalyst_lane_requires_atomic_change");
  155. }
  156. // Ownership only matters where the plug is a finite supply the account draws down. A shader is
  157. // one: pulled from Collections into a profile stack and spent on apply. An ornament is a
  158. // permanent unlock, so requiring a stack for one would refuse a plug the account already has.
  159. const bool consumesStack =
  160. build_data::is_profile_action_source(plugDefinitionIndex, plugDefinition.bucketId)
  161. && build_data::is_consumed_on_apply(plugDefinitionIndex, plugDefinition.bucketId)
  162. && !(socketLane < detail.initialPlugIndices.size()
  163. && detail.initialPlugIndices[socketLane] == plugDefinitionIndex);
  164. if (consumesStack && !holds_plug_source(snapshot, plugDefinition.definitionHash)) {
  165. return fail("plug_ownership");
  166. }
  167. AccountState chargedAccount = snapshot;
  168. build_data::material_requirements::Definition materialSet{};
  169. bool profileChanged = false;
  170. const std::uint16_t materialSetIndex = plugDefinition.insertionMaterialRequirementSetIndex;
  171. if (materialSetIndex != build_data::items::kUnavailableMaterialRequirementSetIndex
  172. && (!build_data::find_material_requirement_set(materialSetIndex, materialSet)
  173. || materialSet.requirementSetIndex != materialSetIndex
  174. || !apply_action_materials(snapshot, materialSet, chargedAccount, profileChanged))) {
  175. return fail("materials");
  176. }
  177. // Applying spends the stack the plug came from; the insertion cost above is a separate charge.
  178. // The authored-cost path refuses any row carrying an instance key, and an action source always
  179. // has one, so the row keeps its identity until the last unit goes and is released with it.
  180. if (consumesStack && !spend_plug_source(chargedAccount, plugDefinition.definitionHash)) {
  181. return fail("plug_stack");
  182. }
  183. profileChanged = profileChanged || consumesStack;
  184. authored_inventory::Sockets authoredSockets{};
  185. if (target->sockets.policy == authored_inventory::SocketPolicy::nativeDefaults) {
  186. if (!materialize_native_sockets(detail, authoredSockets)) {
  187. return fail("native_sockets");
  188. }
  189. } else {
  190. authoredSockets = target->sockets;
  191. if (authoredSockets.policy != authored_inventory::SocketPolicy::authored
  192. || authoredSockets.plugCount != detail.ordinarySocketCount
  193. || !authored_inventory::valid(authoredSockets)) {
  194. return fail("authored_sockets");
  195. }
  196. }
  197. if (authoredSockets.plugs[socketLane].has_value()
  198. && *authoredSockets.plugs[socketLane] == plugDefinition.definitionHash) {
  199. return fail("already_applied");
  200. }
  201. // A rolled socket's apply or re-roll plug is an action, not a plug: the service answered it
  202. // by socketing a result plug from the socket's roll set. The requested plug still decides the
  203. // pool check and the material charge above; only the plug that lands in the lane changes.
  204. build_data::items::Definition grantedDefinition = plugDefinition;
  205. if (classify_rolled_plug(plugDefinition, targetDefinition, socketLane)
  206. == RolledPlugAction::roll) {
  207. const std::uint32_t currentPlugHash = authoredSockets.plugs[socketLane].value_or(0);
  208. const bool reroll = is_rolled_result(currentPlugHash);
  209. // A re-staging must land on the plug the first staging rolled, so the pinned roll is
  210. // taken as long as it is still one the fresh roll could have produced.
  211. RolledPlug rolled{};
  212. if (pinnedPlugHash != 0) {
  213. if (pinnedPlugHash == currentPlugHash
  214. || !pin_rolled_plug(pinnedPlugHash, detail, rolled)) {
  215. return fail("rolled_plug_pin");
  216. }
  217. } else {
  218. const std::uint64_t seed = targetInstanceSoid
  219. ^ (static_cast<std::uint64_t>(GetTickCount64()) << 8U)
  220. ^ static_cast<std::uint64_t>(before.nextInventorySerial);
  221. if (!roll_socket_plug(plugDefinition,
  222. detail,
  223. static_cast<std::uint8_t>(before.characterClass),
  224. currentPlugHash,
  225. seed,
  226. rolled)) {
  227. return fail("rolled_plug_roll");
  228. }
  229. }
  230. if (!build_data::find_item_definition_hash(rolled.plugHash, grantedDefinition)
  231. || grantedDefinition.definitionHash != rolled.plugHash) {
  232. return fail("rolled_plug_roll");
  233. }
  234. // A result that stands for another plug re-rolls that plug's lane as well: the service
  235. // swapped the piece's stat perk to the one the result names, which is what moves the
  236. // stats.
  237. if (rolled.linkedPerkHash != 0) {
  238. build_data::items::Definition linkedDefinition{};
  239. if (rolled.linkedLane == socketLane || rolled.linkedLane >= detail.ordinarySocketCount
  240. || !build_data::find_item_definition_hash(rolled.linkedPerkHash, linkedDefinition)
  241. || linkedDefinition.definitionHash != rolled.linkedPerkHash) {
  242. return fail("rolled_plug_link");
  243. }
  244. authoredSockets.plugs[rolled.linkedLane] = rolled.linkedPerkHash;
  245. report_socket_plug("rolled_plug_link",
  246. "ok",
  247. reroll ? "reroll" : "apply",
  248. before.soid,
  249. targetInstanceSoid,
  250. targetDefinition.definitionIndex,
  251. rolled.linkedLane,
  252. linkedDefinition.definitionIndex,
  253. targetDefinition.bucketId,
  254. linkedDefinition.bucketId,
  255. location.equipped,
  256. location.index);
  257. }
  258. report_socket_plug("rolled_plug",
  259. "ok",
  260. reroll ? "reroll" : "apply",
  261. before.soid,
  262. targetInstanceSoid,
  263. targetDefinition.definitionIndex,
  264. socketLane,
  265. grantedDefinition.definitionIndex,
  266. targetDefinition.bucketId,
  267. grantedDefinition.bucketId,
  268. location.equipped,
  269. location.index);
  270. }
  271. authoredSockets.plugs[socketLane] = grantedDefinition.definitionHash;
  272. CharacterState after = before;
  273. authored_inventory::Item* changed = character_item_at(after, location);
  274. if (changed == nullptr || changed->instanceSoid != target->instanceSoid
  275. || changed->definitionHash != target->definitionHash || changed->level != target->level
  276. || changed->quantity != target->quantity
  277. || changed->mutationSerial != target->mutationSerial) {
  278. return fail("target_copy");
  279. }
  280. changed->sockets = authoredSockets;
  281. AccountState candidate = chargedAccount;
  282. candidate.characters[characterIndex] = after;
  283. family4_loadout::ResolvedLoadout afterLoadout{};
  284. ResolvedPosition beforePosition{};
  285. ResolvedPosition afterPosition{};
  286. const family4_loadout::ResolvedItem* resolvedTarget = nullptr;
  287. for (std::size_t index = 0; index < beforeLoadout.itemCount; ++index) {
  288. const auto& resolved = beforeLoadout.items[index];
  289. if (resolved.instance.instanceSoid == targetInstanceSoid
  290. && resolved.instance.baseDefinitionIndex != targetDefinition.definitionIndex) {
  291. return fail("before_definition");
  292. }
  293. }
  294. if (!account::valid(candidate)
  295. || !family4_loadout::resolve(candidate, characterIndex, afterLoadout)
  296. || !find_resolved_position(beforeLoadout, targetInstanceSoid, beforePosition)
  297. || !find_resolved_position(afterLoadout, targetInstanceSoid, afterPosition)
  298. || !same_position(beforePosition, afterPosition)) {
  299. return fail("candidate_or_position");
  300. }
  301. for (std::size_t index = 0; index < afterLoadout.itemCount; ++index) {
  302. const auto& resolved = afterLoadout.items[index];
  303. if (resolved.instance.instanceSoid != targetInstanceSoid) {
  304. continue;
  305. }
  306. if (resolvedTarget != nullptr) {
  307. return fail("duplicate_target");
  308. }
  309. resolvedTarget = &resolved;
  310. }
  311. if (resolvedTarget == nullptr
  312. || resolvedTarget->instance.baseDefinitionIndex != targetDefinition.definitionIndex
  313. || resolvedTarget->instance.ordinarySockets.state
  314. != middleware::datagen::family4::instance::OrdinarySocketBlockState::present
  315. || !resolvedTarget->instance.ordinarySockets.plugs[socketLane].has_value()
  316. || *resolvedTarget->instance.ordinarySockets.plugs[socketLane]
  317. != grantedDefinition.definitionIndex) {
  318. return fail("after_socket");
  319. }
  320. mutation.beforeCharacter = before;
  321. mutation.afterCharacter = after;
  322. mutation.beforeProfileItems = snapshot.profileItems;
  323. mutation.afterProfileItems = chargedAccount.profileItems;
  324. mutation.accountSoid = snapshot.primarySoid;
  325. mutation.characterSoid = before.soid;
  326. mutation.targetInstanceSoid = targetInstanceSoid;
  327. mutation.targetDefinitionHash = targetDefinition.definitionHash;
  328. mutation.plugDefinitionHash = grantedDefinition.definitionHash;
  329. mutation.materialRequirementSetHash = materialSet.requirementSetHash;
  330. mutation.characterIndex = characterIndex;
  331. mutation.expectedProfileItemCount = snapshot.profileItemCount;
  332. mutation.afterProfileItemCount = chargedAccount.profileItemCount;
  333. mutation.itemIndex = location.index;
  334. mutation.targetDefinitionIndex = targetDefinition.definitionIndex;
  335. mutation.plugDefinitionIndex = grantedDefinition.definitionIndex;
  336. mutation.requestedPlugDefinitionIndex = plugDefinitionIndex;
  337. mutation.materialRequirementSetIndex = materialSetIndex;
  338. mutation.socketLane = socketLane;
  339. mutation.targetBucketId = targetDefinition.bucketId;
  340. mutation.plugBucketId = grantedDefinition.bucketId;
  341. mutation.materialRequirementCount = materialSet.requirementCount;
  342. mutation.profileChanged = profileChanged;
  343. mutation.targetEquipped = location.equipped;
  344. mutation.prepared = true;
  345. return true;
  346. }
  347. /** Stages one complete accumulated item-state value without moving or recreating the item. */
  348. [[nodiscard]] bool stage_item_state(const AccountState& snapshot,
  349. std::size_t characterIndex,
  350. std::uint64_t targetInstanceSoid,
  351. std::uint16_t targetDefinitionIndex,
  352. std::uint32_t flags,
  353. PendingItemState& mutation) noexcept {
  354. mutation = {};
  355. if (!account::valid(snapshot) || characterIndex >= snapshot.characterCount
  356. || targetInstanceSoid == 0 || !authored_inventory::valid_item_state(flags)) {
  357. return false;
  358. }
  359. const CharacterState& before = snapshot.characters[characterIndex];
  360. if (!before.selected || before.soid == 0) {
  361. return false;
  362. }
  363. CharacterItemLocation location{};
  364. family4_loadout::ResolvedLoadout beforeLoadout{};
  365. if (!find_character_item_location(before, targetInstanceSoid, location)
  366. || !family4_loadout::resolve(snapshot, characterIndex, beforeLoadout)) {
  367. return false;
  368. }
  369. const authored_inventory::Item* target = character_item_at(before, location);
  370. build_data::items::Definition definition{};
  371. ResolvedPosition beforePosition{};
  372. if (target == nullptr || target->flags == flags
  373. || !build_data::find_item_definition_hash(target->definitionHash, definition)
  374. || definition.definitionHash != target->definitionHash
  375. || definition.definitionIndex != targetDefinitionIndex
  376. || !find_resolved_position(beforeLoadout, targetInstanceSoid, beforePosition)) {
  377. return false;
  378. }
  379. CharacterState after = before;
  380. authored_inventory::Item* changed = character_item_at(after, location);
  381. if (changed == nullptr || !same_stationary_item(*changed, *target)) {
  382. return false;
  383. }
  384. changed->flags = flags;
  385. AccountState candidate = snapshot;
  386. candidate.characters[characterIndex] = after;
  387. family4_loadout::ResolvedLoadout afterLoadout{};
  388. ResolvedPosition afterPosition{};
  389. if (!account::valid(candidate)
  390. || !family4_loadout::resolve(candidate, characterIndex, afterLoadout)
  391. || !find_resolved_position(afterLoadout, targetInstanceSoid, afterPosition)
  392. || !same_position(beforePosition, afterPosition)) {
  393. return false;
  394. }
  395. mutation.beforeCharacter = before;
  396. mutation.afterCharacter = after;
  397. mutation.characterSoid = before.soid;
  398. mutation.targetInstanceSoid = targetInstanceSoid;
  399. mutation.characterIndex = characterIndex;
  400. mutation.itemIndex = location.index;
  401. mutation.targetDefinitionIndex = targetDefinitionIndex;
  402. mutation.beforeFlags = target->flags;
  403. mutation.afterFlags = flags;
  404. mutation.targetEquipped = location.equipped;
  405. mutation.prepared = true;
  406. return true;
  407. }
  408. } // namespace runtime::detail
  409. } // namespace sunrise::state