queuez_deferred_push.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <array>
  4. #include <cstdio>
  5. #include "../../../../core/logging/log.h"
  6. #include "../../../../middleware/secure_channel/runtime.h"
  7. #include "../../../../state/account/account_state.h"
  8. #include "../../../../state/runtime/runtime.h"
  9. #include "../internal.h"
  10. #include "../push/activity/activity_keepalive_push.h"
  11. #include "queuez_state_validation.h"
  12. namespace sunrise::server::bap::encrypted {
  13. namespace {
  14. /** Widest re-push report, sized for the fields below. */
  15. constexpr std::size_t kRepushReportLimit = 96;
  16. /**
  17. * Logs one delayed re-push with its framed size, so it can be compared to the first copy.
  18. * @param stage Point in the deferred push the line reports.
  19. * @param bytes Framed size of the published notification.
  20. */
  21. void report_repush(const char* stage, std::size_t bytes) noexcept {
  22. std::array<char, kRepushReportLimit> line{};
  23. const int count = std::snprintf(
  24. line.data(), line.size(), "ev=queuez stage=%s result=ok bytes=%zu", stage, bytes);
  25. if (count > 0) {
  26. core::log::write(core::log::Channel::server,
  27. core::log::Level::info,
  28. {line.data(), static_cast<std::size_t>(count)});
  29. }
  30. }
  31. /** Publishes and commits one world reward through the ordinary acquisition notification path. */
  32. [[nodiscard]] bool consume_world_item_acquisition(Session& session,
  33. Scratch& scratch,
  34. std::span<std::byte> response,
  35. std::size_t& written,
  36. bool& touchesScratch) noexcept {
  37. if (!session.worldItemAcquisitionArmed) {
  38. return false;
  39. }
  40. touchesScratch = true;
  41. queuez::ItemAcquisition acquisition{};
  42. const state::PendingItemAcquisition pending = session.pendingWorldItemAcquisition;
  43. if (!queuez::stage_item_acquisition(session.queuez,
  44. pending.accountSoid,
  45. pending.characterSoid,
  46. pending.acquiredInstanceSoid,
  47. pending.profileChanged,
  48. acquisition)) {
  49. core::log::write(core::log::Channel::server,
  50. core::log::Level::warn,
  51. "ev=queuez stage=world_acquisition result=fail reason=stage");
  52. return false;
  53. }
  54. auto nextSendNonce = session.sendNonce;
  55. std::size_t framedSize = 0;
  56. if (!push::append_item_acquisition_notification(scratch,
  57. acquisition,
  58. pending,
  59. state::bap().sessionKey,
  60. nextSendNonce,
  61. scratch.framed,
  62. framedSize)
  63. || framedSize == 0 || framedSize > response.size()) {
  64. core::log::write(core::log::Channel::server,
  65. core::log::Level::warn,
  66. "ev=queuez stage=world_acquisition result=fail reason=encode");
  67. return false;
  68. }
  69. if (!state::commit_item_acquisition(session.pendingWorldItemAcquisition)) {
  70. session.worldItemAcquisitionArmed = false;
  71. core::log::write(core::log::Channel::server,
  72. core::log::Level::warn,
  73. "ev=queuez stage=world_acquisition result=fail reason=commit");
  74. return false;
  75. }
  76. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  77. written = framedSize;
  78. middleware::secure_channel::advance_nonce(nextSendNonce);
  79. session.sendNonce = nextSendNonce;
  80. session.queuez = acquisition.after;
  81. session.worldItemAcquisitionArmed = false;
  82. report_repush("world_acquisition", framedSize);
  83. return true;
  84. }
  85. /** Publishes and commits one profile material reward through its acquisition notification. */
  86. [[nodiscard]] bool consume_world_profile_item_acquisition(Session& session,
  87. Scratch& scratch,
  88. std::span<std::byte> response,
  89. std::size_t& written,
  90. bool& touchesScratch) noexcept {
  91. if (!session.worldProfileItemAcquisitionArmed) {
  92. return false;
  93. }
  94. touchesScratch = true;
  95. queuez::ProfileItemAcquisition acquisition{};
  96. const state::PendingProfileItemAcquisition pending =
  97. session.pendingWorldProfileItemAcquisition;
  98. if (!queuez::stage_profile_item_acquisition(session.queuez,
  99. pending.accountSoid,
  100. pending.acquiredInstanceSoid,
  101. pending.actionSource,
  102. pending.appended,
  103. acquisition)) {
  104. core::log::write(core::log::Channel::server,
  105. core::log::Level::warn,
  106. "ev=queuez stage=world_profile_acquisition result=fail reason=stage");
  107. return false;
  108. }
  109. auto nextSendNonce = session.sendNonce;
  110. std::size_t framedSize = 0;
  111. if (!push::append_profile_item_acquisition_notification(scratch,
  112. acquisition,
  113. pending,
  114. state::bap().sessionKey,
  115. nextSendNonce,
  116. scratch.framed,
  117. framedSize)
  118. || framedSize == 0 || framedSize > response.size()) {
  119. core::log::write(core::log::Channel::server,
  120. core::log::Level::warn,
  121. "ev=queuez stage=world_profile_acquisition result=fail reason=encode");
  122. return false;
  123. }
  124. if (!state::commit_profile_item_acquisition(session.pendingWorldProfileItemAcquisition)) {
  125. session.worldProfileItemAcquisitionArmed = false;
  126. core::log::write(core::log::Channel::server,
  127. core::log::Level::warn,
  128. "ev=queuez stage=world_profile_acquisition result=fail reason=commit");
  129. return false;
  130. }
  131. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  132. written = framedSize;
  133. middleware::secure_channel::advance_nonce(nextSendNonce);
  134. session.sendNonce = nextSendNonce;
  135. session.queuez = acquisition.after;
  136. session.worldProfileItemAcquisitionArmed = false;
  137. report_repush("world_profile_acquisition", framedSize);
  138. return true;
  139. }
  140. /** Publishes the current account graph to a peer invalidated by another connection. */
  141. [[nodiscard]] bool consume_account_resync(Session& session,
  142. Scratch& scratch,
  143. std::span<std::byte> response,
  144. std::size_t& written,
  145. bool& touchesScratch) noexcept {
  146. if (!session.accountResyncArmed || session.accountResyncGeneration == 0) {
  147. return false;
  148. }
  149. touchesScratch = true;
  150. auto nextSendNonce = session.sendNonce;
  151. std::size_t framedSize = 0;
  152. queuez::SessionState currentQueuez{};
  153. if (!push::append_account_resync_notification(scratch,
  154. session.queuez,
  155. state::bap().sessionKey,
  156. nextSendNonce,
  157. scratch.framed,
  158. framedSize,
  159. currentQueuez)) {
  160. core::log::write(core::log::Channel::server,
  161. core::log::Level::warn,
  162. "ev=queuez stage=peer_resync result=fail reason=family4");
  163. return false;
  164. }
  165. if (currentQueuez.family0Active) {
  166. queuez::SessionState appearanceAfter{};
  167. if (!push::append_account_resync_appearance_notification(scratch,
  168. currentQueuez,
  169. state::bap().sessionKey,
  170. nextSendNonce,
  171. scratch.framed,
  172. framedSize,
  173. appearanceAfter)) {
  174. core::log::write(core::log::Channel::server,
  175. core::log::Level::warn,
  176. "ev=queuez stage=peer_resync result=fail reason=family0");
  177. return false;
  178. }
  179. currentQueuez = appearanceAfter;
  180. }
  181. if (currentQueuez.family3Active) {
  182. queuez::SessionState rosterAfter{};
  183. if (!push::append_account_resync_roster_notification(scratch,
  184. currentQueuez,
  185. state::bap().sessionKey,
  186. nextSendNonce,
  187. scratch.framed,
  188. framedSize,
  189. rosterAfter)) {
  190. core::log::write(core::log::Channel::server,
  191. core::log::Level::warn,
  192. "ev=queuez stage=peer_resync result=fail reason=family3");
  193. return false;
  194. }
  195. currentQueuez = rosterAfter;
  196. }
  197. if (framedSize == 0 || framedSize > response.size() || !queuez::valid(currentQueuez)) {
  198. core::log::write(core::log::Channel::server,
  199. core::log::Level::warn,
  200. "ev=queuez stage=peer_resync result=fail reason=output");
  201. return false;
  202. }
  203. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  204. written = framedSize;
  205. session.sendNonce = nextSendNonce;
  206. session.queuez = currentQueuez;
  207. session.accountGeneration = session.accountResyncGeneration;
  208. session.accountResyncArmed = false;
  209. report_repush("peer_resync", framedSize);
  210. return true;
  211. }
  212. /**
  213. * Sends the owed banner re-push once its delay has passed.
  214. * The banner has no subscribe of its own, so the timer is its only second chance.
  215. * @param session Auth, nonce and queuez state owned by the connection.
  216. * @param scratch Transform buffers owned by the lock.
  217. * @param response Whole-frame storage owned by the caller.
  218. * @param written Gets the encoded notification size in bytes.
  219. * @param touchesScratch Set before any scratch buffer is used.
  220. * @return True when a whole banner notification is published.
  221. */
  222. [[nodiscard]] bool consume_banner_repush(Session& session,
  223. Scratch& scratch,
  224. std::span<std::byte> response,
  225. std::size_t& written,
  226. bool& touchesScratch) noexcept {
  227. if (!session.bannerRepushArmed || session.bannerRepushRoot == 0
  228. || GetTickCount64() < session.bannerRepushDueTick) {
  229. return false;
  230. }
  231. // Nothing is owed while the account owns no character to name. The arm stays set, because it
  232. // is the banner's only second chance.
  233. if (state::account::banner_character_soid(state::account_snapshot()) == 0) {
  234. return false;
  235. }
  236. touchesScratch = true;
  237. // The same body the subscribe answer builds, so the version and this host's mirror stay in
  238. // step. `append_banner_notification` fixes the version at zero and a pick has moved past it.
  239. middleware::queuez::Subscription subscription{};
  240. subscription.familyType = queuez::kBannerFamilyType;
  241. subscription.familyRootSoid = session.bannerRepushRoot;
  242. auto nextSendNonce = session.sendNonce;
  243. std::size_t framedSize = 0;
  244. queuez::SessionState bannerAfter{};
  245. bool armsRepush = false;
  246. bool armsBannerRepush = false;
  247. push::append_queuez_notification(scratch,
  248. session.queuez,
  249. subscription,
  250. state::bap().sessionKey,
  251. nextSendNonce,
  252. scratch.framed,
  253. framedSize,
  254. bannerAfter,
  255. armsRepush,
  256. armsBannerRepush);
  257. if (framedSize == 0 || framedSize > response.size()) {
  258. core::log::write(core::log::Channel::server,
  259. core::log::Level::warn,
  260. "ev=queuez stage=banner_repush result=fail");
  261. return false;
  262. }
  263. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  264. written = framedSize;
  265. session.sendNonce = nextSendNonce;
  266. // The frame is committed here, so the recorded delivery and the arm are committed with it.
  267. if (valid(bannerAfter)) {
  268. session.queuez = bannerAfter;
  269. }
  270. session.bannerRepushArmed = false;
  271. report_repush("banner_repush", framedSize);
  272. return true;
  273. }
  274. /**
  275. * Re-derives the selected character's appearance and roster once the ability-bucket rebuild owed
  276. * by a subclass selection has landed. The refresh sent inline with the opcode-801 response can
  277. * still carry empty buckets, because that rebuild runs off the Client content-extraction pump.
  278. * @param session Auth, nonce and queuez state owned by the connection.
  279. * @param scratch Transform buffers owned by the lock.
  280. * @param response Whole-frame storage owned by the caller.
  281. * @param written Gets the encoded notification size in bytes.
  282. * @param touchesScratch Set before any scratch buffer is used.
  283. * @return True when at least one owed record refreshes.
  284. */
  285. [[nodiscard]] bool consume_ability_refresh(Session& session,
  286. Scratch& scratch,
  287. std::span<std::byte> response,
  288. std::size_t& written,
  289. bool& touchesScratch) noexcept {
  290. if (!session.abilityRefreshArmed || GetTickCount64() < session.abilityRefreshDueTick) {
  291. return false;
  292. }
  293. // Nothing is owed until a family that reads abilities is subscribed. The arm stays set, the
  294. // same way the banner re-push below keeps its own.
  295. if (!session.queuez.family0Active && !session.queuez.family3Active) {
  296. return false;
  297. }
  298. touchesScratch = true;
  299. auto nextSendNonce = session.sendNonce;
  300. std::size_t framedSize = 0;
  301. queuez::SessionState current = session.queuez;
  302. bool wrote = false;
  303. if (current.family0Active) {
  304. queuez::SessionState appearanceAfter{};
  305. if (push::append_account_resync_appearance_notification(scratch,
  306. current,
  307. state::bap().sessionKey,
  308. nextSendNonce,
  309. scratch.framed,
  310. framedSize,
  311. appearanceAfter)) {
  312. current = appearanceAfter;
  313. wrote = true;
  314. }
  315. }
  316. if (current.family3Active) {
  317. queuez::SessionState rosterAfter{};
  318. if (push::append_account_resync_roster_notification(scratch,
  319. current,
  320. state::bap().sessionKey,
  321. nextSendNonce,
  322. scratch.framed,
  323. framedSize,
  324. rosterAfter)) {
  325. current = rosterAfter;
  326. wrote = true;
  327. }
  328. }
  329. if (!wrote || framedSize == 0 || framedSize > response.size() || !queuez::valid(current)) {
  330. core::log::write(core::log::Channel::server,
  331. core::log::Level::warn,
  332. "ev=queuez stage=ability_refresh result=fail");
  333. return false;
  334. }
  335. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  336. written = framedSize;
  337. session.sendNonce = nextSendNonce;
  338. session.queuez = current;
  339. // The frame is committed here, so the arm is committed with it. Disarming any earlier drops
  340. // the owed refresh on a transient encode failure.
  341. session.abilityRefreshArmed = false;
  342. report_repush("ability_refresh", framedSize);
  343. return true;
  344. }
  345. } // namespace
  346. /**
  347. * Sends the owed Family-4 re-push once its delay has passed.
  348. * @param session Auth, nonce and queuez state owned by the connection.
  349. * @param scratch Transform buffers owned by the lock.
  350. * @param response Whole-frame storage owned by the caller.
  351. * @param written Gets the encoded notification size in bytes.
  352. * @param touchesScratch Set before any scratch buffer is used.
  353. * @return True when a whole Family-4 notification is published.
  354. */
  355. bool consume_deferred(Session& session,
  356. Scratch& scratch,
  357. std::span<std::byte> response,
  358. std::size_t& written,
  359. bool& touchesScratch) noexcept {
  360. written = 0;
  361. if (!session.authenticated) {
  362. return false;
  363. }
  364. if (consume_world_item_acquisition(session, scratch, response, written, touchesScratch)) {
  365. return true;
  366. }
  367. if (session.worldItemAcquisitionArmed) {
  368. return false;
  369. }
  370. if (consume_world_profile_item_acquisition(session, scratch, response, written, touchesScratch)) {
  371. return true;
  372. }
  373. if (session.worldProfileItemAcquisitionArmed) {
  374. return false;
  375. }
  376. if (consume_account_resync(session, scratch, response, written, touchesScratch)) {
  377. return true;
  378. }
  379. // A failed resync remains armed and blocks unrelated deferred output until it can be retried.
  380. if (session.accountResyncArmed) {
  381. return false;
  382. }
  383. if (consume_ability_refresh(session, scratch, response, written, touchesScratch)) {
  384. return true;
  385. }
  386. if (!session.family4RepushArmed || session.family4RepushRoot == 0
  387. || GetTickCount64() < session.family4RepushDueTick) {
  388. return consume_banner_repush(session, scratch, response, written, touchesScratch)
  389. || push::activity::consume_activity_keepalive(
  390. session, scratch, response, written, touchesScratch);
  391. }
  392. // One attempt is owed, and it is spent whether or not it lands.
  393. touchesScratch = true;
  394. middleware::queuez::Subscription subscription{};
  395. subscription.familyType = queuez::kAccountFamilyType;
  396. subscription.familyRootSoid = session.family4RepushRoot;
  397. auto nextSendNonce = session.sendNonce;
  398. std::size_t framedSize = 0;
  399. queuez::SessionState after{};
  400. bool armsRepush = false;
  401. bool armsBannerRepush = false;
  402. push::append_queuez_notification(scratch,
  403. session.queuez,
  404. subscription,
  405. state::bap().sessionKey,
  406. nextSendNonce,
  407. scratch.framed,
  408. framedSize,
  409. after,
  410. armsRepush,
  411. armsBannerRepush);
  412. if (framedSize == 0 || framedSize > response.size()) {
  413. // Neither failure clears on a retry. Holding the arm starves the keepalive, and the client
  414. // drops the activity session once the keepalive stops.
  415. session.family4RepushArmed = false;
  416. core::log::write(core::log::Channel::server,
  417. core::log::Level::warn,
  418. framedSize == 0 ? "ev=queuez stage=repush result=fail reason=encode"
  419. : "ev=queuez stage=repush result=fail reason=capacity");
  420. return false;
  421. }
  422. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  423. written = framedSize;
  424. session.sendNonce = nextSendNonce;
  425. if (queuez::valid(after)) {
  426. session.queuez = after;
  427. }
  428. session.family4RepushArmed = false;
  429. report_repush("repush", framedSize);
  430. return true;
  431. }
  432. } // namespace sunrise::server::bap::encrypted