replicate_membership.h 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. #pragma once
  2. #include <cstddef>
  3. #include <cstdint>
  4. #include <span>
  5. #include "../../encoding/bit_writer.h"
  6. #include "../../gameplay/descriptor/join_descriptor.h"
  7. #include "activity_client_identity_parser.h"
  8. #include "client_authoritative_data.h"
  9. namespace sunrise::middleware::bap::activity_message::replicate_membership {
  10. /** Membership snapshots use activity message type 12. */
  11. inline constexpr std::uint32_t kMessageType = 12;
  12. /** One local player and an explicit empty view mask use 30,077 meaningful bits. */
  13. inline constexpr std::size_t kMeaningfulBitCount = 30'077;
  14. /** The one-member snapshot has three zero padding bits. */
  15. inline constexpr std::size_t kEncodedSize = 3'760;
  16. /** A remote row adds its channel, process identity, view identity, and player snapshot. */
  17. inline constexpr std::size_t kRemoteMemberBitDelta = 3'044;
  18. /** The complete two-member body carries 33,121 meaningful bits. */
  19. inline constexpr std::size_t kRemoteHostMeaningfulBitCount = 33'121;
  20. /** Byte extent of the two-member body, including its seven zero padding bits. */
  21. inline constexpr std::size_t kRemoteHostEncodedSize = 4'141;
  22. /** One filled descriptor makes its record 1,024 bits longer and shifts every later field. */
  23. inline constexpr std::size_t kDescriptorBitCount = gameplay::descriptor::kDescriptorSize * 8U;
  24. /** Byte size once one record carries a descriptor. */
  25. inline constexpr std::size_t kCitizenEncodedSize =
  26. kEncodedSize + gameplay::descriptor::kDescriptorSize;
  27. /**
  28. * Regions one body may advertise a host for.
  29. * The client picks the record by region index, so a body carrying several lets it join a region
  30. * it is not in yet. That is what a fast travel needs and a bubble crossing does not.
  31. */
  32. inline constexpr std::size_t kCitizenCapacity = 8;
  33. /**
  34. * One remote-citizen advertisement placed in a single region record.
  35. * The record is picked by region index. The client adopts only the record whose index matches
  36. * its pending region.
  37. */
  38. struct CitizenAdvertisement final {
  39. std::array<std::byte, gameplay::descriptor::kDescriptorSize> descriptor{};
  40. /** The ambassador's activity-host id. It is not the descriptor's own session id. */
  41. std::uint64_t onlineSessionId{};
  42. /** Region index of the record that carries it. */
  43. std::int32_t regionIndex{};
  44. /**
  45. * Ambassador member slot. It must differ from the joining client's own slot. An equal slot
  46. * picks the local-ambassador stage instead of the citizen stage.
  47. */
  48. std::uint8_t ambassadorSlot{};
  49. bool present{};
  50. };
  51. /** One remote membership row that opens a family-6 channel and replication view. */
  52. struct RemoteViewMember final {
  53. /** Reflected process-session id. The peer checksum and native peer table retain this string. */
  54. std::array<std::int8_t, 128> processSessionId{};
  55. /** FNV-1a hash of processSessionId through its first NUL. */
  56. std::uint64_t processSessionIdHash{};
  57. /** Reflected player name. The view copies its first 16 bytes as the remote identity. */
  58. std::array<std::int8_t, 64> playerName{};
  59. client_identity::ClientIdentity identity{};
  60. std::array<std::byte, gameplay::descriptor::kNetAddrSize> address{};
  61. bool present{};
  62. };
  63. /**
  64. * One region leg of the local member's transition block, mirrored from the client's own report.
  65. * An absent leg writes its presence bit clear, which the delta codec reads as unchanged.
  66. */
  67. struct RegionLeg final {
  68. std::int32_t sliceSetIndex{-1};
  69. std::uint32_t sliceSetHash{};
  70. std::int32_t regionIndex{-1};
  71. std::int8_t publicState{-1};
  72. std::int8_t auxState{-1};
  73. bool present{};
  74. };
  75. /** A present leg adds its five scalars and two clear presence bits. */
  76. inline constexpr std::size_t kRegionLegBitCount = 10 + 32 + 32 + 2 + 2 + 1 + 1;
  77. /** Ten whole bytes, so a leg never moves the body's padding. */
  78. inline constexpr std::size_t kRegionLegByteCount = kRegionLegBitCount / 8;
  79. static_assert(kRegionLegByteCount * 8 == kRegionLegBitCount);
  80. /** Inputs for one local-player membership snapshot. */
  81. struct MembershipSnapshot final {
  82. client_identity::ClientIdentity identity{};
  83. /**
  84. * Activity Host id every member row publishes as player-state field 4.
  85. * The client sends zero there in its own join request, so the host names itself. It is the
  86. * activity session soid, the same value the join result carries as its field 1.
  87. */
  88. std::uint64_t activityHostId{};
  89. /** The local member's own current and pending region legs. */
  90. RegionLeg currentLeg{};
  91. RegionLeg pendingLeg{};
  92. /** Optional remote host in member slot one. Absent keeps the established one-member body. */
  93. RemoteViewMember remoteViewMember{};
  94. client_authoritative_data::SpawnState spawn{};
  95. client_authoritative_data::TeleportState teleport{};
  96. /**
  97. * Host directory. One entry per region we advertise a host for, in no particular order.
  98. * Empty unless the gameplay channel is advertising an endpoint this run.
  99. */
  100. std::array<CitizenAdvertisement, kCitizenCapacity> citizens{};
  101. /** Filled entries at the front of the directory. */
  102. std::uint8_t citizenCount{};
  103. std::uint32_t revision{};
  104. /** Stable session epoch; changing it clears the client's peer table. */
  105. std::uint32_t epoch{};
  106. /** Transition token written into the lane of every occupied member of every region. */
  107. std::uint8_t transitionToken{};
  108. /**
  109. * One bit per region record, set when that record's authored bubble is public.
  110. * Publicity is a property of the bubble, so all 8 of its region states share one bit.
  111. */
  112. std::uint64_t regionPublicMask{};
  113. /**
  114. * True for a private activity. No bubble host is advertised, and every region record names
  115. * the client's own slot as ambassador. The client then hosts its bubbles as a private
  116. * fireteam does, and moves between them without a host change.
  117. */
  118. bool selfHosted{};
  119. /**
  120. * Region a self-hosted body publishes, or -1. Its bubble's record names it instead of the
  121. * bubble's state-zero region, so a state above zero has a record the client can match.
  122. */
  123. std::int32_t selfHostedRegion{-1};
  124. };
  125. /** The local member always occupies member slot zero. */
  126. inline constexpr std::uint32_t kLocalMemberMask = 1U;
  127. /** A remote view host occupies member slot one. */
  128. inline constexpr std::uint32_t kRemoteMemberMask = 1U << 1U;
  129. /** @return Mask of the member slots this body fills. */
  130. [[nodiscard]] constexpr std::uint32_t
  131. occupied_member_mask(const MembershipSnapshot& snapshot) noexcept {
  132. return kLocalMemberMask | (snapshot.remoteViewMember.present ? kRemoteMemberMask : 0U);
  133. }
  134. /** The native view updater skips the own member and activates the advertised remote member. */
  135. [[nodiscard]] constexpr std::uint32_t
  136. active_view_mask(const MembershipSnapshot& snapshot) noexcept {
  137. return snapshot.remoteViewMember.present ? kRemoteMemberMask : 0U;
  138. }
  139. /** @return Bits the local member's present region legs add. */
  140. [[nodiscard]] constexpr std::size_t
  141. region_leg_bit_count(const MembershipSnapshot& snapshot) noexcept {
  142. return (snapshot.currentLeg.present ? kRegionLegBitCount : 0)
  143. + (snapshot.pendingLeg.present ? kRegionLegBitCount : 0);
  144. }
  145. /** @return Encoded byte size for one snapshot, which grows with each advertised region. */
  146. [[nodiscard]] constexpr std::size_t encoded_size(const MembershipSnapshot& snapshot) noexcept {
  147. const std::size_t base =
  148. snapshot.remoteViewMember.present ? kRemoteHostEncodedSize : kEncodedSize;
  149. return base + snapshot.citizenCount * gameplay::descriptor::kDescriptorSize
  150. + region_leg_bit_count(snapshot) / 8;
  151. }
  152. /** @return Meaningful bits before byte padding for this exact member/directory shape. */
  153. [[nodiscard]] constexpr std::size_t
  154. meaningful_bit_count(const MembershipSnapshot& snapshot) noexcept {
  155. return kMeaningfulBitCount + (snapshot.remoteViewMember.present ? kRemoteMemberBitDelta : 0)
  156. + snapshot.citizenCount * kDescriptorBitCount + region_leg_bit_count(snapshot);
  157. }
  158. /** The host table has one fixed record per bubble, and the client reads at most 64 of them. */
  159. inline constexpr std::size_t kRegionRecordCount = 64;
  160. /** A bubble owns 8 slice-set states, so its regions run `8 * bubble` to `8 * bubble + 7`. */
  161. inline constexpr std::int32_t kRegionStateCount = 8;
  162. /** First region index no record can name. */
  163. inline constexpr std::int32_t kRegionIndexBound =
  164. static_cast<std::int32_t>(kRegionRecordCount) * kRegionStateCount;
  165. /**
  166. * Finds the directory entry one bubble's region record carries.
  167. * A record names one region, so a bubble can advertise only one of its 8 states at a time.
  168. * @param snapshot Snapshot holding the directory.
  169. * @param bubble Bubble ordinal of the record being written.
  170. * @return The entry in that bubble, or null when the directory names none.
  171. */
  172. [[nodiscard]] constexpr const CitizenAdvertisement*
  173. advertisement_in_bubble(const MembershipSnapshot& snapshot, std::size_t bubble) noexcept {
  174. for (std::size_t entry = 0; entry < snapshot.citizenCount; ++entry) {
  175. const CitizenAdvertisement& candidate = snapshot.citizens[entry];
  176. if (candidate.present && candidate.regionIndex >= 0
  177. && static_cast<std::size_t>(candidate.regionIndex / kRegionStateCount) == bubble) {
  178. return &candidate;
  179. }
  180. }
  181. return nullptr;
  182. }
  183. /**
  184. * Encodes one full-player membership snapshot. No allocation.
  185. * @param snapshot Checked identity, revision, transition, and host-echo values.
  186. * @param output Caller storage, left unchanged when validation fails or it is too small.
  187. * @param written Receives the encoded byte count on success or zero on failure.
  188. * @return True when the host-present body was encoded.
  189. */
  190. [[nodiscard]] bool encode_replicate_membership(const MembershipSnapshot& snapshot,
  191. std::span<std::byte> output,
  192. std::size_t& written) noexcept;
  193. /** The local member begins after root, revision, and epoch fields. */
  194. inline constexpr std::size_t kMemberStartBit = 65;
  195. /** The local identity alone shifts the region block to bit 912. */
  196. inline constexpr std::size_t kRegionBlockStartBit = 912;
  197. /** The host-present region block ends before top-level field four. */
  198. inline constexpr std::size_t kRegionBlockEndBit = 29'976;
  199. /** @return Bit at which the region block ends for one snapshot. */
  200. [[nodiscard]] constexpr std::size_t
  201. region_block_end_bit(const MembershipSnapshot& snapshot) noexcept {
  202. return kRegionBlockEndBit + (snapshot.remoteViewMember.present ? kRemoteMemberBitDelta : 0)
  203. + snapshot.citizenCount * kDescriptorBitCount + region_leg_bit_count(snapshot);
  204. }
  205. /** @return First bit of region zero after the exact member-table shape. */
  206. [[nodiscard]] constexpr std::size_t
  207. region_block_start_bit(const MembershipSnapshot& snapshot) noexcept {
  208. return kRegionBlockStartBit + (snapshot.remoteViewMember.present ? kRemoteMemberBitDelta : 0)
  209. + region_leg_bit_count(snapshot);
  210. }
  211. /** @return True when every snapshot field fits the fixed wire field that carries it. */
  212. [[nodiscard]] bool valid(const MembershipSnapshot& snapshot) noexcept;
  213. /**
  214. * Writes the local member, an optional remote-view member, and every remaining absent slot.
  215. * @param writer Fixed-buffer writer positioned at bit 65.
  216. * @param snapshot Exact local identity and optional remote-view row.
  217. * @return True when the writer reaches the region-block presence bit.
  218. */
  219. [[nodiscard]] bool write_member_table(encoding::bits::Writer& writer,
  220. const MembershipSnapshot& snapshot) noexcept;
  221. /**
  222. * Writes the 64 region records and the host-present tail.
  223. * @param writer Fixed-buffer writer positioned at the region-block start bit.
  224. * @param snapshot Transition token and reflected host state.
  225. * @return True when the writer reaches top-level field four.
  226. */
  227. [[nodiscard]] bool write_region_block(encoding::bits::Writer& writer,
  228. const MembershipSnapshot& snapshot) noexcept;
  229. } // namespace sunrise::middleware::bap::activity_message::replicate_membership