queuez_deferred_push.cpp 35 KB

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