family4_selection_move.cpp 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046
  1. #include <algorithm>
  2. #include <array>
  3. #include <cstddef>
  4. #include <cstdio>
  5. #include <limits>
  6. #include <optional>
  7. #include <span>
  8. #include "../../../../../core/logging/log.h"
  9. #include "../../../../../middleware/datagen/family4/account/account_encoder.h"
  10. #include "../../../../../middleware/datagen/family4/account/layout.h"
  11. #include "../../../../../middleware/datagen/family4/account/selection_patch/\
  12. account_selection_patch_encoder.h"
  13. #include "../../../../../middleware/datagen/family4/character/character_encoder.h"
  14. #include "../../../../../middleware/datagen/family4/character/layout.h"
  15. #include "../../../../../middleware/datagen/family4/instance/instance_encoder.h"
  16. #include "../../../../../middleware/datagen/family4/instance/layout.h"
  17. #include "../../../../../state/runtime/runtime.h"
  18. #include "internal.h"
  19. #include "snapshot_storage.h"
  20. namespace sunrise::server::bap::encrypted::push::snapshot {
  21. namespace {
  22. namespace family4_datagen = middleware::datagen::family4;
  23. namespace selection_patch = middleware::datagen::family4::account::selection_patch;
  24. /** Logs the exact resolved and encoded fields used by the opcode-403 character upsert. */
  25. void report_equipment_object(const queuez::EquipmentSwap& swap,
  26. const state::PendingEquipmentSwap& mutation,
  27. const Resolved& selected,
  28. const family4_datagen::character::layout::Object& object) noexcept {
  29. constexpr std::size_t kMissing = (std::numeric_limits<std::size_t>::max)();
  30. std::size_t requestedRow = kMissing;
  31. std::size_t previousRow = kMissing;
  32. std::size_t nativeSlot = kMissing;
  33. bool requestedEquipped = false;
  34. bool previousEquipped = false;
  35. for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
  36. const auto& item = selected.loadout.items[index];
  37. if (item.instance.instanceSoid == mutation.requestedInstanceSoid) {
  38. requestedRow = item.inventoryRow;
  39. nativeSlot = item.equipmentSlot;
  40. requestedEquipped = item.equipped;
  41. } else if (item.instance.instanceSoid == mutation.previousInstanceSoid) {
  42. previousRow = item.inventoryRow;
  43. previousEquipped = item.equipped;
  44. }
  45. }
  46. const std::uint64_t equippedSoid = nativeSlot < object.equippedInstanceSoids.size()
  47. ? object.equippedInstanceSoids[nativeSlot]
  48. : 0;
  49. const std::uint64_t requestedRowSoid = requestedRow < object.inventoryItems.size()
  50. ? object.inventoryItems[requestedRow].instanceSoid
  51. : 0;
  52. const std::uint64_t previousRowSoid = previousRow < object.inventoryItems.size()
  53. ? object.inventoryItems[previousRow].instanceSoid
  54. : 0;
  55. const std::int32_t requestedSerial = requestedRow < object.inventoryItems.size()
  56. ? object.inventoryItems[requestedRow].mutationSerial
  57. : -1;
  58. const std::int32_t previousSerial = previousRow < object.inventoryItems.size()
  59. ? object.inventoryItems[previousRow].mutationSerial
  60. : -1;
  61. std::array<char, core::log::kLineCapacity> line{};
  62. const int count = std::snprintf(
  63. line.data(),
  64. line.size(),
  65. "ev=equip stage=character_object result=ok family_version=%d root=0x%llX definition=%u "
  66. "character=0x%llX items=%zu next_serial=%u requested=0x%llX requested_row=%zu "
  67. "requested_row_soid=0x%llX requested_equipped=%u native_slot=%zu equipped_soid=0x%llX "
  68. "requested_serial=%d previous=0x%llX previous_row=%zu previous_row_soid=0x%llX "
  69. "previous_equipped=%u previous_serial=%d",
  70. swap.after.family4Version,
  71. static_cast<unsigned long long>(swap.after.family4RootSoid),
  72. swap.characterDefinitionId,
  73. static_cast<unsigned long long>(mutation.characterSoid),
  74. selected.loadout.itemCount,
  75. object.nextInventorySerial,
  76. static_cast<unsigned long long>(mutation.requestedInstanceSoid),
  77. requestedRow,
  78. static_cast<unsigned long long>(requestedRowSoid),
  79. static_cast<unsigned>(requestedEquipped),
  80. nativeSlot,
  81. static_cast<unsigned long long>(equippedSoid),
  82. requestedSerial,
  83. static_cast<unsigned long long>(mutation.previousInstanceSoid),
  84. previousRow,
  85. static_cast<unsigned long long>(previousRowSoid),
  86. static_cast<unsigned>(previousEquipped),
  87. previousSerial);
  88. if (count > 0) {
  89. core::log::write(core::log::Channel::server,
  90. core::log::Level::debug,
  91. {line.data(), static_cast<std::size_t>(count)});
  92. }
  93. }
  94. } // namespace
  95. /** Builds the Family-4 increment that moves the character object to the picked character. */
  96. bool prepare_selection_move(Scratch& scratch,
  97. const queuez::SelectCharacter& select,
  98. Prepared& prepared) noexcept {
  99. const Reservation reservation = reserve_prior(scratch, prepared);
  100. if (reservation.rawWriteOffset > scratch.plaintext.size()
  101. || reservation.compressedWriteOffset > scratch.sealed.size()) {
  102. return report_failure("move_reservation");
  103. }
  104. const state::AccountState account = state::account_snapshot();
  105. const std::optional<std::size_t> selectedIndex = find_character_index(account);
  106. Resolved selected{};
  107. if (!state::account::valid(account) || !selectedIndex.has_value()
  108. || !resolve(account, *selectedIndex, selected)
  109. || account.characters[selected.characterIndex].soid != select.selectedCharacterSoid) {
  110. return report_failure("move_selection");
  111. }
  112. Prepared staged{};
  113. staged.rawClearSize = reservation.rawClearSize;
  114. staged.compressedClearSize = reservation.compressedClearSize;
  115. const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  116. std::size_t compressedExtent = reservation.compressedWriteOffset;
  117. std::size_t objectCount = 0;
  118. // The first select resends the character object even when the pick names the current one. With
  119. // no pending change all three operations go out. Only a pending change may skip it, and only
  120. // when the character did not move.
  121. if (!select.patchAccount || select.previousCharacterSoid != select.selectedCharacterSoid) {
  122. // A zero previous key means the snapshot published no character object. There is no slot
  123. // to release, so the pick adds one. Releasing a key the Client never held raises queuez
  124. // error 4.
  125. if (select.previousCharacterSoid != 0) {
  126. // The release carries no payload. The encoding field is never read for an empty
  127. // object, so it just matches its neighbours.
  128. staged.objects[objectCount++] = middleware::queuez::Object{
  129. select.characterDefinitionId,
  130. select.previousCharacterSoid,
  131. middleware::queuez::Encoding::oodle,
  132. {},
  133. };
  134. }
  135. if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
  136. return report_failure("move_character_storage");
  137. }
  138. const auto characterBytes =
  139. rawStorage.first(family4_datagen::character::layout::kObjectSize);
  140. if (!family4_datagen::character::encode(account.characters[selected.characterIndex],
  141. selected.loadout,
  142. selected.lightEvaluation,
  143. characterBytes)) {
  144. return report_failure("move_character_object");
  145. }
  146. if (!append_object(scratch,
  147. characterBytes,
  148. select.characterDefinitionId,
  149. select.selectedCharacterSoid,
  150. staged.objects[objectCount++],
  151. compressedExtent)) {
  152. return report_failure("move_character_object");
  153. }
  154. staged.rawClearSize = (std::max)(staged.rawClearSize,
  155. reservation.rawWriteOffset
  156. + family4_datagen::character::layout::kObjectSize);
  157. }
  158. // The character object is already compressed into sealed storage, so the raw span is free
  159. // for whichever account body this move carries.
  160. if (select.patchAccount) {
  161. std::size_t patchSize = 0;
  162. if (!selection_patch::encode(select.selectedCharacterSoid, rawStorage, patchSize)
  163. || patchSize != selection_patch::kPayloadSize) {
  164. return report_failure("move_account_patch");
  165. }
  166. staged.objects[objectCount++] = middleware::queuez::Object{
  167. select.accountDefinitionId,
  168. select.after.family4RootSoid,
  169. middleware::queuez::Encoding::tagReflection,
  170. rawStorage.first(patchSize),
  171. };
  172. staged.rawClearSize =
  173. (std::max)(staged.rawClearSize, reservation.rawWriteOffset + patchSize);
  174. } else {
  175. if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
  176. return report_failure("move_account_storage");
  177. }
  178. const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
  179. if (!family4_datagen::account::encode(account, accountBytes)) {
  180. return report_failure("move_account_object");
  181. }
  182. if (!append_object(scratch,
  183. accountBytes,
  184. select.accountDefinitionId,
  185. select.after.family4RootSoid,
  186. staged.objects[objectCount++],
  187. compressedExtent)) {
  188. return report_failure("move_account_object");
  189. }
  190. staged.rawClearSize =
  191. (std::max)(staged.rawClearSize,
  192. reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
  193. }
  194. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  195. staged.family = middleware::queuez::Family{
  196. kAccountFamilyType,
  197. select.after.family4RootSoid,
  198. select.after.family4Version,
  199. 0,
  200. std::span(staged.objects).first(objectCount),
  201. };
  202. if (!commit(staged, prepared)) {
  203. clear_after(scratch, reservation);
  204. return report_failure("move_commit");
  205. }
  206. return true;
  207. }
  208. /** Builds a single-character Family-4 upsert from an uncommitted equipment after-image. */
  209. bool prepare_equipment_swap(Scratch& scratch,
  210. const queuez::EquipmentSwap& swap,
  211. const state::PendingEquipmentSwap& mutation,
  212. Prepared& prepared) noexcept {
  213. const Reservation reservation = reserve_prior(scratch, prepared);
  214. if (reservation.rawWriteOffset > scratch.plaintext.size()
  215. || reservation.compressedWriteOffset > scratch.sealed.size()) {
  216. return report_failure("equip_reservation");
  217. }
  218. state::AccountState account = state::account_snapshot();
  219. if (!mutation.prepared || mutation.characterSoid == 0
  220. || mutation.characterSoid != swap.characterSoid
  221. || mutation.characterIndex >= account.characterCount
  222. || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
  223. return report_failure("equip_mutation");
  224. }
  225. account.characters[mutation.characterIndex] = mutation.afterCharacter;
  226. Resolved selected{};
  227. const std::optional<std::size_t> selectedIndex = find_character_index(account);
  228. if (!state::account::valid(account) || !selectedIndex.has_value()
  229. || *selectedIndex != mutation.characterIndex
  230. || !resolve(account, mutation.characterIndex, selected)
  231. || selected.characterObjectId != swap.characterDefinitionId) {
  232. return report_failure("equip_selection");
  233. }
  234. const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  235. if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
  236. return report_failure("equip_character_storage");
  237. }
  238. const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
  239. if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
  240. selected.loadout,
  241. selected.lightEvaluation,
  242. characterBytes)) {
  243. return report_failure("equip_character_object");
  244. }
  245. report_equipment_object(swap,
  246. mutation,
  247. selected,
  248. *reinterpret_cast<const family4_datagen::character::layout::Object*>(
  249. characterBytes.data()));
  250. Prepared staged{};
  251. staged.rawClearSize =
  252. (std::max)(reservation.rawClearSize,
  253. reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
  254. std::size_t compressedExtent = reservation.compressedWriteOffset;
  255. if (!append_object(scratch,
  256. characterBytes,
  257. swap.characterDefinitionId,
  258. swap.characterSoid,
  259. staged.objects.front(),
  260. compressedExtent)) {
  261. return report_failure("equip_character_object");
  262. }
  263. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  264. staged.family = middleware::queuez::Family{
  265. kAccountFamilyType,
  266. swap.after.family4RootSoid,
  267. swap.after.family4Version,
  268. 0,
  269. std::span(staged.objects).first(1),
  270. };
  271. if (!commit(staged, prepared)) {
  272. clear_after(scratch, reservation);
  273. return report_failure("equip_commit");
  274. }
  275. return true;
  276. }
  277. /** Builds a single-character Family-4 upsert from an uncommitted item-state after-image. */
  278. bool prepare_item_state(Scratch& scratch,
  279. const queuez::EquipmentSwap& update,
  280. const state::PendingItemState& mutation,
  281. Prepared& prepared) noexcept {
  282. const Reservation reservation = reserve_prior(scratch, prepared);
  283. if (reservation.rawWriteOffset > scratch.plaintext.size()
  284. || reservation.compressedWriteOffset > scratch.sealed.size()) {
  285. return report_failure("item_state_reservation");
  286. }
  287. state::AccountState account = state::account_snapshot();
  288. if (!mutation.prepared || mutation.characterSoid == 0
  289. || mutation.characterSoid != update.characterSoid
  290. || mutation.characterIndex >= account.characterCount
  291. || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
  292. return report_failure("item_state_mutation");
  293. }
  294. account.characters[mutation.characterIndex] = mutation.afterCharacter;
  295. Resolved selected{};
  296. const std::optional<std::size_t> selectedIndex = find_character_index(account);
  297. if (!state::account::valid(account) || !selectedIndex.has_value()
  298. || *selectedIndex != mutation.characterIndex
  299. || !resolve(account, mutation.characterIndex, selected)
  300. || selected.characterObjectId != update.characterDefinitionId) {
  301. return report_failure("item_state_selection");
  302. }
  303. const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  304. if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
  305. return report_failure("item_state_character_storage");
  306. }
  307. const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
  308. if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
  309. selected.loadout,
  310. selected.lightEvaluation,
  311. characterBytes)) {
  312. return report_failure("item_state_character_object");
  313. }
  314. const auto& encoded =
  315. *reinterpret_cast<const family4_datagen::character::layout::Object*>(characterBytes.data());
  316. if (mutation.itemIndex >= mutation.afterCharacter.inventory.count && !mutation.targetEquipped) {
  317. return report_failure("item_state_inventory_index");
  318. }
  319. std::size_t matchingRows = 0;
  320. for (const auto& row : encoded.inventoryItems) {
  321. matchingRows +=
  322. static_cast<std::size_t>(row.instanceSoid == mutation.targetInstanceSoid
  323. && row.definitionIndex == mutation.targetDefinitionIndex
  324. && row.flags == mutation.afterFlags);
  325. }
  326. if (matchingRows != 1) {
  327. return report_failure("item_state_character_shape");
  328. }
  329. Prepared staged{};
  330. staged.rawClearSize =
  331. (std::max)(reservation.rawClearSize,
  332. reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
  333. std::size_t compressedExtent = reservation.compressedWriteOffset;
  334. if (!append_object(scratch,
  335. characterBytes,
  336. update.characterDefinitionId,
  337. update.characterSoid,
  338. staged.objects.front(),
  339. compressedExtent)) {
  340. return report_failure("item_state_character_object");
  341. }
  342. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  343. staged.family = middleware::queuez::Family{
  344. kAccountFamilyType,
  345. update.after.family4RootSoid,
  346. update.after.family4Version,
  347. 0,
  348. std::span(staged.objects).first(1),
  349. };
  350. if (!commit(staged, prepared)) {
  351. clear_after(scratch, reservation);
  352. return report_failure("item_state_commit");
  353. }
  354. return true;
  355. }
  356. /** Builds a resident item upsert followed by charged account balances when the cost consumes. */
  357. bool prepare_socket_plug(Scratch& scratch,
  358. const queuez::SocketPlug& socketPlug,
  359. const state::PendingSocketPlug& mutation,
  360. Prepared& prepared) noexcept {
  361. const Reservation reservation = reserve_prior(scratch, prepared);
  362. if (reservation.rawWriteOffset > scratch.plaintext.size()
  363. || reservation.compressedWriteOffset > scratch.sealed.size()) {
  364. return report_failure("socket_plug_reservation");
  365. }
  366. state::AccountState account{};
  367. if (!mutation.prepared || mutation.accountSoid == 0 || mutation.characterSoid == 0
  368. || mutation.targetInstanceSoid == 0 || mutation.accountSoid != socketPlug.accountSoid
  369. || mutation.characterSoid != socketPlug.characterSoid
  370. || mutation.targetInstanceSoid != socketPlug.targetInstanceSoid
  371. || mutation.profileChanged != socketPlug.updatesAccount
  372. || mutation.accountSoid != socketPlug.after.family4RootSoid
  373. || socketPlug.accountDefinitionId == 0 || !state::preview_socket_plug(mutation, account)
  374. || mutation.characterIndex >= account.characterCount
  375. || account.primarySoid != mutation.accountSoid
  376. || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
  377. return report_failure("socket_plug_mutation");
  378. }
  379. Resolved selected{};
  380. const std::optional<std::size_t> selectedIndex = find_character_index(account);
  381. if (!state::account::valid(account) || !selectedIndex.has_value()
  382. || *selectedIndex != mutation.characterIndex
  383. || !resolve(account, mutation.characterIndex, selected)
  384. || selected.itemInstanceObjectId != socketPlug.itemInstanceDefinitionId) {
  385. return report_failure("socket_plug_selection");
  386. }
  387. family4_datagen::loadout::ResolvedInstances changed{};
  388. for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
  389. const family4_datagen::loadout::ResolvedItem& item = selected.loadout.items[index];
  390. if (item.instance.instanceSoid != mutation.targetInstanceSoid) {
  391. continue;
  392. }
  393. if (changed.itemCount != 0
  394. || item.instance.baseDefinitionIndex != mutation.targetDefinitionIndex
  395. || mutation.socketLane >= item.instance.ordinarySockets.plugs.size()
  396. || item.instance.ordinarySockets.state
  397. != family4_datagen::instance::OrdinarySocketBlockState::present
  398. || !item.instance.ordinarySockets.plugs[mutation.socketLane].has_value()
  399. || *item.instance.ordinarySockets.plugs[mutation.socketLane]
  400. != mutation.plugDefinitionIndex) {
  401. return report_failure("socket_plug_item_shape");
  402. }
  403. changed.items[0] = {item.equipmentSlot, item.instance};
  404. changed.itemCount = 1;
  405. }
  406. if (changed.itemCount != 1) {
  407. return report_failure("socket_plug_item_missing");
  408. }
  409. const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  410. const std::size_t requiredRawSize = socketPlug.updatesAccount
  411. ? family4_datagen::account::layout::kObjectSize
  412. : family4_datagen::instance::layout::kObjectSize;
  413. if (requiredRawSize > rawStorage.size()) {
  414. return report_failure("socket_plug_item_storage");
  415. }
  416. Prepared staged{};
  417. staged.rawClearSize =
  418. (std::max)(reservation.rawClearSize,
  419. reservation.rawWriteOffset + family4_datagen::instance::layout::kObjectSize);
  420. std::size_t compressedExtent = reservation.compressedWriteOffset;
  421. std::size_t itemCursor = 0;
  422. if (!append_items(scratch,
  423. rawStorage,
  424. socketPlug.itemInstanceDefinitionId,
  425. changed,
  426. 0,
  427. staged,
  428. itemCursor,
  429. compressedExtent)
  430. || itemCursor != 1) {
  431. clear_after(scratch, reservation);
  432. return report_failure("socket_plug_item_object");
  433. }
  434. std::size_t objectCount = 1;
  435. if (socketPlug.updatesAccount) {
  436. const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
  437. if (!family4_datagen::account::encode(account, accountBytes)
  438. || !append_object(scratch,
  439. accountBytes,
  440. socketPlug.accountDefinitionId,
  441. socketPlug.accountSoid,
  442. staged.objects[1],
  443. compressedExtent)) {
  444. clear_after(scratch, reservation);
  445. return report_failure("socket_plug_account_object");
  446. }
  447. staged.rawClearSize =
  448. (std::max)(staged.rawClearSize,
  449. reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
  450. objectCount = 2;
  451. }
  452. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  453. staged.family = middleware::queuez::Family{
  454. kAccountFamilyType,
  455. socketPlug.after.family4RootSoid,
  456. socketPlug.after.family4Version,
  457. 0,
  458. std::span(staged.objects).first(objectCount),
  459. };
  460. if (!commit(staged, prepared)) {
  461. clear_after(scratch, reservation);
  462. return report_failure("socket_plug_commit");
  463. }
  464. std::array<char, core::log::kLineCapacity> line{};
  465. const int count = std::snprintf(
  466. line.data(),
  467. line.size(),
  468. "ev=socket_plug stage=family4_objects result=ok family_version=%d root=0x%llX "
  469. "character=0x%llX instance=0x%llX item_definition=%u target_definition=%u "
  470. "target_bucket=%u lane=%u plug_definition=%u plug_bucket=%u equipped=%u "
  471. "material_set=%u material_set_hash=%u material_rows=%u account_update=%u objects=%zu "
  472. "order=%s",
  473. socketPlug.after.family4Version,
  474. static_cast<unsigned long long>(socketPlug.after.family4RootSoid),
  475. static_cast<unsigned long long>(socketPlug.characterSoid),
  476. static_cast<unsigned long long>(socketPlug.targetInstanceSoid),
  477. socketPlug.itemInstanceDefinitionId,
  478. static_cast<unsigned>(mutation.targetDefinitionIndex),
  479. static_cast<unsigned>(mutation.targetBucketId),
  480. static_cast<unsigned>(mutation.socketLane),
  481. static_cast<unsigned>(mutation.plugDefinitionIndex),
  482. static_cast<unsigned>(mutation.plugBucketId),
  483. static_cast<unsigned>(mutation.targetEquipped),
  484. static_cast<unsigned>(mutation.materialRequirementSetIndex),
  485. mutation.materialRequirementSetHash,
  486. static_cast<unsigned>(mutation.materialRequirementCount),
  487. static_cast<unsigned>(socketPlug.updatesAccount),
  488. objectCount,
  489. socketPlug.updatesAccount ? "item_account" : "item");
  490. if (count > 0) {
  491. core::log::write(core::log::Channel::server,
  492. core::log::Level::debug,
  493. {line.data(), static_cast<std::size_t>(count)});
  494. }
  495. return true;
  496. }
  497. /** Builds a single full account-object upsert from an uncommitted profile-stack after-image. */
  498. bool prepare_profile_item_acquisition(Scratch& scratch,
  499. const queuez::ProfileItemAcquisition& acquisition,
  500. const state::PendingProfileItemAcquisition& mutation,
  501. Prepared& prepared) noexcept {
  502. const Reservation reservation = reserve_prior(scratch, prepared);
  503. if (reservation.rawWriteOffset > scratch.plaintext.size()
  504. || reservation.compressedWriteOffset > scratch.sealed.size()) {
  505. return report_failure("profile_acquire_reservation");
  506. }
  507. if (!mutation.prepared || mutation.accountSoid == 0
  508. || mutation.accountSoid != acquisition.accountSoid
  509. || mutation.acquiredInstanceSoid != acquisition.acquiredInstanceSoid
  510. || mutation.actionSource != acquisition.actionSource
  511. || acquisition.appendedResident != (mutation.appended && mutation.actionSource)
  512. || acquisition.accountSoid != acquisition.after.family4RootSoid
  513. || acquisition.accountDefinitionId == 0
  514. || (acquisition.appendedResident && acquisition.itemInstanceDefinitionId == 0)) {
  515. return report_failure("profile_acquire_mutation");
  516. }
  517. state::AccountState account{};
  518. if (!state::preview_profile_item_acquisition(mutation, account)
  519. || account.primarySoid != acquisition.accountSoid || !state::account::valid(account)) {
  520. return report_failure("profile_acquire_account");
  521. }
  522. const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  523. if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
  524. return report_failure("profile_acquire_account_storage");
  525. }
  526. const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
  527. if (!family4_datagen::account::encode(account, accountBytes)) {
  528. return report_failure("profile_acquire_account_encode");
  529. }
  530. // The native account observer compares profile quantities but only emits pickup feedback when
  531. // the changed row's mutation serial also appears in this transient 16-record bank at 0x6978.
  532. // Keep the descriptor local to this one incremental upsert; ordinary snapshots encode an empty
  533. // bank, while the row's rising mutation serial remains persistent State.
  534. constexpr std::uint16_t kAcquisitionChangeSequence = 0;
  535. constexpr std::uint16_t kAcquisitionChangeNextWriteSlot = 1;
  536. constexpr std::uint16_t kAcquisitionChangeNextSequence = 1;
  537. constexpr std::uint8_t kAcquisitionChangeKind = 1;
  538. constexpr std::uint16_t kAcquisitionChangeFlags = 0;
  539. auto& accountObject =
  540. *reinterpret_cast<family4_datagen::account::layout::Object*>(accountBytes.data());
  541. std::size_t acquiredRow = accountObject.profileItems.size();
  542. for (std::size_t row = 0; row < accountObject.profileItems.size(); ++row) {
  543. const auto& inventoryRow = accountObject.profileItems[row];
  544. if (inventoryRow.mutationSerial != mutation.acquiredMutationSerial) {
  545. continue;
  546. }
  547. if (acquiredRow != accountObject.profileItems.size()) {
  548. clear_after(scratch, reservation);
  549. return report_failure("profile_acquire_row_duplicate");
  550. }
  551. acquiredRow = row;
  552. }
  553. const auto recordIsZero =
  554. [](const family4_datagen::account::layout::ProfileInventoryChangeRecord& record) noexcept {
  555. return record.sequence == 0 && record.reserved == 0 && record.mutationSerial == 0
  556. && record.kind == 0 && record.reservedKind == 0 && record.flags == 0;
  557. };
  558. const bool recordsAreZero = std::all_of(accountObject.profileInventoryChanges.records.cbegin(),
  559. accountObject.profileInventoryChanges.records.cend(),
  560. recordIsZero);
  561. if (acquiredRow >= accountObject.profileItems.size()
  562. || accountObject.profileItems[acquiredRow].quantity != mutation.acquiredQuantity
  563. || accountObject.profileInventoryChanges.writeSlot != 0
  564. || accountObject.profileInventoryChanges.nextSequence != 0 || !recordsAreZero) {
  565. clear_after(scratch, reservation);
  566. return report_failure("profile_acquire_inventory_change_state");
  567. }
  568. accountObject.profileInventoryChanges.writeSlot = kAcquisitionChangeNextWriteSlot;
  569. accountObject.profileInventoryChanges.nextSequence = kAcquisitionChangeNextSequence;
  570. auto& acquisitionChange = accountObject.profileInventoryChanges.records.front();
  571. acquisitionChange.sequence = kAcquisitionChangeSequence;
  572. acquisitionChange.mutationSerial = mutation.acquiredMutationSerial;
  573. acquisitionChange.kind = kAcquisitionChangeKind;
  574. acquisitionChange.flags = kAcquisitionChangeFlags;
  575. Prepared staged{};
  576. staged.rawClearSize =
  577. (std::max)(reservation.rawClearSize,
  578. reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
  579. std::size_t compressedExtent = reservation.compressedWriteOffset;
  580. const std::size_t accountObjectIndex = acquisition.appendedResident ? 1U : 0U;
  581. if (!append_object(scratch,
  582. accountBytes,
  583. acquisition.accountDefinitionId,
  584. acquisition.accountSoid,
  585. staged.objects[accountObjectIndex],
  586. compressedExtent)) {
  587. return report_failure("profile_acquire_account_object");
  588. }
  589. std::size_t objectCount = 1;
  590. if (acquisition.appendedResident) {
  591. if (mutation.profileIndex >= account.profileItemCount) {
  592. return report_failure("profile_acquire_instance_index");
  593. }
  594. family4_datagen::instance::ResolvedInstance instance{};
  595. const state::account::inventory::ProfileItem& profileItem =
  596. account.profileItems[mutation.profileIndex];
  597. const auto instanceBytes = rawStorage.first(family4_datagen::instance::layout::kObjectSize);
  598. if (!resolve_profile_item_instance(profileItem, instance)
  599. || instance.instanceSoid != acquisition.acquiredInstanceSoid
  600. || !family4_datagen::instance::encode(instance, instanceBytes)
  601. || !append_object(scratch,
  602. instanceBytes,
  603. acquisition.itemInstanceDefinitionId,
  604. acquisition.acquiredInstanceSoid,
  605. staged.objects.front(),
  606. compressedExtent)) {
  607. return report_failure("profile_acquire_instance_object");
  608. }
  609. objectCount = 2;
  610. }
  611. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  612. staged.family = middleware::queuez::Family{
  613. kAccountFamilyType,
  614. acquisition.after.family4RootSoid,
  615. acquisition.after.family4Version,
  616. 0,
  617. std::span(staged.objects).first(objectCount),
  618. };
  619. if (!commit(staged, prepared)) {
  620. clear_after(scratch, reservation);
  621. return report_failure("profile_acquire_commit");
  622. }
  623. std::array<char, core::log::kLineCapacity> line{};
  624. const int count =
  625. std::snprintf(line.data(),
  626. line.size(),
  627. "ev=profile_acquire stage=account_object result=ok family_version=%d "
  628. "account=0x%llX definition=%u item_count=%zu definition_hash=%u quantity=%d "
  629. "native_row=%zu mutation_serial=%d change_slot=%u change_next_sequence=%u "
  630. "change_kind=%u account_payload_bytes=%zu objects=%zu object_order=%s",
  631. acquisition.after.family4Version,
  632. static_cast<unsigned long long>(acquisition.accountSoid),
  633. acquisition.accountDefinitionId,
  634. mutation.afterItemCount,
  635. mutation.acquiredDefinitionHash,
  636. mutation.acquiredQuantity,
  637. acquiredRow,
  638. mutation.acquiredMutationSerial,
  639. static_cast<unsigned>(kAcquisitionChangeNextWriteSlot),
  640. static_cast<unsigned>(kAcquisitionChangeNextSequence),
  641. static_cast<unsigned>(kAcquisitionChangeKind),
  642. prepared.family.objects[accountObjectIndex].payload.size(),
  643. objectCount,
  644. acquisition.appendedResident ? "item-account" : "account");
  645. if (count > 0) {
  646. core::log::write(core::log::Channel::server,
  647. core::log::Level::debug,
  648. {line.data(), static_cast<std::size_t>(count)});
  649. }
  650. return true;
  651. }
  652. /** Builds a new item-instance upsert before its character after-image. */
  653. bool prepare_item_acquisition(Scratch& scratch,
  654. const queuez::ItemAcquisition& acquisition,
  655. const state::PendingItemAcquisition& mutation,
  656. Prepared& prepared) noexcept {
  657. const Reservation reservation = reserve_prior(scratch, prepared);
  658. if (reservation.rawWriteOffset > scratch.plaintext.size()
  659. || reservation.compressedWriteOffset > scratch.sealed.size()) {
  660. return report_failure("acquire_reservation");
  661. }
  662. state::AccountState account{};
  663. if (!mutation.prepared || mutation.characterSoid == 0 || mutation.acquiredInstanceSoid == 0
  664. || mutation.accountSoid == 0 || mutation.accountSoid != acquisition.accountSoid
  665. || mutation.characterSoid != acquisition.characterSoid
  666. || mutation.acquiredInstanceSoid != acquisition.acquiredInstanceSoid
  667. || mutation.profileChanged != acquisition.updatesAccount
  668. || acquisition.accountSoid != acquisition.after.family4RootSoid
  669. || acquisition.accountDefinitionId == 0 || acquisition.after.family4ResidentCount == 0
  670. || acquisition.after.family4Residents[acquisition.after.family4ResidentCount - 1U]
  671. .objectSoid
  672. != mutation.acquiredInstanceSoid
  673. || acquisition.after.family4Residents[acquisition.after.family4ResidentCount - 1U]
  674. .definitionId
  675. != acquisition.itemInstanceDefinitionId) {
  676. return report_failure("acquire_mutation");
  677. }
  678. if (!state::preview_item_acquisition(mutation, account)
  679. || mutation.characterIndex >= account.characterCount
  680. || account.primarySoid != acquisition.accountSoid
  681. || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
  682. return report_failure("acquire_account");
  683. }
  684. Resolved selected{};
  685. const std::optional<std::size_t> selectedIndex = find_character_index(account);
  686. if (!state::account::valid(account) || !selectedIndex.has_value()
  687. || *selectedIndex != mutation.characterIndex
  688. || !resolve(account, mutation.characterIndex, selected)
  689. || selected.characterObjectId != acquisition.characterDefinitionId
  690. || selected.itemInstanceObjectId != acquisition.itemInstanceDefinitionId) {
  691. return report_failure("acquire_selection");
  692. }
  693. family4_datagen::loadout::ResolvedInstances acquired{};
  694. std::int32_t acquiredMutationSerial = -1;
  695. for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
  696. const family4_datagen::loadout::ResolvedItem& item = selected.loadout.items[index];
  697. if (item.instance.instanceSoid != mutation.acquiredInstanceSoid) {
  698. continue;
  699. }
  700. if (acquired.itemCount != 0 || item.equipped || item.inventoryRow != mutation.inventoryRow
  701. || item.equipmentSlot != mutation.equipmentSlot) {
  702. return report_failure("acquire_item_row");
  703. }
  704. acquired.items[0] = {item.equipmentSlot, item.instance};
  705. acquired.itemCount = 1;
  706. acquiredMutationSerial = item.mutationSerial;
  707. }
  708. if (acquired.itemCount != 1 || acquiredMutationSerial < 0) {
  709. return report_failure("acquire_item_missing");
  710. }
  711. const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  712. if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
  713. return report_failure("acquire_character_storage");
  714. }
  715. const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
  716. if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
  717. selected.loadout,
  718. selected.lightEvaluation,
  719. characterBytes)) {
  720. return report_failure("acquire_character_object");
  721. }
  722. // The native character-object observer requires a transient inventory-change record whose
  723. // serial matches the newly filled row. Without it the row is accepted, but the observer never
  724. // queues the acquisition feedback. Keep this record local to this one acquisition push; the
  725. // canonical encoder leaves the bank empty on every later snapshot.
  726. constexpr std::size_t kBitsPerFlagByte = 8;
  727. constexpr std::int32_t kEncodedOccupiedRowWatermark = 1;
  728. constexpr std::uint16_t kAcquisitionChangeSequence = 0;
  729. constexpr std::uint16_t kAcquisitionChangeNextWriteSlot = 1;
  730. constexpr std::uint16_t kAcquisitionChangeNextSequence = 1;
  731. constexpr std::uint8_t kAcquisitionChangeKind = 1;
  732. constexpr std::uint16_t kAcquisitionChangeFlags = 0;
  733. auto& characterObject =
  734. *reinterpret_cast<family4_datagen::character::layout::Object*>(characterBytes.data());
  735. const std::size_t acquiredRow = mutation.inventoryRow;
  736. if (acquiredRow >= characterObject.inventoryItems.size()
  737. || acquiredRow >= characterObject.instanceProgressWatermarks.size()) {
  738. clear_after(scratch, reservation);
  739. return report_failure("acquire_new_item_row");
  740. }
  741. const std::size_t newItemFlagIndex = acquiredRow / kBitsPerFlagByte;
  742. const std::byte newItemFlagMask = std::byte{1U} << (acquiredRow % kBitsPerFlagByte);
  743. const auto recordIsZero =
  744. [](const family4_datagen::character::layout::InventoryChangeRecord& record) noexcept {
  745. return record.sequence == 0 && record.reserved == 0 && record.mutationSerial == 0
  746. && record.kind == 0 && record.reservedKind == 0 && record.flags == 0;
  747. };
  748. const bool unknownIsZero =
  749. std::all_of(characterObject.inventoryChangeUnknown.cbegin(),
  750. characterObject.inventoryChangeUnknown.cend(),
  751. [](std::byte value) noexcept { return value == std::byte{}; });
  752. const bool recordsAreZero = std::all_of(characterObject.inventoryChanges.records.cbegin(),
  753. characterObject.inventoryChanges.records.cend(),
  754. recordIsZero);
  755. const auto& acquiredInventoryRow = characterObject.inventoryItems[acquiredRow];
  756. if (newItemFlagIndex >= characterObject.newItemFlags.size()
  757. || acquiredInventoryRow.instanceSoid != mutation.acquiredInstanceSoid
  758. || acquiredInventoryRow.mutationSerial != acquiredMutationSerial
  759. || (characterObject.newItemFlags[newItemFlagIndex] & newItemFlagMask) != newItemFlagMask
  760. || characterObject.instanceProgressWatermarks[acquiredRow] != kEncodedOccupiedRowWatermark
  761. || !unknownIsZero || characterObject.inventoryChanges.writeSlot != 0
  762. || characterObject.inventoryChanges.nextSequence != 0 || !recordsAreZero) {
  763. clear_after(scratch, reservation);
  764. return report_failure("acquire_inventory_change_state");
  765. }
  766. characterObject.inventoryChanges.writeSlot = kAcquisitionChangeNextWriteSlot;
  767. characterObject.inventoryChanges.nextSequence = kAcquisitionChangeNextSequence;
  768. auto& acquisitionChange = characterObject.inventoryChanges.records.front();
  769. acquisitionChange.sequence = kAcquisitionChangeSequence;
  770. acquisitionChange.mutationSerial = acquiredMutationSerial;
  771. acquisitionChange.kind = kAcquisitionChangeKind;
  772. acquisitionChange.flags = kAcquisitionChangeFlags;
  773. if (!std::all_of(characterObject.inventoryChanges.records.cbegin() + 1,
  774. characterObject.inventoryChanges.records.cend(),
  775. recordIsZero)) {
  776. clear_after(scratch, reservation);
  777. return report_failure("acquire_inventory_change_records");
  778. }
  779. Prepared staged{};
  780. staged.rawClearSize =
  781. (std::max)(reservation.rawClearSize,
  782. reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
  783. std::size_t compressedExtent = reservation.compressedWriteOffset;
  784. if (!append_object(scratch,
  785. characterBytes,
  786. acquisition.characterDefinitionId,
  787. acquisition.characterSoid,
  788. staged.objects[0],
  789. compressedExtent)) {
  790. return report_failure("acquire_character_object");
  791. }
  792. std::size_t itemCursor = 0;
  793. if (!append_items(scratch,
  794. rawStorage,
  795. acquisition.itemInstanceDefinitionId,
  796. acquired,
  797. 1,
  798. staged,
  799. itemCursor,
  800. compressedExtent)
  801. || itemCursor != 1) {
  802. clear_after(scratch, reservation);
  803. return report_failure("acquire_item_object");
  804. }
  805. const auto& acquiredObject =
  806. *reinterpret_cast<const family4_datagen::instance::layout::Object*>(rawStorage.data());
  807. if (acquiredObject.instanceSoid != mutation.acquiredInstanceSoid
  808. || acquiredObject.roll.progress
  809. != family4_datagen::instance::layout::kInitialInstanceProgress) {
  810. clear_after(scratch, reservation);
  811. return report_failure("acquire_item_progress");
  812. }
  813. const std::int32_t acquiredInstanceProgress = acquiredObject.roll.progress;
  814. std::size_t objectCount = 2;
  815. if (acquisition.updatesAccount) {
  816. if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
  817. clear_after(scratch, reservation);
  818. return report_failure("acquire_account_storage");
  819. }
  820. const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
  821. if (!family4_datagen::account::encode(account, accountBytes)
  822. || !append_object(scratch,
  823. accountBytes,
  824. acquisition.accountDefinitionId,
  825. acquisition.accountSoid,
  826. staged.objects[2],
  827. compressedExtent)) {
  828. clear_after(scratch, reservation);
  829. return report_failure("acquire_account_object");
  830. }
  831. staged.rawClearSize =
  832. (std::max)(staged.rawClearSize,
  833. reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
  834. objectCount = 3;
  835. }
  836. // Creation increments publish the dependency before the reference to it. Compression order is
  837. // irrelevant because each descriptor already owns its sealed span, so exchange only the wire
  838. // descriptors: new item first, then the character after-image. Dismantle deliberately uses the
  839. // inverse dependency order (drop the character reference, then release the item).
  840. std::swap(staged.objects[0], staged.objects[1]);
  841. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  842. staged.family = middleware::queuez::Family{
  843. kAccountFamilyType,
  844. acquisition.after.family4RootSoid,
  845. acquisition.after.family4Version,
  846. 0,
  847. std::span(staged.objects).first(objectCount),
  848. };
  849. if (!commit(staged, prepared)) {
  850. clear_after(scratch, reservation);
  851. return report_failure("acquire_commit");
  852. }
  853. std::array<char, core::log::kLineCapacity> line{};
  854. const int count = std::snprintf(
  855. line.data(),
  856. line.size(),
  857. "ev=acquire stage=family4_objects result=ok family_version=%d root=0x%llX "
  858. "character=0x%llX character_definition=%u instance=0x%llX item_definition=%u "
  859. "definition_hash=%u inventory_row=%u equipment_slot=%u next_serial=%u objects=%zu "
  860. "order=%s new_item_flag=1 watermark=1 acquired_row_serial=%d "
  861. "inventory_change_write_slot=%u inventory_change_next_sequence=%u "
  862. "inventory_change_record=0 inventory_change_sequence=%u "
  863. "inventory_change_serial=%d inventory_change_kind=%u inventory_change_flags=%u "
  864. "instance_progress=%d",
  865. acquisition.after.family4Version,
  866. static_cast<unsigned long long>(acquisition.after.family4RootSoid),
  867. static_cast<unsigned long long>(acquisition.characterSoid),
  868. acquisition.characterDefinitionId,
  869. static_cast<unsigned long long>(acquisition.acquiredInstanceSoid),
  870. acquisition.itemInstanceDefinitionId,
  871. mutation.acquiredDefinitionHash,
  872. static_cast<unsigned>(mutation.inventoryRow),
  873. static_cast<unsigned>(mutation.equipmentSlot),
  874. mutation.afterCharacter.nextInventorySerial,
  875. objectCount,
  876. acquisition.updatesAccount ? "item_character_account" : "item_character",
  877. acquiredMutationSerial,
  878. static_cast<unsigned>(kAcquisitionChangeNextWriteSlot),
  879. static_cast<unsigned>(kAcquisitionChangeNextSequence),
  880. static_cast<unsigned>(kAcquisitionChangeSequence),
  881. acquiredMutationSerial,
  882. static_cast<unsigned>(kAcquisitionChangeKind),
  883. static_cast<unsigned>(kAcquisitionChangeFlags),
  884. acquiredInstanceProgress);
  885. if (count > 0) {
  886. core::log::write(core::log::Channel::server,
  887. core::log::Level::debug,
  888. {line.data(), static_cast<std::size_t>(count)});
  889. }
  890. return true;
  891. }
  892. /** Builds a character upsert followed by one empty item-instance release descriptor. */
  893. bool prepare_item_dismantle(Scratch& scratch,
  894. const queuez::ItemDismantle& dismantle,
  895. const state::PendingItemDismantle& mutation,
  896. Prepared& prepared) noexcept {
  897. const Reservation reservation = reserve_prior(scratch, prepared);
  898. if (reservation.rawWriteOffset > scratch.plaintext.size()
  899. || reservation.compressedWriteOffset > scratch.sealed.size()) {
  900. return report_failure("dismantle_reservation");
  901. }
  902. state::AccountState account = state::account_snapshot();
  903. if (!mutation.prepared || mutation.characterSoid == 0 || mutation.dismantledInstanceSoid == 0
  904. || mutation.dismantledItem.instanceSoid != mutation.dismantledInstanceSoid
  905. || mutation.characterSoid != dismantle.characterSoid
  906. || mutation.dismantledInstanceSoid != dismantle.dismantledInstanceSoid
  907. || mutation.characterIndex >= account.characterCount
  908. || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
  909. return report_failure("dismantle_mutation");
  910. }
  911. account.characters[mutation.characterIndex] = mutation.afterCharacter;
  912. Resolved selected{};
  913. const std::optional<std::size_t> selectedIndex = find_character_index(account);
  914. if (!state::account::valid(account) || !selectedIndex.has_value()
  915. || *selectedIndex != mutation.characterIndex
  916. || !resolve(account, mutation.characterIndex, selected)
  917. || selected.characterObjectId != dismantle.characterDefinitionId
  918. || selected.itemInstanceObjectId != dismantle.itemInstanceDefinitionId) {
  919. return report_failure("dismantle_selection");
  920. }
  921. for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
  922. if (selected.loadout.items[index].instance.instanceSoid
  923. == mutation.dismantledInstanceSoid) {
  924. return report_failure("dismantle_item_present");
  925. }
  926. }
  927. const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  928. if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
  929. return report_failure("dismantle_character_storage");
  930. }
  931. const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
  932. if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
  933. selected.loadout,
  934. selected.lightEvaluation,
  935. characterBytes)) {
  936. return report_failure("dismantle_character_object");
  937. }
  938. Prepared staged{};
  939. staged.rawClearSize =
  940. (std::max)(reservation.rawClearSize,
  941. reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
  942. std::size_t compressedExtent = reservation.compressedWriteOffset;
  943. if (!append_object(scratch,
  944. characterBytes,
  945. dismantle.characterDefinitionId,
  946. dismantle.characterSoid,
  947. staged.objects[0],
  948. compressedExtent)) {
  949. return report_failure("dismantle_character_object");
  950. }
  951. // Queuez represents a release with the ordinary object key and an empty payload. The
  952. // encoding selector is not read for empty descriptors; oodle matches the surrounding objects.
  953. staged.objects[1] = middleware::queuez::Object{
  954. dismantle.itemInstanceDefinitionId,
  955. dismantle.dismantledInstanceSoid,
  956. middleware::queuez::Encoding::oodle,
  957. {},
  958. };
  959. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  960. staged.family = middleware::queuez::Family{
  961. kAccountFamilyType,
  962. dismantle.after.family4RootSoid,
  963. dismantle.after.family4Version,
  964. 0,
  965. std::span(staged.objects).first(2),
  966. };
  967. if (!commit(staged, prepared)) {
  968. clear_after(scratch, reservation);
  969. return report_failure("dismantle_commit");
  970. }
  971. std::array<char, core::log::kLineCapacity> line{};
  972. const int count =
  973. std::snprintf(line.data(),
  974. line.size(),
  975. "ev=dismantle stage=family4_objects result=ok family_version=%d root=0x%llX "
  976. "character=0x%llX character_definition=%u instance=0x%llX item_definition=%u "
  977. "definition_hash=%u inventory_index=%zu inventory_row=%u equipment_slot=%u "
  978. "moved_items=%zu items_after=%zu next_serial=%u",
  979. dismantle.after.family4Version,
  980. static_cast<unsigned long long>(dismantle.after.family4RootSoid),
  981. static_cast<unsigned long long>(dismantle.characterSoid),
  982. dismantle.characterDefinitionId,
  983. static_cast<unsigned long long>(dismantle.dismantledInstanceSoid),
  984. dismantle.itemInstanceDefinitionId,
  985. mutation.dismantledItem.definitionHash,
  986. mutation.inventoryIndex,
  987. static_cast<unsigned>(mutation.inventoryRow),
  988. static_cast<unsigned>(mutation.equipmentSlot),
  989. mutation.movedInventoryItemCount,
  990. mutation.afterCharacter.inventory.count,
  991. mutation.afterCharacter.nextInventorySerial);
  992. if (count > 0) {
  993. core::log::write(core::log::Channel::server,
  994. core::log::Level::debug,
  995. {line.data(), static_cast<std::size_t>(count)});
  996. }
  997. return true;
  998. }
  999. } // namespace sunrise::server::bap::encrypted::push::snapshot