queuez_deferred_push.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <limits>
  4. #include <memory>
  5. #include <new>
  6. #include "../../../../core/logging/log.h"
  7. #include "../../../../middleware/secure_channel/runtime.h"
  8. #include "../../../../state/account/account_state.h"
  9. #include "../../../../state/activity/destination/definition.h"
  10. #include "../../../../state/activity/runtime.h"
  11. #include "../../../../state/build_data/runtime.h"
  12. #include "../../../../state/runtime/runtime.h"
  13. #include "../internal.h"
  14. #include "../push/activity/activity_keepalive_push.h"
  15. #include "queuez_reward_staging.h"
  16. #include "queuez_state_validation.h"
  17. #include "state/investment/store_internal.h"
  18. namespace sunrise::server::bap::encrypted {
  19. namespace {
  20. /** @return The peer's retained row overlay, or empty once its presentation hold has passed. */
  21. [[nodiscard]] std::span<const queuez::AcquisitionPresentationRow>
  22. active_acquisition_presentation_rows(const Session& session) noexcept {
  23. if (GetTickCount64() >= session.acquisitionPresentationUntilTick
  24. || session.acquisitionPresentationRowCount > session.acquisitionPresentationRows.size()) {
  25. return {};
  26. }
  27. return std::span(session.acquisitionPresentationRows)
  28. .first(session.acquisitionPresentationRowCount);
  29. }
  30. /** Drops the visual XP notification and republishes the account, which already holds the XP. */
  31. void drop_seasonal_experience_presentation(Session& session) noexcept {
  32. session.pendingSeasonalExperienceAmount = 0;
  33. session.pendingSeasonalExperienceMutationSerial = 0;
  34. bap::arm_account_resync_everywhere();
  35. core::log::write(core::log::Channel::server,
  36. core::log::Level::warn,
  37. "ev=season_xp stage=deferred_presentation result=drop");
  38. }
  39. /** @return The picked character, or null when the account is invalid or nothing is picked. */
  40. [[nodiscard]] const state::CharacterState*
  41. selected_character(const state::AccountState& account) noexcept {
  42. if (!state::account::valid(account)) {
  43. return nullptr;
  44. }
  45. for (std::size_t index = 0; index < account.characterCount; ++index) {
  46. if (account.characters[index].selected) {
  47. return &account.characters[index];
  48. }
  49. }
  50. return nullptr;
  51. }
  52. bool consume_world_record_reward(const WorldRewardRequest& request,
  53. Session& session,
  54. Scratch& scratch,
  55. std::span<std::byte> response,
  56. std::size_t& written,
  57. bool& touchesScratch) noexcept {
  58. const std::unique_ptr<state::PendingRecordRewardGrant> pending(
  59. new (std::nothrow) state::PendingRecordRewardGrant);
  60. const auto fail = [&](const char* reason) noexcept {
  61. report_reward_refusal("world_publish", request.itemDefinitionIndex, reason);
  62. return false;
  63. };
  64. const char* reason = "transaction";
  65. state::investment::store::Transaction transaction;
  66. if (!pending || !transaction.ready() || request.quantity <= 0
  67. || !state::prepare_item_reward(request.itemDefinitionIndex,
  68. static_cast<std::uint32_t>(request.quantity),
  69. *pending,
  70. &reason)) {
  71. return fail(reason);
  72. }
  73. queuez::RecordRewardGrant update{};
  74. if (!queuez::stage_record_reward_grant(session.queuez, *pending, update)) {
  75. return fail("inventory_stage");
  76. }
  77. touchesScratch = true;
  78. std::size_t framedSize = 0;
  79. if (!push::append_record_reward_notification(scratch,
  80. session.queuez,
  81. update,
  82. *pending,
  83. active_acquisition_presentation_rows(session),
  84. session.sessionKey,
  85. session.sendNonce,
  86. scratch.framed,
  87. framedSize)
  88. || framedSize == 0) {
  89. return fail("inventory_encode");
  90. }
  91. if (framedSize > response.size()) {
  92. return false;
  93. }
  94. if (!state::commit_record_reward(*pending)) {
  95. return fail("inventory_commit");
  96. }
  97. if (!bap::complete_world_reward(request.id) || !transaction.commit()) {
  98. return fail("queue_commit");
  99. }
  100. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  101. written = framedSize;
  102. middleware::secure_channel::advance_nonce(session.sendNonce);
  103. session.queuez = update.after;
  104. bap::arm_account_resync_elsewhere(session);
  105. bap::arm_acquisition_presentation_hold(session);
  106. return true;
  107. }
  108. /** Publishes and commits one character-inventory world reward. */
  109. [[nodiscard]] bool consume_world_item_acquisition(const WorldRewardRequest& request,
  110. Session& session,
  111. Scratch& scratch,
  112. std::span<std::byte> response,
  113. std::size_t& written,
  114. bool& touchesScratch) noexcept {
  115. state::build_data::items::Definition item{};
  116. if (!state::build_data::find_item_definition_index(request.itemDefinitionIndex, item)) {
  117. return false;
  118. }
  119. if (state::item_grant_route(request.itemDefinitionIndex) != state::ItemGrantRoute::quest) {
  120. return consume_world_record_reward(
  121. request, session, scratch, response, written, touchesScratch);
  122. }
  123. state::investment::store::Transaction transaction;
  124. if (!transaction.ready()) {
  125. return false;
  126. }
  127. state::PendingItemAcquisition pending{};
  128. if (!state::prepare_item_acquisition_for_item(request.itemDefinitionIndex, pending)) {
  129. core::log::write(core::log::Channel::server,
  130. core::log::Level::warn,
  131. "ev=queuez stage=world_acquisition result=fail reason=prepare");
  132. bap::settle_world_reward();
  133. return false;
  134. }
  135. touchesScratch = true;
  136. queuez::ItemAcquisition acquisition{};
  137. if (!queuez::stage_item_acquisition(session.queuez,
  138. pending.accountSoid,
  139. pending.characterSoid,
  140. pending.acquiredInstanceSoid,
  141. pending.updates_account(),
  142. acquisition)) {
  143. core::log::write(core::log::Channel::server,
  144. core::log::Level::warn,
  145. "ev=queuez stage=world_acquisition result=fail reason=stage");
  146. bap::settle_world_reward();
  147. return false;
  148. }
  149. auto nextSendNonce = session.sendNonce;
  150. std::size_t framedSize = 0;
  151. if (!push::append_item_acquisition_notification(scratch,
  152. acquisition,
  153. pending,
  154. active_acquisition_presentation_rows(session),
  155. session.sessionKey,
  156. nextSendNonce,
  157. scratch.framed,
  158. framedSize)
  159. || framedSize == 0 || framedSize > response.size()) {
  160. core::log::write(core::log::Channel::server,
  161. core::log::Level::warn,
  162. "ev=queuez stage=world_acquisition result=fail reason=encode");
  163. bap::settle_world_reward();
  164. return false;
  165. }
  166. if (!state::commit_item_acquisition(pending) || !bap::complete_world_reward(request.id)
  167. || !transaction.commit()) {
  168. core::log::write(core::log::Channel::server,
  169. core::log::Level::warn,
  170. "ev=queuez stage=world_acquisition result=fail reason=commit");
  171. bap::settle_world_reward();
  172. return false;
  173. }
  174. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  175. written = framedSize;
  176. middleware::secure_channel::advance_nonce(nextSendNonce);
  177. session.sendNonce = nextSendNonce;
  178. session.queuez = acquisition.after;
  179. bap::arm_account_resync_elsewhere(session);
  180. bap::arm_acquisition_presentation_hold(session);
  181. return true;
  182. }
  183. /** Publishes and commits one profile-inventory world reward. */
  184. [[nodiscard]] bool consume_world_profile_item_acquisition(const WorldRewardRequest& request,
  185. Session& session,
  186. Scratch& scratch,
  187. std::span<std::byte> response,
  188. std::size_t& written,
  189. bool& touchesScratch) noexcept {
  190. state::investment::store::Transaction transaction;
  191. if (!transaction.ready()) {
  192. return false;
  193. }
  194. state::PendingProfileItemAcquisition pending{};
  195. if (!state::prepare_profile_item_acquisition_for_item(
  196. request.itemDefinitionIndex, request.quantity, pending)) {
  197. core::log::write(core::log::Channel::server,
  198. core::log::Level::warn,
  199. "ev=queuez stage=world_profile_acquisition result=fail reason=prepare");
  200. bap::settle_world_reward();
  201. return false;
  202. }
  203. touchesScratch = true;
  204. queuez::ProfileItemAcquisition acquisition{};
  205. if (!queuez::stage_profile_item_acquisition(session.queuez,
  206. pending.accountSoid,
  207. pending.acquiredInstanceSoid,
  208. pending.actionSource,
  209. pending.appended,
  210. acquisition)) {
  211. core::log::write(core::log::Channel::server,
  212. core::log::Level::warn,
  213. "ev=queuez stage=world_profile_acquisition result=fail reason=stage");
  214. bap::settle_world_reward();
  215. return false;
  216. }
  217. auto nextSendNonce = session.sendNonce;
  218. std::size_t framedSize = 0;
  219. if (!push::append_profile_item_acquisition_notification(scratch,
  220. acquisition,
  221. pending,
  222. session.sessionKey,
  223. nextSendNonce,
  224. scratch.framed,
  225. framedSize)
  226. || framedSize == 0 || framedSize > response.size()) {
  227. core::log::write(core::log::Channel::server,
  228. core::log::Level::warn,
  229. "ev=queuez stage=world_profile_acquisition result=fail reason=encode");
  230. bap::settle_world_reward();
  231. return false;
  232. }
  233. if (!state::commit_profile_item_acquisition(pending) || !bap::complete_world_reward(request.id)
  234. || !transaction.commit()) {
  235. core::log::write(core::log::Channel::server,
  236. core::log::Level::warn,
  237. "ev=queuez stage=world_profile_acquisition result=fail reason=commit");
  238. bap::settle_world_reward();
  239. return false;
  240. }
  241. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  242. written = framedSize;
  243. middleware::secure_channel::advance_nonce(nextSendNonce);
  244. session.sendNonce = nextSendNonce;
  245. session.queuez = acquisition.after;
  246. bap::arm_account_resync_elsewhere(session);
  247. bap::arm_acquisition_presentation_hold(session);
  248. return true;
  249. }
  250. /** Publishes one non-persistent XP reward row so the native seasonal XP HUD animates. */
  251. [[nodiscard]] bool consume_seasonal_experience_presentation(Session& session,
  252. Scratch& scratch,
  253. std::span<std::byte> response,
  254. std::size_t& written,
  255. bool& touchesScratch) noexcept {
  256. if (session.pendingSeasonalExperienceAmount <= 0) {
  257. return false;
  258. }
  259. if (session.pendingSeasonalExperienceMutationSerial == 0) {
  260. std::int32_t mutationSerial = 0;
  261. // The row belongs to the picked character, so the gain waits for a pick.
  262. if (!state::reserve_selected_character_inventory_serial(mutationSerial)) {
  263. return false;
  264. }
  265. session.pendingSeasonalExperienceMutationSerial =
  266. static_cast<std::uint32_t>(mutationSerial) + 1U;
  267. }
  268. touchesScratch = true;
  269. auto nextSendNonce = session.sendNonce;
  270. std::size_t framedSize = 0;
  271. queuez::SessionState after{};
  272. if (!push::append_seasonal_experience_notification(
  273. scratch,
  274. session.queuez,
  275. session.pendingSeasonalExperienceAmount,
  276. static_cast<std::int32_t>(session.pendingSeasonalExperienceMutationSerial - 1U),
  277. active_acquisition_presentation_rows(session),
  278. session.sessionKey,
  279. nextSendNonce,
  280. scratch.framed,
  281. framedSize,
  282. after)
  283. || framedSize == 0 || framedSize > response.size()) {
  284. drop_seasonal_experience_presentation(session);
  285. return false;
  286. }
  287. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  288. written = framedSize;
  289. middleware::secure_channel::advance_nonce(nextSendNonce);
  290. session.sendNonce = nextSendNonce;
  291. session.queuez = after;
  292. session.pendingSeasonalExperienceAmount = 0;
  293. session.pendingSeasonalExperienceMutationSerial = 0;
  294. bap::arm_account_resync_elsewhere(session);
  295. return true;
  296. }
  297. /** Publishes the current account graph to a peer invalidated by another connection. */
  298. [[nodiscard]] bool consume_account_resync(Session& session,
  299. Scratch& scratch,
  300. std::span<std::byte> response,
  301. std::size_t& written,
  302. bool& touchesScratch) noexcept {
  303. if (!session.accountResyncArmed) {
  304. return false;
  305. }
  306. touchesScratch = true;
  307. auto nextSendNonce = session.sendNonce;
  308. std::size_t framedSize = 0;
  309. queuez::SessionState currentQueuez{};
  310. if (!push::append_account_resync_notification(scratch,
  311. session.queuez,
  312. active_acquisition_presentation_rows(session),
  313. session.sessionKey,
  314. nextSendNonce,
  315. scratch.framed,
  316. framedSize,
  317. currentQueuez)) {
  318. core::log::write(core::log::Channel::server,
  319. core::log::Level::warn,
  320. "ev=queuez stage=peer_resync result=fail reason=family4");
  321. return false;
  322. }
  323. bool auxiliaryRefreshFailed = false;
  324. if (currentQueuez.family0Active) {
  325. queuez::SessionState appearanceAfter{};
  326. if (!push::append_account_resync_appearance_notification(scratch,
  327. currentQueuez,
  328. session.sessionKey,
  329. nextSendNonce,
  330. scratch.framed,
  331. framedSize,
  332. appearanceAfter)) {
  333. core::log::write(core::log::Channel::server,
  334. core::log::Level::warn,
  335. "ev=queuez stage=peer_resync result=fail reason=family0");
  336. auxiliaryRefreshFailed = true;
  337. } else {
  338. currentQueuez = appearanceAfter;
  339. }
  340. }
  341. if (currentQueuez.family3Active) {
  342. queuez::SessionState rosterAfter{};
  343. if (!push::append_account_resync_roster_notification(scratch,
  344. currentQueuez,
  345. session.sessionKey,
  346. nextSendNonce,
  347. scratch.framed,
  348. framedSize,
  349. rosterAfter)) {
  350. core::log::write(core::log::Channel::server,
  351. core::log::Level::warn,
  352. "ev=queuez stage=peer_resync result=fail reason=family3");
  353. auxiliaryRefreshFailed = true;
  354. } else {
  355. currentQueuez = rosterAfter;
  356. }
  357. }
  358. if (framedSize == 0 || framedSize > response.size() || !queuez::valid(currentQueuez)) {
  359. core::log::write(core::log::Channel::server,
  360. core::log::Level::warn,
  361. "ev=queuez stage=peer_resync result=fail reason=output");
  362. return false;
  363. }
  364. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  365. written = framedSize;
  366. session.sendNonce = nextSendNonce;
  367. session.queuez = currentQueuez;
  368. session.accountResyncArmed = false;
  369. if (auxiliaryRefreshFailed) {
  370. // Family 4 already produced a complete frame. Appearance and roster are derived views,
  371. // so they retry in their own deferred lane rather than holding the account update.
  372. session.abilityRefreshDueTick = GetTickCount64();
  373. session.characterRefreshScope = CharacterRefreshScope::recordsAndRoster;
  374. }
  375. return true;
  376. }
  377. /** Sends the banner pair the launch cinematic hold armed. */
  378. [[nodiscard]] bool consume_banner_repush(Session& session,
  379. Scratch& scratch,
  380. std::span<std::byte> response,
  381. std::size_t& written,
  382. bool& touchesScratch) noexcept {
  383. if (!session.bannerRepushArmed || session.bannerRepushRoot == 0
  384. || GetTickCount64() < session.bannerRepushDueTick) {
  385. return false;
  386. }
  387. // Retain the arm until the account has a character to name.
  388. if (state::account::banner_character_soid(state::account_snapshot()) == 0) {
  389. return false;
  390. }
  391. touchesScratch = true;
  392. // Reuse the subscription path so its version and the host mirror stay aligned.
  393. middleware::queuez::Subscription subscription{};
  394. subscription.familyType = queuez::kBannerFamilyType;
  395. subscription.familyRootSoid = session.bannerRepushRoot;
  396. auto nextSendNonce = session.sendNonce;
  397. std::size_t framedSize = 0;
  398. queuez::SessionState bannerAfter{};
  399. bool armsRepush = false;
  400. push::append_queuez_notification(scratch,
  401. session.queuez,
  402. subscription,
  403. session.sessionKey,
  404. nextSendNonce,
  405. scratch.framed,
  406. framedSize,
  407. bannerAfter,
  408. armsRepush);
  409. if (framedSize == 0 || framedSize > response.size()) {
  410. core::log::write(core::log::Channel::server,
  411. core::log::Level::warn,
  412. "ev=queuez stage=banner_repush result=fail");
  413. return false;
  414. }
  415. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  416. written = framedSize;
  417. session.sendNonce = nextSendNonce;
  418. // The frame is committed here, so the recorded delivery and the arm are committed with it.
  419. if (valid(bannerAfter)) {
  420. session.queuez = bannerAfter;
  421. }
  422. session.bannerRepushArmed = false;
  423. return true;
  424. }
  425. /**
  426. * Sends the family-two re-push the equip that moved the member record owes.
  427. * The body is rebuilt against the subscribe's root. The arm is spent before the frame is built.
  428. * @param session Auth, nonce and queuez state owned by the connection.
  429. * @param scratch Transform buffers owned by the lock.
  430. * @param response Whole-frame storage owned by the caller.
  431. * @param written Gets the encoded notification size in bytes.
  432. * @param touchesScratch Set before any scratch buffer is used.
  433. * @return True when a whole family-two notification is published.
  434. */
  435. [[nodiscard]] bool consume_social_roster_repush(Session& session,
  436. Scratch& scratch,
  437. std::span<std::byte> response,
  438. std::size_t& written,
  439. bool& touchesScratch) noexcept {
  440. if (!session.socialRosterRepushArmed || session.socialRosterRepushRoot == 0) {
  441. return false;
  442. }
  443. // Spent up front, so no path below can leave it owed.
  444. session.socialRosterRepushArmed = false;
  445. touchesScratch = true;
  446. // The same body the subscribe answer builds, rebuilt against current State so the emblem it
  447. // carries is the one now worn.
  448. middleware::queuez::Subscription subscription{};
  449. subscription.familyType = queuez::kSocialRosterFamilyType;
  450. subscription.familyRootSoid = session.socialRosterRepushRoot;
  451. auto nextSendNonce = session.sendNonce;
  452. std::size_t framedSize = 0;
  453. queuez::SessionState rosterAfter{};
  454. bool armsRepush = false;
  455. push::append_queuez_notification(scratch,
  456. session.queuez,
  457. subscription,
  458. session.sessionKey,
  459. nextSendNonce,
  460. scratch.framed,
  461. framedSize,
  462. rosterAfter,
  463. armsRepush);
  464. if (framedSize == 0 || framedSize > response.size()) {
  465. core::log::write(core::log::Channel::server,
  466. core::log::Level::warn,
  467. "ev=queuez stage=social_roster_repush result=fail");
  468. return false;
  469. }
  470. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  471. written = framedSize;
  472. session.sendNonce = nextSendNonce;
  473. if (valid(rosterAfter)) {
  474. session.queuez = rosterAfter;
  475. }
  476. return true;
  477. }
  478. /** Refreshes character records after an asynchronous ability-bucket rebuild. */
  479. [[nodiscard]] bool consume_ability_refresh(Session& session,
  480. Scratch& scratch,
  481. std::span<std::byte> response,
  482. std::size_t& written,
  483. bool& touchesScratch) noexcept {
  484. if (session.characterRefreshScope == CharacterRefreshScope::none
  485. || GetTickCount64() < session.abilityRefreshDueTick) {
  486. return false;
  487. }
  488. // Retain the arm until a family that reads abilities is active.
  489. if (!session.queuez.family0Active && !session.queuez.family3Active) {
  490. return false;
  491. }
  492. touchesScratch = true;
  493. auto nextSendNonce = session.sendNonce;
  494. std::size_t framedSize = 0;
  495. queuez::SessionState current = session.queuez;
  496. bool wrote = false;
  497. if (current.family0Active) {
  498. queuez::SessionState appearanceAfter{};
  499. if (!push::append_account_resync_appearance_notification(scratch,
  500. current,
  501. session.sessionKey,
  502. nextSendNonce,
  503. scratch.framed,
  504. framedSize,
  505. appearanceAfter)) {
  506. return false;
  507. }
  508. current = appearanceAfter;
  509. wrote = true;
  510. }
  511. if (current.family3Active) {
  512. queuez::SessionState rosterAfter{};
  513. if (!push::append_account_resync_roster_notification(
  514. scratch,
  515. current,
  516. session.sessionKey,
  517. nextSendNonce,
  518. scratch.framed,
  519. framedSize,
  520. rosterAfter,
  521. session.characterRefreshScope == CharacterRefreshScope::recordsAndRoster)) {
  522. return false;
  523. }
  524. current = rosterAfter;
  525. wrote = true;
  526. }
  527. if (!wrote || framedSize == 0 || framedSize > response.size() || !queuez::valid(current)) {
  528. core::log::write(core::log::Channel::server,
  529. core::log::Level::warn,
  530. "ev=queuez stage=ability_refresh result=fail");
  531. return false;
  532. }
  533. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  534. written = framedSize;
  535. session.sendNonce = nextSendNonce;
  536. session.queuez = current;
  537. // Clear the arm only after publication.
  538. session.characterRefreshScope = CharacterRefreshScope::none;
  539. return true;
  540. }
  541. /**
  542. * Publishes the account unlock overrides an artifact purchase or reset changed.
  543. * The arm is spent before the frame is built, so one committed change owes exactly one frame.
  544. * @param session Auth, nonce and queuez state owned by the connection.
  545. * @param scratch Transform buffers owned by the lock.
  546. * @param response Whole-frame storage owned by the caller.
  547. * @param written Gets the encoded notification size in bytes.
  548. * @param touchesScratch Set before any scratch buffer is used.
  549. * @return True when the family-five snapshot is published.
  550. */
  551. [[nodiscard]] bool consume_artifact_family5_refresh(Session& session,
  552. Scratch& scratch,
  553. std::span<std::byte> response,
  554. std::size_t& written,
  555. bool& touchesScratch) noexcept {
  556. if (!session.artifactRefreshArmed) {
  557. return false;
  558. }
  559. session.artifactRefreshArmed = false;
  560. if (session.queuez.family5Version == (std::numeric_limits<std::int32_t>::max)()) {
  561. core::log::write(core::log::Channel::server,
  562. core::log::Level::warn,
  563. "ev=queuez stage=family5_refresh result=fail reason=version");
  564. return false;
  565. }
  566. touchesScratch = true;
  567. const std::int32_t version = session.queuez.family5Version + 1;
  568. auto nextSendNonce = session.sendNonce;
  569. std::size_t framedSize = 0;
  570. if (!push::append_family5_override_notification(
  571. scratch, version, session.sessionKey, nextSendNonce, scratch.framed, framedSize)
  572. || framedSize == 0 || framedSize > response.size()) {
  573. core::log::write(core::log::Channel::server,
  574. core::log::Level::warn,
  575. "ev=queuez stage=family5_refresh result=fail reason=frame");
  576. return false;
  577. }
  578. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  579. written = framedSize;
  580. middleware::secure_channel::advance_nonce(nextSendNonce);
  581. session.sendNonce = nextSendNonce;
  582. session.queuez.family5Version = version;
  583. // The client rebuilds its derived unlock state on the family-4 update that follows this.
  584. return true;
  585. }
  586. /** Re-publishes only the selected character after an artifact purchase. */
  587. [[nodiscard]] bool consume_artifact_family4_refresh(Session& session,
  588. Scratch& scratch,
  589. std::span<std::byte> response,
  590. std::size_t& written,
  591. bool& touchesScratch) noexcept {
  592. if (!session.artifactFamily4RefreshArmed
  593. || GetTickCount64() < session.artifactFamily4RefreshDueTick) {
  594. return false;
  595. }
  596. const state::AccountState account = state::account_snapshot();
  597. const state::CharacterState* selected = selected_character(account);
  598. if (selected == nullptr) {
  599. return false;
  600. }
  601. state::PendingArtifactPurchase refresh{};
  602. refresh.accountSoid = account.primarySoid;
  603. refresh.characterSoid = selected->soid;
  604. refresh.characterIndex = static_cast<std::size_t>(selected - account.characters.data());
  605. refresh.beforeMask = state::artifact_mod_mask();
  606. refresh.afterMask = refresh.beforeMask;
  607. refresh.prepared = true;
  608. queuez::EquipmentSwap update{};
  609. auto nextSendNonce = session.sendNonce;
  610. std::size_t framedSize = 0;
  611. touchesScratch = true;
  612. const auto presentationRows = active_acquisition_presentation_rows(session);
  613. if (!queuez::stage_equipment_swap(session.queuez, refresh.characterSoid, update)
  614. || !push::append_artifact_purchase_notification(scratch,
  615. update,
  616. refresh,
  617. presentationRows,
  618. session.sessionKey,
  619. nextSendNonce,
  620. scratch.framed,
  621. framedSize)
  622. || framedSize == 0 || framedSize > response.size()) {
  623. core::log::write(core::log::Channel::server,
  624. core::log::Level::warn,
  625. "ev=queuez stage=artifact_refresh result=fail");
  626. return false;
  627. }
  628. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  629. written = framedSize;
  630. middleware::secure_channel::advance_nonce(nextSendNonce);
  631. session.sendNonce = nextSendNonce;
  632. session.queuez = update.after;
  633. session.artifactFamily4RefreshArmed = false;
  634. session.artifactFamily4RefreshDueTick = 0;
  635. return true;
  636. }
  637. /** Publishes one reset-affected item resident per poll using the proven socket-update shape. */
  638. [[nodiscard]] bool consume_artifact_item_refresh(Session& session,
  639. Scratch& scratch,
  640. std::span<std::byte> response,
  641. std::size_t& written,
  642. bool& touchesScratch) noexcept {
  643. if (session.artifactResetRefreshCursor >= session.artifactResetRefresh.instanceCount) {
  644. session.artifactResetRefresh = {};
  645. session.artifactResetRefreshCursor = 0;
  646. return false;
  647. }
  648. const state::AccountState account = state::account_snapshot();
  649. const state::CharacterState* selected = selected_character(account);
  650. if (selected == nullptr) {
  651. return false;
  652. }
  653. const std::uint64_t instanceSoid =
  654. session.artifactResetRefresh.instanceSoids[session.artifactResetRefreshCursor];
  655. queuez::EquipmentSwap update{};
  656. auto nextSendNonce = session.sendNonce;
  657. std::size_t framedSize = 0;
  658. touchesScratch = true;
  659. if (!queuez::stage_equipment_swap(session.queuez, selected->soid, update)
  660. || !push::append_artifact_item_refresh_notification(scratch,
  661. update,
  662. instanceSoid,
  663. session.sessionKey,
  664. nextSendNonce,
  665. scratch.framed,
  666. framedSize)
  667. || framedSize == 0 || framedSize > response.size()) {
  668. core::log::write(core::log::Channel::server,
  669. core::log::Level::warn,
  670. "ev=queuez stage=artifact_item_refresh result=fail");
  671. return false;
  672. }
  673. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  674. written = framedSize;
  675. middleware::secure_channel::advance_nonce(nextSendNonce);
  676. session.sendNonce = nextSendNonce;
  677. session.queuez = update.after;
  678. ++session.artifactResetRefreshCursor;
  679. if (session.artifactResetRefreshCursor >= session.artifactResetRefresh.instanceCount) {
  680. // Equipped sockets feed Family 0/3's derived perk banks. Refresh them once every
  681. // changed item resident has landed so reset cannot leave the previous champion effect
  682. // cached.
  683. session.abilityRefreshDueTick = GetTickCount64();
  684. if (session.characterRefreshScope == CharacterRefreshScope::none) {
  685. session.characterRefreshScope = CharacterRefreshScope::records;
  686. }
  687. }
  688. return true;
  689. }
  690. } // namespace
  691. /** Publishes the next due reward, refresh, retry, or keepalive. */
  692. bool consume_deferred(Session& session,
  693. Scratch& scratch,
  694. std::span<std::byte> response,
  695. std::size_t& written,
  696. bool& touchesScratch) noexcept {
  697. written = 0;
  698. if (!session.authenticated) {
  699. return false;
  700. }
  701. // The overrides go first: they are what the purchased mod unlocks, and the Family-4
  702. // companion waits on its own delay.
  703. if (consume_artifact_family5_refresh(session, scratch, response, written, touchesScratch)) {
  704. return true;
  705. }
  706. if (consume_artifact_family4_refresh(session, scratch, response, written, touchesScratch)) {
  707. return true;
  708. }
  709. if (consume_artifact_item_refresh(session, scratch, response, written, touchesScratch)) {
  710. return true;
  711. }
  712. if (consume_account_resync(session, scratch, response, written, touchesScratch)) {
  713. return true;
  714. }
  715. // A failed resync blocks every incremental that could depend on its missing objects.
  716. if (session.accountResyncArmed) {
  717. return false;
  718. }
  719. WorldRewardRequest reward{};
  720. if (session.queuez.family4Active && bap::current_world_reward(reward)) {
  721. bool published = false;
  722. switch (reward.kind) {
  723. case WorldRewardKind::item:
  724. published = consume_world_item_acquisition(
  725. reward, session, scratch, response, written, touchesScratch);
  726. break;
  727. case WorldRewardKind::profileItem:
  728. published = consume_world_profile_item_acquisition(
  729. reward, session, scratch, response, written, touchesScratch);
  730. break;
  731. }
  732. if (published) {
  733. return true;
  734. }
  735. }
  736. if (consume_seasonal_experience_presentation(
  737. session, scratch, response, written, touchesScratch)) {
  738. return true;
  739. }
  740. if (consume_ability_refresh(session, scratch, response, written, touchesScratch)) {
  741. return true;
  742. }
  743. if (!session.family4RepushArmed || session.family4RepushRoot == 0
  744. || GetTickCount64() < session.family4RepushDueTick
  745. || GetTickCount64() < session.acquisitionPresentationUntilTick) {
  746. return consume_social_roster_repush(session, scratch, response, written, touchesScratch)
  747. || consume_banner_repush(session, scratch, response, written, touchesScratch)
  748. || push::activity::consume_activity_keepalive(
  749. session, scratch, response, written, touchesScratch);
  750. }
  751. // One attempt is owed, and it is spent whether or not it lands.
  752. touchesScratch = true;
  753. middleware::queuez::Subscription subscription{};
  754. subscription.familyType = queuez::kAccountFamilyType;
  755. subscription.familyRootSoid = session.family4RepushRoot;
  756. auto nextSendNonce = session.sendNonce;
  757. std::size_t framedSize = 0;
  758. queuez::SessionState after{};
  759. bool armsRepush = false;
  760. push::append_queuez_notification(scratch,
  761. session.queuez,
  762. subscription,
  763. session.sessionKey,
  764. nextSendNonce,
  765. scratch.framed,
  766. framedSize,
  767. after,
  768. armsRepush);
  769. if (framedSize == 0 || framedSize > response.size()) {
  770. // Neither failure clears on a retry. Holding the arm starves the keepalive, and the
  771. // client drops the activity session once the keepalive stops.
  772. session.family4RepushArmed = false;
  773. core::log::write(core::log::Channel::server,
  774. core::log::Level::warn,
  775. framedSize == 0 ? "ev=queuez stage=repush result=fail reason=encode"
  776. : "ev=queuez stage=repush result=fail reason=capacity");
  777. return false;
  778. }
  779. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  780. written = framedSize;
  781. session.sendNonce = nextSendNonce;
  782. if (queuez::valid(after)) {
  783. session.queuez = after;
  784. }
  785. session.family4RepushArmed = false;
  786. return true;
  787. }
  788. } // namespace sunrise::server::bap::encrypted