web_service_actions.cpp 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. #include "web_service_actions.h"
  2. #include <array>
  3. #include <cstdio>
  4. #include <limits>
  5. #include <string_view>
  6. #include "../../core/logging/log.h"
  7. #include "../../middleware/web_service/messages/opcode1801.h"
  8. #include "../../middleware/web_service/messages/opcode1820.h"
  9. #include "../../middleware/web_service/messages/opcode1901.h"
  10. #include "../../middleware/web_service/messages/opcode402.h"
  11. #include "../../middleware/web_service/messages/opcode403.h"
  12. #include "../../middleware/web_service/messages/opcode406.h"
  13. #include "../../middleware/web_service/messages/opcode504.h"
  14. #include "../../middleware/web_service/messages/opcode801.h"
  15. #include "../../middleware/web_service/messages/opcode903.h"
  16. #include "../../state/account/account_state.h"
  17. #include "../../state/build_data/runtime.h"
  18. #include "../../state/runtime/runtime.h"
  19. namespace sunrise::server::web_service {
  20. namespace {
  21. /** Socket kind the shader model occupies, which is the only kind a shader swap may target. */
  22. constexpr std::uint8_t kEquippedShaderModelSocketKind = 0;
  23. /** Index stored when no definition resolves. The catalog is u16-indexed, so this cannot be one. */
  24. constexpr std::uint32_t kUnavailableDefinitionIndex = (std::numeric_limits<std::uint16_t>::max)();
  25. } // namespace
  26. /** Logs one exact correlated equipment response after its Queuez update is staged. */
  27. void report_equip_response(const middleware::web_service::Message& message,
  28. std::int32_t family4Version,
  29. std::span<const std::byte> response) noexcept {
  30. std::array<char, core::log::kLineCapacity> line{};
  31. const int prefix = std::snprintf(line.data(),
  32. line.size(),
  33. "ev=equipment stage=response opcode=%u transaction=%u "
  34. "family_version=%d bytes=%zu hex=",
  35. static_cast<unsigned>(message.opcode),
  36. static_cast<unsigned>(message.transactionId),
  37. family4Version,
  38. response.size());
  39. if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
  40. return;
  41. }
  42. std::size_t length = static_cast<std::size_t>(prefix);
  43. (void)core::log::append_hex(line, length, response);
  44. core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
  45. }
  46. /** Logs the final item-creation status pair and the exact Family-4 revision it promises. */
  47. void report_item_acquisition_response(const middleware::web_service::Message& message,
  48. std::int32_t family4Version,
  49. std::uint64_t acquiredInstanceSoid,
  50. std::span<const std::byte> response) noexcept {
  51. std::array<char, core::log::kLineCapacity> line{};
  52. const int prefix = std::snprintf(
  53. line.data(),
  54. line.size(),
  55. "ev=acquire stage=response result=ok opcode=%u transaction=%u family_version=%d "
  56. "instance=0x%llX bytes=%zu hex=",
  57. static_cast<unsigned>(message.opcode),
  58. static_cast<unsigned>(message.transactionId),
  59. family4Version,
  60. static_cast<unsigned long long>(acquiredInstanceSoid),
  61. response.size());
  62. if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
  63. return;
  64. }
  65. std::size_t length = static_cast<std::size_t>(prefix);
  66. (void)core::log::append_hex(line, length, response);
  67. core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
  68. }
  69. /** Logs the final profile-stack status pair and the exact Family-4 account revision it promises. */
  70. void report_profile_item_acquisition_response(const middleware::web_service::Message& message,
  71. std::int32_t family4Version,
  72. std::uint32_t definitionHash,
  73. std::int32_t quantity,
  74. std::span<const std::byte> response) noexcept {
  75. std::array<char, core::log::kLineCapacity> line{};
  76. const int prefix =
  77. std::snprintf(line.data(),
  78. line.size(),
  79. "ev=profile_acquire stage=response result=ok opcode=%u transaction=%u "
  80. "family_version=%d definition_hash=0x%08X quantity=%d bytes=%zu hex=",
  81. static_cast<unsigned>(message.opcode),
  82. static_cast<unsigned>(message.transactionId),
  83. family4Version,
  84. definitionHash,
  85. quantity,
  86. response.size());
  87. if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
  88. return;
  89. }
  90. std::size_t length = static_cast<std::size_t>(prefix);
  91. (void)core::log::append_hex(line, length, response);
  92. core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
  93. }
  94. /** Logs the final dismantle status pair and the exact Family-4 revision it promises. */
  95. void report_item_dismantle_response(const middleware::web_service::Message& message,
  96. std::int32_t family4Version,
  97. std::uint64_t dismantledInstanceSoid,
  98. std::span<const std::byte> response) noexcept {
  99. std::array<char, core::log::kLineCapacity> line{};
  100. const int prefix = std::snprintf(
  101. line.data(),
  102. line.size(),
  103. "ev=dismantle stage=response result=ok opcode=%u transaction=%u family_version=%d "
  104. "instance=0x%llX bytes=%zu hex=",
  105. static_cast<unsigned>(message.opcode),
  106. static_cast<unsigned>(message.transactionId),
  107. family4Version,
  108. static_cast<unsigned long long>(dismantledInstanceSoid),
  109. response.size());
  110. if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
  111. return;
  112. }
  113. std::size_t length = static_cast<std::size_t>(prefix);
  114. (void)core::log::append_hex(line, length, response);
  115. core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
  116. }
  117. /** Logs the exact opcode-903 status pair and the item-instance revision it promises. */
  118. void report_socket_plug_response(const middleware::web_service::Message& message,
  119. std::int32_t family4Version,
  120. std::uint64_t targetInstanceSoid,
  121. std::uint8_t socketLane,
  122. std::uint16_t plugDefinitionIndex,
  123. std::span<const std::byte> response) noexcept {
  124. std::array<char, core::log::kLineCapacity> line{};
  125. const int prefix = std::snprintf(
  126. line.data(),
  127. line.size(),
  128. "ev=socket_plug stage=response result=ok opcode=%u transaction=%u family_version=%d "
  129. "instance=0x%llX lane=%u plug_definition=%u bytes=%zu hex=",
  130. static_cast<unsigned>(message.opcode),
  131. static_cast<unsigned>(message.transactionId),
  132. family4Version,
  133. static_cast<unsigned long long>(targetInstanceSoid),
  134. static_cast<unsigned>(socketLane),
  135. static_cast<unsigned>(plugDefinitionIndex),
  136. response.size());
  137. if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
  138. return;
  139. }
  140. std::size_t length = static_cast<std::size_t>(prefix);
  141. (void)core::log::append_hex(line, length, response);
  142. core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
  143. }
  144. /** Logs the exact opcode-801 status pair and subclass item revision it promises. */
  145. void report_subclass_selection_response(const middleware::web_service::Message& message,
  146. std::int32_t family4Version,
  147. const state::PendingSubclassSelection& mutation,
  148. std::span<const std::byte> response) noexcept {
  149. std::array<char, core::log::kLineCapacity> line{};
  150. const int prefix =
  151. std::snprintf(line.data(),
  152. line.size(),
  153. "ev=subclass_select stage=response result=ok opcode=%u transaction=%u "
  154. "family_version=%d instance=0x%llX entry=%u bytes=%zu hex=",
  155. static_cast<unsigned>(message.opcode),
  156. static_cast<unsigned>(message.transactionId),
  157. family4Version,
  158. static_cast<unsigned long long>(mutation.subclassInstanceSoid),
  159. static_cast<unsigned>(mutation.requestedEntry),
  160. response.size());
  161. if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
  162. return;
  163. }
  164. // Upper-case hex, which is the form the rest of the log lines use.
  165. constexpr char kHex[] = "0123456789ABCDEF";
  166. std::size_t length = static_cast<std::size_t>(prefix);
  167. for (const std::byte byte : response) {
  168. if (length + 2 >= line.size()) {
  169. break;
  170. }
  171. const unsigned value = std::to_integer<unsigned>(byte);
  172. line[length++] = kHex[(value >> 4U) & 0xFU];
  173. line[length++] = kHex[value & 0xFU];
  174. }
  175. core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
  176. }
  177. /** One line carries the picked id and whether the selection moved. */
  178. constexpr std::size_t kSelectLineCapacity = 96;
  179. /**
  180. * Records the player's character pick, which arrives nowhere else.
  181. * A bad or unknown id leaves the selection alone. The reply is the status pair either way. The
  182. * Family-4 object move follows this call, and the family-zero pair after it.
  183. * @param message Parsed select-character request.
  184. * @param outcome Gets the picked key once the selection has moved in State.
  185. */
  186. void select_character(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
  187. middleware::web_service::messages::opcode504::Request picked;
  188. if (!middleware::web_service::messages::opcode504::parse_request(message, picked)) {
  189. core::log::write(
  190. core::log::Channel::server, core::log::Level::warn, "ev=ws504 stage=parse result=fail");
  191. return;
  192. }
  193. bool changed = false;
  194. if (!state::set_selected_character(picked.characterSoid, changed)) {
  195. core::log::write(core::log::Channel::server,
  196. core::log::Level::warn,
  197. "ev=ws504 stage=select result=unknown");
  198. return;
  199. }
  200. outcome.hasSelectedCharacter = true;
  201. outcome.selectedCharacterSoid = picked.characterSoid;
  202. std::array<char, kSelectLineCapacity> line{};
  203. const int written = std::snprintf(line.data(),
  204. line.size(),
  205. "ev=ws504 stage=select result=ok soid=0x%llX changed=%u",
  206. static_cast<unsigned long long>(picked.characterSoid),
  207. static_cast<unsigned>(changed));
  208. if (written > 0) {
  209. core::log::write(core::log::Channel::server,
  210. core::log::Level::debug,
  211. {line.data(), static_cast<std::size_t>(written)});
  212. }
  213. }
  214. /** Reads the shared opcode-403/404 SOID descriptor through its codec. */
  215. [[nodiscard]] bool parse_equipment_instance(const middleware::web_service::Message& message,
  216. std::uint64_t& instanceSoid) noexcept {
  217. middleware::web_service::messages::opcode403::Request request{};
  218. const bool parsed =
  219. middleware::web_service::messages::opcode403::parse_request(message, request);
  220. instanceSoid = request.instanceSoid;
  221. return parsed;
  222. }
  223. /** Prepares one opcode-403/404 equipment mutation without publishing State early. */
  224. void mutate_equipment(const middleware::web_service::Message& message,
  225. bool unequip,
  226. Outcome& outcome) noexcept {
  227. std::uint64_t requestedInstanceSoid = 0;
  228. if (!parse_equipment_instance(message, requestedInstanceSoid)) {
  229. std::array<char, 112> line{};
  230. const int count = std::snprintf(line.data(),
  231. line.size(),
  232. "ev=equipment stage=parse result=fail opcode=%u "
  233. "payload_bytes=%zu",
  234. static_cast<unsigned>(message.opcode),
  235. message.payload.size());
  236. if (count > 0) {
  237. core::log::write(core::log::Channel::server,
  238. core::log::Level::warn,
  239. {line.data(), static_cast<std::size_t>(count)});
  240. }
  241. return;
  242. }
  243. state::PendingEquipmentSwap mutation;
  244. const bool prepared = unequip
  245. ? state::prepare_equipment_unequip(requestedInstanceSoid, mutation)
  246. : state::prepare_equipment_swap(requestedInstanceSoid, mutation);
  247. if (!prepared) {
  248. std::array<char, 144> line{};
  249. const int count = std::snprintf(
  250. line.data(),
  251. line.size(),
  252. "ev=equipment stage=prepare result=fail opcode=%u action=%s requested=0x%llX",
  253. static_cast<unsigned>(message.opcode),
  254. unequip ? "unequip" : "equip",
  255. static_cast<unsigned long long>(requestedInstanceSoid));
  256. if (count > 0) {
  257. core::log::write(core::log::Channel::server,
  258. core::log::Level::warn,
  259. {line.data(), static_cast<std::size_t>(count)});
  260. }
  261. return;
  262. }
  263. outcome.mutation = mutation;
  264. std::array<char, 224> line{};
  265. const int count =
  266. std::snprintf(line.data(),
  267. line.size(),
  268. "ev=equipment stage=prepare result=ok opcode=%u action=%s character=0x%llX "
  269. "previous=0x%llX requested=0x%llX native_slot=%u moved_items=%zu",
  270. static_cast<unsigned>(message.opcode),
  271. unequip ? "unequip" : "equip",
  272. static_cast<unsigned long long>(mutation.characterSoid),
  273. static_cast<unsigned long long>(mutation.previousInstanceSoid),
  274. static_cast<unsigned long long>(mutation.requestedInstanceSoid),
  275. static_cast<unsigned>(mutation.nativeEquipmentSlot),
  276. mutation.movedItemCount);
  277. if (count > 0) {
  278. core::log::write(core::log::Channel::server,
  279. core::log::Level::debug,
  280. {line.data(), static_cast<std::size_t>(count)});
  281. }
  282. }
  283. /** Parses and prepares one exact selected-character opcode-801 subclass node selection. */
  284. void mutate_subclass_selection(const middleware::web_service::Message& message,
  285. Outcome& outcome) noexcept {
  286. middleware::web_service::messages::opcode801::Request request{};
  287. if (!middleware::web_service::messages::opcode801::parse_request(message, request)) {
  288. std::array<char, 128> line{};
  289. const int count =
  290. std::snprintf(line.data(),
  291. line.size(),
  292. "ev=ws801 stage=parse result=fail transaction=%u payload_bytes=%zu",
  293. static_cast<unsigned>(message.transactionId),
  294. message.payload.size());
  295. if (count > 0) {
  296. core::log::write(core::log::Channel::server,
  297. core::log::Level::warn,
  298. {line.data(), static_cast<std::size_t>(count)});
  299. }
  300. return;
  301. }
  302. state::PendingSubclassSelection mutation{};
  303. if (!state::prepare_subclass_selection(
  304. request.subclassInstanceSoid, request.socketEntry, mutation)) {
  305. std::array<char, 160> line{};
  306. const int count = std::snprintf(
  307. line.data(),
  308. line.size(),
  309. "ev=ws801 stage=prepare result=fail transaction=%u instance=0x%llX entry=%u",
  310. static_cast<unsigned>(message.transactionId),
  311. static_cast<unsigned long long>(request.subclassInstanceSoid),
  312. static_cast<unsigned>(request.socketEntry));
  313. if (count > 0) {
  314. core::log::write(core::log::Channel::server,
  315. core::log::Level::warn,
  316. {line.data(), static_cast<std::size_t>(count)});
  317. }
  318. return;
  319. }
  320. outcome.mutation = mutation;
  321. std::array<char, 224> line{};
  322. const int count = std::snprintf(
  323. line.data(),
  324. line.size(),
  325. "ev=ws801 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
  326. "entry=%u socket_list=%u",
  327. static_cast<unsigned>(message.transactionId),
  328. static_cast<unsigned long long>(mutation.characterSoid),
  329. static_cast<unsigned long long>(mutation.subclassInstanceSoid),
  330. static_cast<unsigned>(mutation.requestedEntry),
  331. static_cast<unsigned>(mutation.socketEntryListIndex));
  332. if (count > 0) {
  333. core::log::write(core::log::Channel::server,
  334. core::log::Level::info,
  335. {line.data(), static_cast<std::size_t>(count)});
  336. }
  337. }
  338. /** Parses and prepares one exact selected-character opcode-903 socket selection. */
  339. void mutate_socket_plug(const middleware::web_service::Message& message,
  340. Outcome& outcome) noexcept {
  341. middleware::web_service::messages::opcode903::Request request{};
  342. if (!middleware::web_service::messages::opcode903::parse_request(message, request)
  343. || !request.hasInstance || request.instanceSoid == 0 || request.hasTargetDefinition
  344. || !request.hasPlugDefinition
  345. || request.socketIndex >= state::account::inventory::kPlugCapacity) {
  346. std::array<char, 192> line{};
  347. const int count = std::snprintf(
  348. line.data(),
  349. line.size(),
  350. "ev=ws903 stage=parse result=fail transaction=%u payload_bytes=%zu has_instance=%u "
  351. "instance=0x%llX has_target_definition=%u socket=%u has_plug_definition=%u",
  352. static_cast<unsigned>(message.transactionId),
  353. message.payload.size(),
  354. static_cast<unsigned>(request.hasInstance),
  355. static_cast<unsigned long long>(request.instanceSoid),
  356. static_cast<unsigned>(request.hasTargetDefinition),
  357. request.socketIndex,
  358. static_cast<unsigned>(request.hasPlugDefinition));
  359. if (count > 0) {
  360. core::log::write(core::log::Channel::server,
  361. core::log::Level::warn,
  362. {line.data(), static_cast<std::size_t>(count)});
  363. }
  364. return;
  365. }
  366. state::PendingSocketPlug mutation{};
  367. if (!state::prepare_socket_plug(request.instanceSoid,
  368. static_cast<std::uint8_t>(request.socketIndex),
  369. request.plugDefinitionIndex,
  370. mutation)) {
  371. std::array<char, 192> line{};
  372. const int count = std::snprintf(
  373. line.data(),
  374. line.size(),
  375. "ev=ws903 stage=prepare result=fail transaction=%u instance=0x%llX lane=%u "
  376. "plug_definition=%u",
  377. static_cast<unsigned>(message.transactionId),
  378. static_cast<unsigned long long>(request.instanceSoid),
  379. request.socketIndex,
  380. static_cast<unsigned>(request.plugDefinitionIndex));
  381. if (count > 0) {
  382. core::log::write(core::log::Channel::server,
  383. core::log::Level::warn,
  384. {line.data(), static_cast<std::size_t>(count)});
  385. }
  386. return;
  387. }
  388. outcome.mutation = mutation;
  389. std::array<char, 240> line{};
  390. const int count = std::snprintf(
  391. line.data(),
  392. line.size(),
  393. "ev=ws903 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
  394. "target_definition=%u target_bucket=%u lane=%u plug_definition=%u plug_bucket=%u "
  395. "equipped=%u item_index=%zu",
  396. static_cast<unsigned>(message.transactionId),
  397. static_cast<unsigned long long>(mutation.characterSoid),
  398. static_cast<unsigned long long>(mutation.targetInstanceSoid),
  399. static_cast<unsigned>(mutation.targetDefinitionIndex),
  400. static_cast<unsigned>(mutation.targetBucketId),
  401. static_cast<unsigned>(mutation.socketLane),
  402. static_cast<unsigned>(mutation.plugDefinitionIndex),
  403. static_cast<unsigned>(mutation.plugBucketId),
  404. static_cast<unsigned>(mutation.targetEquipped),
  405. mutation.itemIndex);
  406. if (count > 0) {
  407. core::log::write(core::log::Channel::server,
  408. core::log::Level::debug,
  409. {line.data(), static_cast<std::size_t>(count)});
  410. }
  411. }
  412. /** Parses and prepares one character-location opcode-1901 socket selection. */
  413. void mutate_equipped_socket_plug(const middleware::web_service::Message& message,
  414. Outcome& outcome) noexcept {
  415. namespace opcode1901 = middleware::web_service::messages::opcode1901;
  416. opcode1901::Request request{};
  417. const bool parsed = opcode1901::parse_request(message, request);
  418. // A request prepares at most one State mutation, so a run naming several sockets cannot be
  419. // applied as the one transaction it has to be. It is understood and declined rather than
  420. // treated as malformed, and the reply now carries that refusal.
  421. const opcode1901::Replacement& replacement = request.replacements.front();
  422. if (!parsed || request.replacementCount != 1
  423. || replacement.modelSocketKind != kEquippedShaderModelSocketKind
  424. || replacement.auxiliary != 0
  425. || replacement.socketIndex >= state::account::inventory::kPlugCapacity
  426. || request.instanceIdentityToken == 0) {
  427. std::array<char, 256> line{};
  428. const int count = std::snprintf(
  429. line.data(),
  430. line.size(),
  431. "ev=ws1901 stage=parse result=fail transaction=%u payload_bytes=%zu replacements=%zu "
  432. "plug_definition=%u canonical_kind=%u model_kind=%u socket=%u auxiliary=0x%llX "
  433. "equipment_selector=%llu",
  434. static_cast<unsigned>(message.transactionId),
  435. message.payload.size(),
  436. request.replacementCount,
  437. static_cast<unsigned>(replacement.plugDefinitionIndex),
  438. static_cast<unsigned>(replacement.canonicalSocketKind),
  439. static_cast<unsigned>(replacement.modelSocketKind),
  440. replacement.socketIndex,
  441. static_cast<unsigned long long>(replacement.auxiliary),
  442. static_cast<unsigned long long>(request.equipmentSelector));
  443. if (count > 0) {
  444. core::log::write(core::log::Channel::server,
  445. core::log::Level::warn,
  446. {line.data(), static_cast<std::size_t>(count)});
  447. }
  448. return;
  449. }
  450. const std::uint64_t identityToken = request.instanceIdentityToken;
  451. state::PendingSocketPlug mutation{};
  452. if (!state::prepare_character_selector_socket_plug(
  453. request.instanceIdentityToken,
  454. static_cast<std::uint8_t>(replacement.socketIndex),
  455. replacement.plugDefinitionIndex,
  456. mutation)) {
  457. std::array<char, 224> line{};
  458. const int count = std::snprintf(
  459. line.data(),
  460. line.size(),
  461. "ev=ws1901 stage=prepare result=fail transaction=%u equipment_selector=%llu "
  462. "identity_token=%llu lane=%u plug_definition=%u canonical_kind=%u model_kind=%u "
  463. "auxiliary=0x%llX",
  464. static_cast<unsigned>(message.transactionId),
  465. static_cast<unsigned long long>(request.equipmentSelector),
  466. static_cast<unsigned long long>(identityToken),
  467. replacement.socketIndex,
  468. static_cast<unsigned>(replacement.plugDefinitionIndex),
  469. static_cast<unsigned>(replacement.canonicalSocketKind),
  470. static_cast<unsigned>(replacement.modelSocketKind),
  471. static_cast<unsigned long long>(replacement.auxiliary));
  472. if (count > 0) {
  473. core::log::write(core::log::Channel::server,
  474. core::log::Level::warn,
  475. {line.data(), static_cast<std::size_t>(count)});
  476. }
  477. return;
  478. }
  479. outcome.mutation = mutation;
  480. std::array<char, 288> line{};
  481. const int count = std::snprintf(
  482. line.data(),
  483. line.size(),
  484. "ev=ws1901 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
  485. "equipment_selector=%llu identity_token=%llu target_definition=%u target_bucket=%u "
  486. "lane=%u plug_definition=%u plug_bucket=%u canonical_kind=%u model_kind=%u "
  487. "auxiliary=0x%llX",
  488. static_cast<unsigned>(message.transactionId),
  489. static_cast<unsigned long long>(mutation.characterSoid),
  490. static_cast<unsigned long long>(mutation.targetInstanceSoid),
  491. static_cast<unsigned long long>(request.equipmentSelector),
  492. static_cast<unsigned long long>(identityToken),
  493. static_cast<unsigned>(mutation.targetDefinitionIndex),
  494. static_cast<unsigned>(mutation.targetBucketId),
  495. static_cast<unsigned>(mutation.socketLane),
  496. static_cast<unsigned>(mutation.plugDefinitionIndex),
  497. static_cast<unsigned>(mutation.plugBucketId),
  498. static_cast<unsigned>(replacement.canonicalSocketKind),
  499. static_cast<unsigned>(replacement.modelSocketKind),
  500. static_cast<unsigned long long>(replacement.auxiliary));
  501. if (count > 0) {
  502. core::log::write(core::log::Channel::server,
  503. core::log::Level::debug,
  504. {line.data(), static_cast<std::size_t>(count)});
  505. }
  506. }
  507. /** Parses and prepares one complete accumulated item-state value from opcode 406. */
  508. void mutate_item_state(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
  509. middleware::web_service::messages::opcode406::Request request{};
  510. if (!middleware::web_service::messages::opcode406::parse_request(message, request)) {
  511. std::array<char, 224> line{};
  512. const int count = std::snprintf(
  513. line.data(),
  514. line.size(),
  515. "ev=ws406 stage=parse result=fail transaction=%u payload_bytes=%zu instance=0x%llX "
  516. "definition=%u flags=0x%X",
  517. static_cast<unsigned>(message.transactionId),
  518. message.payload.size(),
  519. static_cast<unsigned long long>(request.instanceSoid),
  520. static_cast<unsigned>(request.definitionIndex),
  521. request.flags);
  522. if (count > 0) {
  523. core::log::write(core::log::Channel::server,
  524. core::log::Level::warn,
  525. {line.data(), static_cast<std::size_t>(count)});
  526. }
  527. return;
  528. }
  529. const std::uint64_t instanceSoid = request.instanceSoid;
  530. const std::uint32_t flags = request.flags;
  531. state::PendingItemState mutation{};
  532. if (!state::prepare_item_state(instanceSoid, request.definitionIndex, flags, mutation)) {
  533. return;
  534. }
  535. outcome.mutation = mutation;
  536. std::array<char, 224> line{};
  537. const int count = std::snprintf(
  538. line.data(),
  539. line.size(),
  540. "ev=ws406 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
  541. "definition=%u flags_before=0x%X flags_after=0x%X equipped=%u item_index=%zu",
  542. static_cast<unsigned>(message.transactionId),
  543. static_cast<unsigned long long>(mutation.characterSoid),
  544. static_cast<unsigned long long>(mutation.targetInstanceSoid),
  545. static_cast<unsigned>(mutation.targetDefinitionIndex),
  546. mutation.beforeFlags,
  547. mutation.afterFlags,
  548. mutation.targetEquipped ? 1U : 0U,
  549. mutation.itemIndex);
  550. if (count > 0) {
  551. core::log::write(core::log::Channel::server,
  552. core::log::Level::debug,
  553. {line.data(), static_cast<std::size_t>(count)});
  554. }
  555. }
  556. /** Records strict opcode-402 parsing, identity checks, and State preparation outcomes. */
  557. void report_item_dismantle(const middleware::web_service::Message& message,
  558. std::string_view result,
  559. std::string_view reason,
  560. std::uint64_t instanceSoid,
  561. std::uint32_t definitionIndex,
  562. std::uint32_t definitionHash,
  563. std::uint32_t quantity) noexcept {
  564. std::array<char, core::log::kLineCapacity> line{};
  565. const int count = std::snprintf(
  566. line.data(),
  567. line.size(),
  568. "ev=ws402 stage=prepare result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
  569. "instance=0x%llX definition_index=%u definition_hash=0x%08X quantity=%u",
  570. static_cast<int>(result.size()),
  571. result.data(),
  572. static_cast<int>(reason.size()),
  573. reason.data(),
  574. static_cast<unsigned>(message.transactionId),
  575. message.payload.size(),
  576. static_cast<unsigned long long>(instanceSoid),
  577. definitionIndex,
  578. definitionHash,
  579. quantity);
  580. if (count > 0) {
  581. core::log::write(core::log::Channel::server,
  582. result == "ok" ? core::log::Level::debug : core::log::Level::warn,
  583. {line.data(), static_cast<std::size_t>(count)});
  584. }
  585. }
  586. /** Prepares the exact fixed-width opcode-402 Character-inventory removal request. */
  587. void dismantle_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
  588. middleware::web_service::messages::opcode402::Request request{};
  589. if (!middleware::web_service::messages::opcode402::parse_request(message, request)) {
  590. report_item_dismantle(
  591. message, "fail", "payload_bits", request.instanceSoid, request.definitionIndex, 0, 0);
  592. return;
  593. }
  594. const std::uint64_t instanceSoid = request.instanceSoid;
  595. const std::uint16_t definitionIndex = request.definitionIndex;
  596. // The codec owns the value; this alias keeps the dismantle checks below readable.
  597. constexpr std::uint32_t kSingleQuantity =
  598. middleware::web_service::messages::opcode402::kSingleQuantity;
  599. state::build_data::items::Definition definition{};
  600. if (!state::build_data::find_item_definition_index(definitionIndex, definition)) {
  601. report_item_dismantle(
  602. message, "fail", "definition", instanceSoid, definitionIndex, 0, kSingleQuantity);
  603. return;
  604. }
  605. state::PendingItemDismantle mutation{};
  606. if (!state::prepare_item_dismantle(instanceSoid, mutation)) {
  607. report_item_dismantle(message,
  608. "fail",
  609. "state",
  610. instanceSoid,
  611. definitionIndex,
  612. definition.definitionHash,
  613. kSingleQuantity);
  614. return;
  615. }
  616. if (mutation.dismantledItem.definitionHash != definition.definitionHash
  617. || mutation.dismantledItem.quantity != static_cast<std::int32_t>(kSingleQuantity)) {
  618. report_item_dismantle(message,
  619. "fail",
  620. "identity",
  621. instanceSoid,
  622. definitionIndex,
  623. definition.definitionHash,
  624. kSingleQuantity);
  625. return;
  626. }
  627. outcome.mutation = mutation;
  628. report_item_dismantle(message,
  629. "ok",
  630. "ready",
  631. instanceSoid,
  632. definitionIndex,
  633. definition.definitionHash,
  634. kSingleQuantity);
  635. }
  636. /** Records opcode-1801 Triumphs claim requests and the record row each one names. */
  637. void report_record_claim(const middleware::web_service::Message& message,
  638. std::string_view result,
  639. std::string_view reason,
  640. std::uint32_t recordIndex,
  641. std::uint32_t completionFlagIndex) noexcept {
  642. std::array<char, core::log::kLineCapacity> line{};
  643. const int count = std::snprintf(
  644. line.data(),
  645. line.size(),
  646. "ev=ws1801 stage=claim result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
  647. "record_index=%u completion_flag_index=%u",
  648. static_cast<int>(result.size()),
  649. result.data(),
  650. static_cast<int>(reason.size()),
  651. reason.data(),
  652. static_cast<unsigned>(message.transactionId),
  653. message.payload.size(),
  654. recordIndex,
  655. completionFlagIndex);
  656. if (count > 0) {
  657. core::log::write(core::log::Channel::server,
  658. result == "ok" ? core::log::Level::debug : core::log::Level::warn,
  659. {line.data(), static_cast<std::size_t>(count)});
  660. }
  661. }
  662. /** Records strict opcode-1820 parsing, installed mapping, and State preparation outcomes. */
  663. void report_item_acquisition(const middleware::web_service::Message& message,
  664. std::string_view result,
  665. std::string_view reason,
  666. std::uint32_t collectibleIndex,
  667. std::uint32_t itemDefinitionIndex,
  668. std::uint32_t definitionHash,
  669. std::uint64_t instanceSoid) noexcept {
  670. std::array<char, core::log::kLineCapacity> line{};
  671. const int count = std::snprintf(
  672. line.data(),
  673. line.size(),
  674. "ev=ws1820 stage=prepare result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
  675. "collectible_index=%u item_definition_index=%u definition_hash=0x%08X instance=0x%llX",
  676. static_cast<int>(result.size()),
  677. result.data(),
  678. static_cast<int>(reason.size()),
  679. reason.data(),
  680. static_cast<unsigned>(message.transactionId),
  681. message.payload.size(),
  682. collectibleIndex,
  683. itemDefinitionIndex,
  684. definitionHash,
  685. static_cast<unsigned long long>(instanceSoid));
  686. if (count > 0) {
  687. core::log::write(core::log::Channel::server,
  688. result == "ok" ? core::log::Level::debug : core::log::Level::warn,
  689. {line.data(), static_cast<std::size_t>(count)});
  690. }
  691. }
  692. /** Prepares the exact three-byte opcode-1820 Collections item request. */
  693. void acquire_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
  694. middleware::web_service::messages::opcode1820::Request request{};
  695. if (!middleware::web_service::messages::opcode1820::parse_request(message, request)) {
  696. report_item_acquisition(message,
  697. "fail",
  698. "payload_bits",
  699. kUnavailableDefinitionIndex,
  700. kUnavailableDefinitionIndex,
  701. 0,
  702. 0);
  703. return;
  704. }
  705. const std::uint16_t collectibleIndex = request.collectibleIndex;
  706. std::uint16_t itemDefinitionIndex = 0;
  707. if (!state::build_data::find_collectible_item_definition_index(collectibleIndex,
  708. itemDefinitionIndex)) {
  709. report_item_acquisition(message,
  710. "fail",
  711. "collectible_definition",
  712. collectibleIndex,
  713. kUnavailableDefinitionIndex,
  714. 0,
  715. 0);
  716. return;
  717. }
  718. state::build_data::items::Definition definition{};
  719. if (!state::build_data::find_item_definition_index(itemDefinitionIndex, definition)) {
  720. report_item_acquisition(
  721. message, "fail", "item_definition", collectibleIndex, itemDefinitionIndex, 0, 0);
  722. return;
  723. }
  724. state::build_data::items::details::Definition detail{};
  725. state::build_data::inventory::buckets::Descriptor bucket{};
  726. if (!state::build_data::find_configured_item_detail(itemDefinitionIndex, detail)
  727. || detail.definitionIndex != itemDefinitionIndex
  728. || detail.definitionHash != definition.definitionHash
  729. || detail.bucketId != definition.bucketId
  730. || !state::build_data::find_inventory_bucket_descriptor(detail.bucketId, bucket)) {
  731. report_item_acquisition(message,
  732. "fail",
  733. "item_detail_or_bucket",
  734. collectibleIndex,
  735. itemDefinitionIndex,
  736. definition.definitionHash,
  737. 0);
  738. return;
  739. }
  740. namespace bucket_domain = state::build_data::inventory::buckets;
  741. namespace detail_domain = state::build_data::items::details;
  742. if (bucket.arraySelector == bucket_domain::ArraySelector::profile) {
  743. if (detail.instancedDefinitionState != detail_domain::InstancedDefinitionState::stackable) {
  744. report_item_acquisition(message,
  745. "fail",
  746. "profile_item_instanced",
  747. collectibleIndex,
  748. itemDefinitionIndex,
  749. definition.definitionHash,
  750. 0);
  751. return;
  752. }
  753. state::PendingProfileItemAcquisition mutation{};
  754. if (!state::prepare_profile_item_acquisition(
  755. collectibleIndex, definition.definitionHash, mutation)) {
  756. report_item_acquisition(message,
  757. "fail",
  758. "profile_state",
  759. collectibleIndex,
  760. itemDefinitionIndex,
  761. definition.definitionHash,
  762. 0);
  763. return;
  764. }
  765. outcome.mutation = mutation;
  766. report_item_acquisition(message,
  767. "ok",
  768. "profile_ready",
  769. collectibleIndex,
  770. itemDefinitionIndex,
  771. definition.definitionHash,
  772. 0);
  773. return;
  774. }
  775. if (bucket.arraySelector != bucket_domain::ArraySelector::character) {
  776. report_item_acquisition(message,
  777. "fail",
  778. "unsupported_inventory_array",
  779. collectibleIndex,
  780. itemDefinitionIndex,
  781. definition.definitionHash,
  782. 0);
  783. return;
  784. }
  785. state::PendingItemAcquisition mutation{};
  786. if (!state::prepare_item_acquisition(collectibleIndex, definition.definitionHash, mutation)) {
  787. report_item_acquisition(message,
  788. "fail",
  789. "state",
  790. collectibleIndex,
  791. itemDefinitionIndex,
  792. definition.definitionHash,
  793. 0);
  794. return;
  795. }
  796. outcome.mutation = mutation;
  797. report_item_acquisition(message,
  798. "ok",
  799. "ready",
  800. collectibleIndex,
  801. itemDefinitionIndex,
  802. definition.definitionHash,
  803. mutation.acquiredInstanceSoid);
  804. }
  805. /** Decodes one opcode-1801 Triumphs claim and reports the record it names. */
  806. void claim_record(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
  807. // Deliberately no mutation. The shared reply path answers an action that prepares nothing with
  808. // the refusal status, so preparing a placeholder here would turn a silently-accepted claim into
  809. // an explicitly rejected one. Attach the transition here once claimed state is identified.
  810. (void)outcome;
  811. namespace records = state::build_data::records;
  812. middleware::web_service::messages::opcode1801::Request request{};
  813. if (!middleware::web_service::messages::opcode1801::parse_request(message, request)) {
  814. report_record_claim(message, "fail", "payload_bits", 0, records::kUnavailableFlagIndex);
  815. return;
  816. }
  817. records::Definition definition{};
  818. if (!state::build_data::find_record_definition(request.recordIndex, definition)) {
  819. report_record_claim(
  820. message, "fail", "record_definition", request.recordIndex,
  821. records::kUnavailableFlagIndex);
  822. return;
  823. }
  824. if (definition.completionFlagIndex == records::kUnavailableFlagIndex) {
  825. // The record carries no completion flag, or its slot has no row in the account bank.
  826. report_record_claim(
  827. message, "fail", "no_completion_flag", request.recordIndex,
  828. records::kUnavailableFlagIndex);
  829. return;
  830. }
  831. report_record_claim(
  832. message, "ok", "resolved", request.recordIndex, definition.completionFlagIndex);
  833. }
  834. } // namespace sunrise::server::web_service