activity_transaction_notifications.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #include "activity_transaction_notifications.h"
  2. #include "../../../../middleware/secure_channel/runtime.h"
  3. #include "../../../gameplay/gameplay_advertisement.h"
  4. #include "../push/activity/activity_arrival.h"
  5. #include "../push/activity/activity_global_state_push.h"
  6. #include "../push/activity/activity_membership_push.h"
  7. #include "../push/activity/activity_message_push.h"
  8. #include "../push/activity/activity_notification_frame.h"
  9. #include "../push/activity/activity_roster_push.h"
  10. #include "../push/activity/activity_world_globals_push.h"
  11. #include "../push/activity/internal.h"
  12. namespace sunrise::server::bap::encrypted::activity_transaction {
  13. namespace {
  14. /**
  15. * Reports whether the citizen advertisement this membership body would carry is still coming.
  16. * One membership update lands per revision, so a body sent before the region's host session exists
  17. * spends that revision on a record no later push can fill. Holding costs one keepalive.
  18. * @param activity Prepared activity transaction, whose region this body publishes.
  19. * @return True when the push has to wait.
  20. */
  21. [[nodiscard]] bool advertisement_pending(const Session& session,
  22. const activity_message::ActivityPlan& activity) noexcept {
  23. if (session.activity.role != ActivityClientRole::privateCurrent
  24. || !state::activity::binding_matches(session.activity.source)) {
  25. return false;
  26. }
  27. // Take the delta's region, not the committed one. Staging runs before the commit, so the
  28. // committed value still names the region the player has left.
  29. const push::activity::EffectiveRegion region =
  30. push::activity::private_planned_region(activity.membershipMutation,
  31. session.activity.source);
  32. const server::gameplay::AdvertisementState state =
  33. push::activity::region_advertisement(session, region.index);
  34. if (state != server::gameplay::AdvertisementState::pending) {
  35. return false;
  36. }
  37. return true;
  38. }
  39. /**
  40. * Stages the whole host snapshot the client's state-refresh request asks for.
  41. * The order matches the join burst: the global state and its clock enable, then membership, then
  42. * the roster. The roster's participation key binds to the player the membership publishes.
  43. * @param session Connection-owned roster counters, advanced only by a staged roster.
  44. * @param scratch Lock-owned transform buffers.
  45. * @param activity Prepared activity transaction carrying the membership snapshot.
  46. * @param key Active AES-GCM session key.
  47. * @param nonce Local send nonce advanced only by complete staged notifications.
  48. * @param response Lock-owned complete-frame staging storage.
  49. * @param written Existing staged byte count, updated only by complete notifications.
  50. * @return True when at least one of the three notifications was staged.
  51. */
  52. [[nodiscard]] bool stage_refresh(Session& session,
  53. Scratch& scratch,
  54. const activity_message::ActivityPlan& activity,
  55. std::span<const std::byte, state::kAesKeySize> key,
  56. std::array<std::byte, state::kBapNonceSize>& nonce,
  57. std::span<std::byte> response,
  58. std::size_t& written,
  59. bool allowEntityRetirement) noexcept {
  60. bool staged = push::activity::append_global_state_notification(
  61. scratch, session.activity.session, key, nonce, response, written)
  62. && push::activity::append_world_globals_notification(
  63. scratch, session.activity.session.sessionId, key, nonce, response, written);
  64. bool stagedMembership = false;
  65. if (session.activity.role == ActivityClientRole::privateCurrent
  66. && activity.membershipMutation.hasSnapshot && !advertisement_pending(session, activity)) {
  67. stagedMembership = push::activity::append_membership_notification(
  68. scratch, session, activity, key, nonce, response, written);
  69. staged = stagedMembership || staged;
  70. }
  71. // Message 18 asks for the whole host snapshot, so its roster is solicited and is never
  72. // suppressed as a repeat. The client sends it when its own mirror is stale, which the host's
  73. // delivered-body record cannot see. Its bubble field names the slice set the client holds.
  74. const push::activity::RefreshReport refresh{activity.membershipMutation.bubbleIndex,
  75. activity.membershipMutation.requestedRevision};
  76. return push::activity::append_roster_notification(session,
  77. scratch,
  78. key,
  79. nonce,
  80. response,
  81. written,
  82. nullptr,
  83. nullptr,
  84. true,
  85. &refresh,
  86. allowEntityRetirement)
  87. || staged;
  88. }
  89. /**
  90. * Stages what one client-reported, host-committed delta owes: membership, the roster, or both.
  91. *
  92. * The roster follows membership, because its participation key binds to the player membership
  93. * publishes.
  94. * @param session Connection-owned roster counters, advanced only by a staged roster.
  95. * @param scratch Lock-owned transform buffers.
  96. * @param activity Prepared activity transaction and its region-move flag.
  97. * @param key Active AES-GCM session key.
  98. * @param nonce Local send nonce advanced only by complete staged notifications.
  99. * @param response Lock-owned complete-frame staging storage.
  100. * @param written Existing staged byte count, updated only by complete notifications.
  101. * @return True when every notification the delta owed was staged.
  102. */
  103. [[nodiscard]] bool stage_authoritative(Session& session,
  104. Scratch& scratch,
  105. const activity_message::ActivityPlan& activity,
  106. std::span<const std::byte, state::kAesKeySize> key,
  107. std::array<std::byte, state::kBapNonceSize>& nonce,
  108. std::span<std::byte> response,
  109. std::size_t& written,
  110. bool allowEntityRetirement) noexcept {
  111. bool staged = false;
  112. bool stagedMembership = false;
  113. bool held = false;
  114. bool owed = false;
  115. bool suppressed = false;
  116. if (session.activity.role == ActivityClientRole::privateCurrent
  117. && activity.membershipMutation.hasSnapshot) {
  118. owed = true;
  119. held = advertisement_pending(session, activity);
  120. if (!held) {
  121. // A delta that changed the published fields never matches the delivered body, so
  122. // only a true repeat is skipped here.
  123. stagedMembership = push::activity::append_membership_notification(
  124. scratch, session, activity, key, nonce, response, written, true, &suppressed);
  125. staged = stagedMembership;
  126. }
  127. }
  128. if (session.activity.role == ActivityClientRole::privateCurrent && activity.regionMoved) {
  129. owed = true;
  130. // This notification is encoded before the membership transaction commits. Its roster
  131. // must use the prepared move, never the old committed msg-22 region.
  132. const push::activity::EffectiveRegion region =
  133. push::activity::planned_region(activity.membershipMutation, session.activity.source);
  134. // This response is staged before the authoritative transaction commits. Carry the
  135. // incoming current leg into readiness evaluation as well as the selected roster region;
  136. // otherwise the body repeats the stale loading lifetime even though this very report says
  137. // the slice set is now instantiated. No later edge is guaranteed to republish that field.
  138. push::activity::RefreshReport report{};
  139. report.currentRegion =
  140. activity.membershipMutation.authoritativeInput.currentRegion.index;
  141. report.hasCurrentRegion =
  142. activity.membershipMutation.authoritativeInput.hasCurrentRegion;
  143. // The client reports the region it now holds once its slice set is instantiated, and the
  144. // roster is that report's answer. It is solicited, so it is never skipped as a repeat,
  145. // including while the slice set is still instantiating.
  146. staged = push::activity::append_roster_notification(session,
  147. scratch,
  148. key,
  149. nonce,
  150. response,
  151. written,
  152. nullptr,
  153. &region,
  154. true,
  155. &report,
  156. allowEntityRetirement)
  157. || staged;
  158. }
  159. // A delta that owed nothing is not a failure, and a public link never owes the block above.
  160. // Held and already-delivered bodies are not failures either: a false here would drop the
  161. // very commit the body reflects.
  162. return !owed || staged || held || suppressed;
  163. }
  164. } // namespace
  165. /**
  166. * Stages the notifications one activity transaction requests.
  167. * @param session Connection-owned roster counters, advanced only by a staged roster.
  168. * @param scratch Lock-owned transform buffers.
  169. * @param activity Prepared activity transaction and delivery selection.
  170. * @param key Active AES-GCM session key.
  171. * @param nonce Local send nonce advanced only by complete staged notifications.
  172. * @param response Lock-owned complete-frame staging storage.
  173. * @param written Existing staged byte count, updated only by complete notifications.
  174. * @return True when every requested notification is staged.
  175. */
  176. bool stage_notifications(Session& session,
  177. Scratch& scratch,
  178. const activity_message::ActivityPlan& activity,
  179. std::span<const std::byte, state::kAesKeySize> key,
  180. std::array<std::byte, state::kBapNonceSize>& nonce,
  181. std::span<std::byte> response,
  182. std::size_t& written,
  183. bool allowEntityRetirement) noexcept {
  184. // Each encoder refuses an absent session itself, so a plan that delivers nothing needs no
  185. // session at all. Message type 52 is the one that arrives on an unallocated link.
  186. if (activity.delivery == activity_message::Delivery::joinNotifications) {
  187. return push::activity::append_join_notifications(
  188. scratch, session, activity, key, nonce, response, written);
  189. }
  190. if (activity.delivery == activity_message::Delivery::purgeNotification) {
  191. namespace control = middleware::bap::activity_message::host_control;
  192. const auto& purge = activity.authorityPurge;
  193. if (!purge.pending || purge.sourceGeneration != session.activity.bindingGeneration
  194. || activity.sessionId != session.activity.session.sessionId
  195. || activity_link_count_locked(session.activity.session) != 1
  196. || purge.body.epoch
  197. != static_cast<std::uint8_t>(session.activity.replicationEpoch + 1U)) {
  198. return false;
  199. }
  200. std::array<std::byte, control::kPurgeAuthorityByteCount> body{};
  201. std::size_t bodySize = 0;
  202. if (!control::encode_purge_authority(purge.body, body, bodySize)
  203. || !push::activity::append_notification_frame(scratch,
  204. activity.sessionId,
  205. control::kPurgeAuthorityMessageType,
  206. std::span(body).first(bodySize),
  207. key,
  208. nonce,
  209. response,
  210. written)) {
  211. return false;
  212. }
  213. middleware::secure_channel::advance_nonce(nonce);
  214. return true;
  215. }
  216. if (activity.delivery == activity_message::Delivery::rosterNotification) {
  217. // The client's inbound dispatch table has no entry for a type-52 response, so that
  218. // message is a one-way report and owes nothing on its own.
  219. if (!session.activityRosterOwedForEpoch) {
  220. return true;
  221. }
  222. // The epoch it reports is what an earlier answer was missing, so that answer goes now. The
  223. // epoch comes from this message; the connection's own copy is published after this runs.
  224. return push::activity::append_roster_notification(session,
  225. scratch,
  226. key,
  227. nonce,
  228. response,
  229. written,
  230. &activity.patchEpoch,
  231. nullptr,
  232. true,
  233. nullptr,
  234. allowEntityRetirement);
  235. }
  236. if (activity.delivery == activity_message::Delivery::entitySlotNotification) {
  237. return push::activity::append_entity_slot_notification(scratch,
  238. activity.sessionId,
  239. activity.entitySlotMutation.mask,
  240. key,
  241. nonce,
  242. response,
  243. written);
  244. }
  245. if (activity.delivery == activity_message::Delivery::membershipNotification) {
  246. // A public target gets exactly one membership body, in its join burst. A second one here
  247. // would land mid-transition and would set no one-shot latch.
  248. if (session.activity.role == ActivityClientRole::publicTarget) {
  249. return true;
  250. }
  251. return push::activity::append_membership_notification(
  252. scratch, session, activity, key, nonce, response, written);
  253. }
  254. if (activity.delivery == activity_message::Delivery::refreshNotifications) {
  255. return stage_refresh(
  256. session, scratch, activity, key, nonce, response, written, allowEntityRetirement);
  257. }
  258. if (activity.delivery == activity_message::Delivery::authoritativeNotifications) {
  259. return stage_authoritative(
  260. session, scratch, activity, key, nonce, response, written, allowEntityRetirement);
  261. }
  262. return activity.delivery == activity_message::Delivery::none;
  263. }
  264. } // namespace sunrise::server::bap::encrypted::activity_transaction