queuez_deferred_push.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include "../../../../core/logging/log.h"
  4. #include "../../../../middleware/secure_channel/runtime.h"
  5. #include "../../../../state/account/account_state.h"
  6. #include "../../../../state/progression/seasonal_experience.h"
  7. #include "../../../../state/runtime/runtime.h"
  8. #include "../internal.h"
  9. #include "../push/activity/activity_keepalive_push.h"
  10. #include "queuez_state_validation.h"
  11. namespace sunrise::server::bap::encrypted {
  12. namespace {
  13. constexpr std::uint8_t kSeasonalExperiencePresentationFailureLimit = 8;
  14. [[nodiscard]] std::span<const queuez::AcquisitionPresentationRow>
  15. active_acquisition_presentation_rows(const Session& session) noexcept {
  16. if (GetTickCount64() >= session.acquisitionPresentationUntilTick
  17. || session.acquisitionPresentationRowCount > session.acquisitionPresentationRows.size()) {
  18. return {};
  19. }
  20. return std::span(session.acquisitionPresentationRows)
  21. .first(session.acquisitionPresentationRowCount);
  22. }
  23. /** Drops only the visual XP notification after repeated failures; the XP is already durable. */
  24. void fail_seasonal_experience_presentation(Session& session) noexcept {
  25. if (++session.pendingSeasonalExperienceFailures < kSeasonalExperiencePresentationFailureLimit) {
  26. return;
  27. }
  28. session.pendingSeasonalExperienceAmount = 0;
  29. session.pendingSeasonalExperienceMutationSerial = 0;
  30. session.pendingSeasonalExperienceFailures = 0;
  31. bap::arm_account_resync_everywhere();
  32. core::log::write(core::log::Channel::server,
  33. core::log::Level::warn,
  34. "ev=season_xp stage=deferred_presentation result=drop reason=retry_limit");
  35. }
  36. /** Publishes and commits one character-inventory world reward. */
  37. [[nodiscard]] bool consume_world_item_acquisition(const WorldRewardRequest& request,
  38. Session& session,
  39. Scratch& scratch,
  40. std::span<std::byte> response,
  41. std::size_t& written,
  42. bool& touchesScratch) noexcept {
  43. state::PendingItemAcquisition pending{};
  44. if (!state::prepare_item_acquisition_for_item(request.itemDefinitionIndex, pending)) {
  45. core::log::write(core::log::Channel::server,
  46. core::log::Level::warn,
  47. "ev=queuez stage=world_acquisition result=fail reason=prepare");
  48. bap::fail_world_reward_attempt();
  49. return false;
  50. }
  51. touchesScratch = true;
  52. queuez::ItemAcquisition acquisition{};
  53. if (!queuez::stage_item_acquisition(session.queuez,
  54. pending.accountSoid,
  55. pending.characterSoid,
  56. pending.acquiredInstanceSoid,
  57. pending.profileChanged,
  58. acquisition)) {
  59. core::log::write(core::log::Channel::server,
  60. core::log::Level::warn,
  61. "ev=queuez stage=world_acquisition result=fail reason=stage");
  62. bap::fail_world_reward_attempt();
  63. return false;
  64. }
  65. auto nextSendNonce = session.sendNonce;
  66. std::size_t framedSize = 0;
  67. if (!push::append_item_acquisition_notification(scratch,
  68. acquisition,
  69. pending,
  70. std::nullopt,
  71. active_acquisition_presentation_rows(session),
  72. state::bap().sessionKey,
  73. nextSendNonce,
  74. scratch.framed,
  75. framedSize)
  76. || framedSize == 0 || framedSize > response.size()) {
  77. core::log::write(core::log::Channel::server,
  78. core::log::Level::warn,
  79. "ev=queuez stage=world_acquisition result=fail reason=encode");
  80. bap::fail_world_reward_attempt();
  81. return false;
  82. }
  83. if (!state::commit_item_acquisition(pending)) {
  84. core::log::write(core::log::Channel::server,
  85. core::log::Level::warn,
  86. "ev=queuez stage=world_acquisition result=fail reason=commit");
  87. bap::fail_world_reward_attempt();
  88. return false;
  89. }
  90. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  91. written = framedSize;
  92. middleware::secure_channel::advance_nonce(nextSendNonce);
  93. session.sendNonce = nextSendNonce;
  94. session.queuez = acquisition.after;
  95. bap::complete_world_reward();
  96. bap::arm_account_resync_elsewhere(session);
  97. bap::arm_acquisition_presentation_hold(session);
  98. return true;
  99. }
  100. /** Publishes and commits one profile-inventory world reward. */
  101. [[nodiscard]] bool consume_world_profile_item_acquisition(const WorldRewardRequest& request,
  102. Session& session,
  103. Scratch& scratch,
  104. std::span<std::byte> response,
  105. std::size_t& written,
  106. bool& touchesScratch) noexcept {
  107. state::PendingProfileItemAcquisition pending{};
  108. if (!state::prepare_profile_item_acquisition_for_item(
  109. request.itemDefinitionIndex, request.quantity, pending)) {
  110. core::log::write(core::log::Channel::server,
  111. core::log::Level::warn,
  112. "ev=queuez stage=world_profile_acquisition result=fail reason=prepare");
  113. bap::fail_world_reward_attempt();
  114. return false;
  115. }
  116. touchesScratch = true;
  117. queuez::ProfileItemAcquisition acquisition{};
  118. if (!queuez::stage_profile_item_acquisition(session.queuez,
  119. pending.accountSoid,
  120. pending.acquiredInstanceSoid,
  121. pending.actionSource,
  122. pending.appended,
  123. acquisition)) {
  124. core::log::write(core::log::Channel::server,
  125. core::log::Level::warn,
  126. "ev=queuez stage=world_profile_acquisition result=fail reason=stage");
  127. bap::fail_world_reward_attempt();
  128. return false;
  129. }
  130. auto nextSendNonce = session.sendNonce;
  131. std::size_t framedSize = 0;
  132. if (!push::append_profile_item_acquisition_notification(scratch,
  133. acquisition,
  134. pending,
  135. std::nullopt,
  136. state::bap().sessionKey,
  137. nextSendNonce,
  138. scratch.framed,
  139. framedSize)
  140. || framedSize == 0 || framedSize > response.size()) {
  141. core::log::write(core::log::Channel::server,
  142. core::log::Level::warn,
  143. "ev=queuez stage=world_profile_acquisition result=fail reason=encode");
  144. bap::fail_world_reward_attempt();
  145. return false;
  146. }
  147. if (!state::commit_profile_item_acquisition(pending)) {
  148. core::log::write(core::log::Channel::server,
  149. core::log::Level::warn,
  150. "ev=queuez stage=world_profile_acquisition result=fail reason=commit");
  151. bap::fail_world_reward_attempt();
  152. return false;
  153. }
  154. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  155. written = framedSize;
  156. middleware::secure_channel::advance_nonce(nextSendNonce);
  157. session.sendNonce = nextSendNonce;
  158. session.queuez = acquisition.after;
  159. bap::complete_world_reward();
  160. bap::arm_account_resync_elsewhere(session);
  161. bap::arm_acquisition_presentation_hold(session);
  162. return true;
  163. }
  164. /** Publishes one non-persistent XP reward row so the native seasonal XP HUD animates. */
  165. [[nodiscard]] bool consume_seasonal_experience_presentation(Session& session,
  166. Scratch& scratch,
  167. std::span<std::byte> response,
  168. std::size_t& written,
  169. bool& touchesScratch) noexcept {
  170. if (session.pendingSeasonalExperienceAmount <= 0) {
  171. return false;
  172. }
  173. touchesScratch = true;
  174. if (session.pendingSeasonalExperienceMutationSerial == 0) {
  175. std::int32_t mutationSerial = 0;
  176. if (!state::reserve_selected_character_inventory_serial(mutationSerial)) {
  177. core::log::write(core::log::Channel::server,
  178. core::log::Level::warn,
  179. "ev=season_xp stage=deferred_presentation result=fail reason=serial");
  180. fail_seasonal_experience_presentation(session);
  181. return false;
  182. }
  183. session.pendingSeasonalExperienceMutationSerial =
  184. static_cast<std::uint32_t>(mutationSerial) + 1U;
  185. }
  186. auto nextSendNonce = session.sendNonce;
  187. std::size_t framedSize = 0;
  188. queuez::SessionState after{};
  189. if (!push::append_seasonal_experience_notification(
  190. scratch,
  191. session.queuez,
  192. session.pendingSeasonalExperienceAmount,
  193. static_cast<std::int32_t>(session.pendingSeasonalExperienceMutationSerial - 1U),
  194. active_acquisition_presentation_rows(session),
  195. state::bap().sessionKey,
  196. nextSendNonce,
  197. scratch.framed,
  198. framedSize,
  199. after)
  200. || framedSize == 0 || framedSize > response.size()) {
  201. core::log::write(core::log::Channel::server,
  202. core::log::Level::warn,
  203. "ev=season_xp stage=deferred_presentation result=fail");
  204. fail_seasonal_experience_presentation(session);
  205. return false;
  206. }
  207. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  208. written = framedSize;
  209. middleware::secure_channel::advance_nonce(nextSendNonce);
  210. session.sendNonce = nextSendNonce;
  211. session.queuez = after;
  212. session.pendingSeasonalExperienceAmount = 0;
  213. session.pendingSeasonalExperienceMutationSerial = 0;
  214. session.pendingSeasonalExperienceFailures = 0;
  215. bap::arm_account_resync_elsewhere(session);
  216. return true;
  217. }
  218. /** Publishes the current account graph to a peer invalidated by another connection. */
  219. [[nodiscard]] bool consume_account_resync(Session& session,
  220. Scratch& scratch,
  221. std::span<std::byte> response,
  222. std::size_t& written,
  223. bool& touchesScratch) noexcept {
  224. if (!session.accountResyncArmed) {
  225. return false;
  226. }
  227. touchesScratch = true;
  228. auto nextSendNonce = session.sendNonce;
  229. std::size_t framedSize = 0;
  230. queuez::SessionState currentQueuez{};
  231. if (!push::append_account_resync_notification(scratch,
  232. session.queuez,
  233. active_acquisition_presentation_rows(session),
  234. state::bap().sessionKey,
  235. nextSendNonce,
  236. scratch.framed,
  237. framedSize,
  238. currentQueuez)) {
  239. core::log::write(core::log::Channel::server,
  240. core::log::Level::warn,
  241. "ev=queuez stage=peer_resync result=fail reason=family4");
  242. return false;
  243. }
  244. if (currentQueuez.family0Active) {
  245. queuez::SessionState appearanceAfter{};
  246. if (!push::append_account_resync_appearance_notification(scratch,
  247. currentQueuez,
  248. state::bap().sessionKey,
  249. nextSendNonce,
  250. scratch.framed,
  251. framedSize,
  252. appearanceAfter)) {
  253. core::log::write(core::log::Channel::server,
  254. core::log::Level::warn,
  255. "ev=queuez stage=peer_resync result=fail reason=family0");
  256. return false;
  257. }
  258. currentQueuez = appearanceAfter;
  259. }
  260. if (currentQueuez.family3Active) {
  261. queuez::SessionState rosterAfter{};
  262. if (!push::append_account_resync_roster_notification(scratch,
  263. currentQueuez,
  264. state::bap().sessionKey,
  265. nextSendNonce,
  266. scratch.framed,
  267. framedSize,
  268. rosterAfter)) {
  269. core::log::write(core::log::Channel::server,
  270. core::log::Level::warn,
  271. "ev=queuez stage=peer_resync result=fail reason=family3");
  272. return false;
  273. }
  274. currentQueuez = rosterAfter;
  275. }
  276. if (framedSize == 0 || framedSize > response.size() || !queuez::valid(currentQueuez)) {
  277. core::log::write(core::log::Channel::server,
  278. core::log::Level::warn,
  279. "ev=queuez stage=peer_resync result=fail reason=output");
  280. return false;
  281. }
  282. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  283. written = framedSize;
  284. session.sendNonce = nextSendNonce;
  285. session.queuez = currentQueuez;
  286. session.accountResyncArmed = false;
  287. return true;
  288. }
  289. /** Sends the owed banner retry after its delay. */
  290. [[nodiscard]] bool consume_banner_repush(Session& session,
  291. Scratch& scratch,
  292. std::span<std::byte> response,
  293. std::size_t& written,
  294. bool& touchesScratch) noexcept {
  295. if (!session.bannerRepushArmed || session.bannerRepushRoot == 0
  296. || GetTickCount64() < session.bannerRepushDueTick) {
  297. return false;
  298. }
  299. // Retain the arm until the account has a character to name.
  300. if (state::account::banner_character_soid(state::account_snapshot()) == 0) {
  301. return false;
  302. }
  303. touchesScratch = true;
  304. // Reuse the subscription path so its version and the host mirror stay aligned.
  305. middleware::queuez::Subscription subscription{};
  306. subscription.familyType = queuez::kBannerFamilyType;
  307. subscription.familyRootSoid = session.bannerRepushRoot;
  308. auto nextSendNonce = session.sendNonce;
  309. std::size_t framedSize = 0;
  310. queuez::SessionState bannerAfter{};
  311. bool armsRepush = false;
  312. bool armsBannerRepush = false;
  313. push::append_queuez_notification(scratch,
  314. session.queuez,
  315. subscription,
  316. state::bap().sessionKey,
  317. nextSendNonce,
  318. scratch.framed,
  319. framedSize,
  320. bannerAfter,
  321. armsRepush,
  322. armsBannerRepush);
  323. if (framedSize == 0 || framedSize > response.size()) {
  324. core::log::write(core::log::Channel::server,
  325. core::log::Level::warn,
  326. "ev=queuez stage=banner_repush result=fail");
  327. return false;
  328. }
  329. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  330. written = framedSize;
  331. session.sendNonce = nextSendNonce;
  332. // The frame is committed here, so the recorded delivery and the arm are committed with it.
  333. if (valid(bannerAfter)) {
  334. session.queuez = bannerAfter;
  335. }
  336. session.bannerRepushArmed = false;
  337. return true;
  338. }
  339. /** Refreshes appearance and roster after an asynchronous ability-bucket rebuild. */
  340. [[nodiscard]] bool consume_ability_refresh(Session& session,
  341. Scratch& scratch,
  342. std::span<std::byte> response,
  343. std::size_t& written,
  344. bool& touchesScratch) noexcept {
  345. if (!session.abilityRefreshArmed || GetTickCount64() < session.abilityRefreshDueTick) {
  346. return false;
  347. }
  348. // Retain the arm until a family that reads abilities is active.
  349. if (!session.queuez.family0Active && !session.queuez.family3Active) {
  350. return false;
  351. }
  352. touchesScratch = true;
  353. auto nextSendNonce = session.sendNonce;
  354. std::size_t framedSize = 0;
  355. queuez::SessionState current = session.queuez;
  356. bool wrote = false;
  357. if (current.family0Active) {
  358. queuez::SessionState appearanceAfter{};
  359. if (push::append_account_resync_appearance_notification(scratch,
  360. current,
  361. state::bap().sessionKey,
  362. nextSendNonce,
  363. scratch.framed,
  364. framedSize,
  365. appearanceAfter)) {
  366. current = appearanceAfter;
  367. wrote = true;
  368. }
  369. }
  370. if (current.family3Active) {
  371. queuez::SessionState rosterAfter{};
  372. if (push::append_account_resync_roster_notification(scratch,
  373. current,
  374. state::bap().sessionKey,
  375. nextSendNonce,
  376. scratch.framed,
  377. framedSize,
  378. rosterAfter)) {
  379. current = rosterAfter;
  380. wrote = true;
  381. }
  382. }
  383. if (!wrote || framedSize == 0 || framedSize > response.size() || !queuez::valid(current)) {
  384. core::log::write(core::log::Channel::server,
  385. core::log::Level::warn,
  386. "ev=queuez stage=ability_refresh result=fail");
  387. return false;
  388. }
  389. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  390. written = framedSize;
  391. session.sendNonce = nextSendNonce;
  392. session.queuez = current;
  393. // Clear the arm only after publication.
  394. session.abilityRefreshArmed = false;
  395. return true;
  396. }
  397. /** Re-publishes only the selected character after an artifact purchase. */
  398. [[nodiscard]] bool consume_artifact_family4_refresh(Session& session,
  399. Scratch& scratch,
  400. std::span<std::byte> response,
  401. std::size_t& written,
  402. bool& touchesScratch) noexcept {
  403. if (!session.artifactFamily4RefreshArmed
  404. || GetTickCount64() < session.artifactFamily4RefreshDueTick) {
  405. return false;
  406. }
  407. const state::AccountState account = state::account_snapshot();
  408. std::size_t selected = account.characterCount;
  409. for (std::size_t index = 0; index < account.characterCount; ++index) {
  410. if (account.characters[index].selected) {
  411. selected = index;
  412. break;
  413. }
  414. }
  415. if (!state::account::valid(account) || selected >= account.characterCount) {
  416. return false;
  417. }
  418. state::PendingArtifactPurchase refresh{};
  419. refresh.accountSoid = account.primarySoid;
  420. refresh.characterSoid = account.characters[selected].soid;
  421. refresh.characterIndex = selected;
  422. refresh.beforeMask = state::progression::seasonal_experience::artifact_mod_mask();
  423. refresh.afterMask = refresh.beforeMask;
  424. refresh.prepared = true;
  425. queuez::EquipmentSwap update{};
  426. auto nextSendNonce = session.sendNonce;
  427. std::size_t framedSize = 0;
  428. touchesScratch = true;
  429. if (!queuez::stage_equipment_swap(session.queuez, refresh.characterSoid, update)
  430. || !push::append_artifact_purchase_notification(scratch,
  431. update,
  432. refresh,
  433. active_acquisition_presentation_rows(session),
  434. state::bap().sessionKey,
  435. nextSendNonce,
  436. scratch.framed,
  437. framedSize)
  438. || framedSize == 0 || framedSize > response.size()) {
  439. core::log::write(core::log::Channel::server,
  440. core::log::Level::warn,
  441. "ev=queuez stage=artifact_refresh result=fail");
  442. return false;
  443. }
  444. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  445. written = framedSize;
  446. middleware::secure_channel::advance_nonce(nextSendNonce);
  447. session.sendNonce = nextSendNonce;
  448. session.queuez = update.after;
  449. session.artifactFamily4RefreshArmed = false;
  450. session.artifactFamily4RefreshDueTick = 0;
  451. return true;
  452. }
  453. /** Publishes one reset-affected item resident per poll using the proven socket-update shape. */
  454. [[nodiscard]] bool consume_artifact_item_refresh(Session& session,
  455. Scratch& scratch,
  456. std::span<std::byte> response,
  457. std::size_t& written,
  458. bool& touchesScratch) noexcept {
  459. if (session.artifactResetRefreshCursor >= session.artifactResetRefresh.instanceCount) {
  460. session.artifactResetRefresh = {};
  461. session.artifactResetRefreshCursor = 0;
  462. return false;
  463. }
  464. const state::AccountState account = state::account_snapshot();
  465. std::size_t selected = account.characterCount;
  466. for (std::size_t index = 0; index < account.characterCount; ++index) {
  467. if (account.characters[index].selected) {
  468. selected = index;
  469. break;
  470. }
  471. }
  472. if (!state::account::valid(account) || selected >= account.characterCount) {
  473. return false;
  474. }
  475. const std::uint64_t instanceSoid =
  476. session.artifactResetRefresh.instanceSoids[session.artifactResetRefreshCursor];
  477. queuez::EquipmentSwap update{};
  478. auto nextSendNonce = session.sendNonce;
  479. std::size_t framedSize = 0;
  480. touchesScratch = true;
  481. if (!queuez::stage_equipment_swap(
  482. session.queuez, account.characters[selected].soid, update)
  483. || !push::append_artifact_item_refresh_notification(scratch,
  484. update,
  485. instanceSoid,
  486. state::bap().sessionKey,
  487. nextSendNonce,
  488. scratch.framed,
  489. framedSize)
  490. || framedSize == 0 || framedSize > response.size()) {
  491. core::log::write(core::log::Channel::server,
  492. core::log::Level::warn,
  493. "ev=queuez stage=artifact_item_refresh result=fail");
  494. return false;
  495. }
  496. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  497. written = framedSize;
  498. middleware::secure_channel::advance_nonce(nextSendNonce);
  499. session.sendNonce = nextSendNonce;
  500. session.queuez = update.after;
  501. ++session.artifactResetRefreshCursor;
  502. return true;
  503. }
  504. } // namespace
  505. /** Publishes the next due reward, refresh, retry, or keepalive. */
  506. bool consume_deferred(Session& session,
  507. Scratch& scratch,
  508. std::span<std::byte> response,
  509. std::size_t& written,
  510. bool& touchesScratch) noexcept {
  511. written = 0;
  512. if (!session.authenticated) {
  513. return false;
  514. }
  515. if (consume_artifact_family4_refresh(session, scratch, response, written, touchesScratch)) {
  516. return true;
  517. }
  518. if (consume_artifact_item_refresh(session, scratch, response, written, touchesScratch)) {
  519. return true;
  520. }
  521. if (consume_account_resync(session, scratch, response, written, touchesScratch)) {
  522. return true;
  523. }
  524. // A failed resync blocks every incremental that could depend on its missing objects.
  525. if (session.accountResyncArmed) {
  526. return false;
  527. }
  528. WorldRewardRequest reward{};
  529. if (session.queuez.family4Active && bap::current_world_reward(reward)) {
  530. bool published = false;
  531. switch (reward.kind) {
  532. case WorldRewardKind::item:
  533. published = consume_world_item_acquisition(
  534. reward, session, scratch, response, written, touchesScratch);
  535. break;
  536. case WorldRewardKind::profileItem:
  537. published = consume_world_profile_item_acquisition(
  538. reward, session, scratch, response, written, touchesScratch);
  539. break;
  540. }
  541. if (published) {
  542. return true;
  543. }
  544. }
  545. if (consume_seasonal_experience_presentation(
  546. session, scratch, response, written, touchesScratch)) {
  547. return true;
  548. }
  549. if (consume_ability_refresh(session, scratch, response, written, touchesScratch)) {
  550. return true;
  551. }
  552. if (!session.family4RepushArmed || session.family4RepushRoot == 0
  553. || GetTickCount64() < session.family4RepushDueTick
  554. || GetTickCount64() < session.acquisitionPresentationUntilTick) {
  555. return consume_banner_repush(session, scratch, response, written, touchesScratch)
  556. || push::activity::consume_activity_keepalive(
  557. session, scratch, response, written, touchesScratch);
  558. }
  559. // One attempt is owed, and it is spent whether or not it lands.
  560. touchesScratch = true;
  561. middleware::queuez::Subscription subscription{};
  562. subscription.familyType = queuez::kAccountFamilyType;
  563. subscription.familyRootSoid = session.family4RepushRoot;
  564. auto nextSendNonce = session.sendNonce;
  565. std::size_t framedSize = 0;
  566. queuez::SessionState after{};
  567. bool armsRepush = false;
  568. bool armsBannerRepush = false;
  569. push::append_queuez_notification(scratch,
  570. session.queuez,
  571. subscription,
  572. state::bap().sessionKey,
  573. nextSendNonce,
  574. scratch.framed,
  575. framedSize,
  576. after,
  577. armsRepush,
  578. armsBannerRepush);
  579. if (framedSize == 0 || framedSize > response.size()) {
  580. // Neither failure clears on a retry. Holding the arm starves the keepalive, and the client
  581. // drops the activity session once the keepalive stops.
  582. session.family4RepushArmed = false;
  583. core::log::write(core::log::Channel::server,
  584. core::log::Level::warn,
  585. framedSize == 0 ? "ev=queuez stage=repush result=fail reason=encode"
  586. : "ev=queuez stage=repush result=fail reason=capacity");
  587. return false;
  588. }
  589. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  590. written = framedSize;
  591. session.sendNonce = nextSendNonce;
  592. if (queuez::valid(after)) {
  593. session.queuez = after;
  594. }
  595. session.family4RepushArmed = false;
  596. return true;
  597. }
  598. } // namespace sunrise::server::bap::encrypted