queuez_deferred_push.cpp 39 KB

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