gameplay_advertisement.cpp 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #include "gameplay_advertisement.h"
  2. #include <atomic>
  3. #include <cstddef>
  4. #include <cstdint>
  5. #include "../../middleware/gameplay/descriptor/join_descriptor.h"
  6. #include "endpoint/gameplay_endpoint.h"
  7. #include "gameplay_log.h"
  8. #include "group/group_host_sessions.h"
  9. namespace sunrise::server::gameplay {
  10. namespace {
  11. namespace message = middleware::bap::activity_message::replicate_membership;
  12. /** Member-slot fields are 6 bits at bias 1, so the slot itself cannot exceed 62. */
  13. constexpr std::uint8_t kMaximumMemberSlot = 62;
  14. /** Slot a readiness query stands in with. Every publisher names the one member it publishes. */
  15. constexpr std::uint8_t kQueriedMemberSlot = 0;
  16. /** Odd multiplier that spreads one region index across the whole 64-bit space. */
  17. constexpr std::uint64_t kRegionStride = 0x9E3779B97F4A7C15ULL;
  18. /** @return A stable nonzero region-specific copy of one process identity field. */
  19. [[nodiscard]] std::uint64_t region_identity(std::uint64_t base, std::int32_t regionIndex) noexcept {
  20. const auto region = static_cast<std::uint64_t>(static_cast<std::uint32_t>(regionIndex));
  21. const std::uint64_t derived = base ^ (kRegionStride * (region + 1U));
  22. return derived == 0 ? kRegionStride : derived;
  23. }
  24. /** @return Group-session key carried by one region's descriptor. */
  25. [[nodiscard]] std::uint64_t region_machine_id(std::int32_t regionIndex) noexcept {
  26. return region_identity(endpoint::identity().machineId, regionIndex);
  27. }
  28. /** No outcome packs to this, so the first advertisement always reports. */
  29. constexpr std::uint64_t kNoOutcome = ~0ULL;
  30. /** Ambassador slot sits above the region index, which holds the low 32 bits. */
  31. constexpr unsigned kSlotShift = 32;
  32. /** Skip reason sits above the ambassador slot. */
  33. constexpr unsigned kReasonShift = 40;
  34. /** Region source sits above the skip reason. */
  35. constexpr unsigned kSourceShift = 48;
  36. /** Why an advertisement was not built. */
  37. enum class Skip : std::uint64_t {
  38. none,
  39. notReady,
  40. noRegion,
  41. slotRange,
  42. noSource,
  43. descriptor,
  44. noHostSession,
  45. hostSessionConflict,
  46. hostSessionFull,
  47. };
  48. std::atomic<std::uint64_t> g_reported{kNoOutcome};
  49. /** Log names for RegionSource, in its declaration order. */
  50. constexpr const char* kSources[] = {"reported", "arrival"};
  51. /** Reports one changed advertisement outcome. */
  52. void report_outcome(Skip skip,
  53. std::int32_t regionIndex,
  54. RegionSource regionSource,
  55. std::uint8_t ambassadorSlot) noexcept {
  56. const auto outcome = static_cast<std::uint64_t>(static_cast<std::uint32_t>(regionIndex))
  57. | (static_cast<std::uint64_t>(ambassadorSlot) << kSlotShift)
  58. | (static_cast<std::uint64_t>(skip) << kReasonShift)
  59. | (static_cast<std::uint64_t>(regionSource) << kSourceShift);
  60. if (g_reported.exchange(outcome, std::memory_order_relaxed) == outcome) {
  61. return;
  62. }
  63. const char* source = kSources[static_cast<std::size_t>(regionSource)];
  64. if (skip == Skip::none) {
  65. report(core::log::Level::info,
  66. "ev=gameplay stage=advertise result=ok region=%d region_source=%s "
  67. "ambassador_slot=%u",
  68. static_cast<int>(regionIndex),
  69. source,
  70. static_cast<unsigned>(ambassadorSlot));
  71. return;
  72. }
  73. /** Stable log labels follow the Skip enumeration's ordinal order. */
  74. static constexpr const char* kReasons[] = {"none",
  75. "not_ready",
  76. "no_region",
  77. "slot_range",
  78. "no_source",
  79. "descriptor",
  80. "no_host_session",
  81. "host_session_conflict",
  82. "host_session_full"};
  83. report(core::log::Level::info,
  84. "ev=gameplay stage=advertise result=skip reason=%s region=%d region_source=%s",
  85. kReasons[static_cast<std::size_t>(skip)],
  86. static_cast<int>(regionIndex),
  87. source);
  88. }
  89. /** @return Encodable ambassador slot that differs from the joining client's slot. */
  90. [[nodiscard]] std::uint8_t ambassador_slot(std::uint8_t localMemberSlot) noexcept {
  91. return localMemberSlot == 0 ? 1 : 0;
  92. }
  93. /** Builds one source-bound candidate and retains its host generation on success. */
  94. [[nodiscard]] Skip build_candidate(const state::activity::SessionBinding& source,
  95. std::int32_t regionIndex,
  96. std::uint8_t localMemberSlot,
  97. message::CitizenAdvertisement& candidate,
  98. std::uint64_t& hostGeneration) noexcept {
  99. candidate = {};
  100. hostGeneration = 0;
  101. if (!endpoint::ready()) {
  102. return Skip::notReady;
  103. }
  104. if (regionIndex < 0) {
  105. return Skip::noRegion;
  106. }
  107. if (localMemberSlot > kMaximumMemberSlot) {
  108. return Skip::slotRange;
  109. }
  110. const endpoint::Identity identity = endpoint::identity();
  111. const state::gameplay::Endpoint advertised = endpoint::advertised();
  112. group::HostSessionBinding host{};
  113. switch (
  114. group::request_host_session(region_machine_id(regionIndex), source, regionIndex, host)) {
  115. case group::HostSessionState::ready:
  116. break;
  117. case group::HostSessionState::pending:
  118. return Skip::noHostSession;
  119. case group::HostSessionState::conflict:
  120. return Skip::hostSessionConflict;
  121. case group::HostSessionState::full:
  122. return Skip::hostSessionFull;
  123. case group::HostSessionState::absent:
  124. default:
  125. return Skip::noSource;
  126. }
  127. if (!group::retain_host_session(host.generation)) {
  128. return Skip::noHostSession;
  129. }
  130. middleware::gameplay::descriptor::JoinEndpoint join{};
  131. join.address = advertised.address;
  132. join.port = advertised.port;
  133. join.machineId = host.groupSessionId;
  134. join.onlineSessionId = region_identity(identity.onlineSessionId, regionIndex);
  135. if (!middleware::gameplay::descriptor::build(join, candidate.descriptor)) {
  136. group::release_host_session(host.generation);
  137. candidate = {};
  138. return Skip::descriptor;
  139. }
  140. candidate.onlineSessionId = host.target.sessionId;
  141. candidate.regionIndex = regionIndex;
  142. candidate.ambassadorSlot = ambassador_slot(localMemberSlot);
  143. candidate.present = true;
  144. hostGeneration = host.generation;
  145. return Skip::none;
  146. }
  147. } // namespace
  148. /** Builds a citizen advertisement from one exact source activity generation. */
  149. void build_advertisement(const state::activity::SessionBinding& source,
  150. std::int32_t regionIndex,
  151. RegionSource regionSource,
  152. std::uint8_t localMemberSlot,
  153. message::CitizenAdvertisement& output,
  154. std::uint64_t& hostGeneration) noexcept {
  155. const Skip skip = build_candidate(source, regionIndex, localMemberSlot, output, hostGeneration);
  156. report_outcome(skip, regionIndex, regionSource, output.ambassadorSlot);
  157. }
  158. /** Claims a missing host row and reports whether its advertisement is ready. */
  159. AdvertisementState advertisement_state(const state::activity::SessionBinding& source,
  160. std::int32_t regionIndex) noexcept {
  161. message::CitizenAdvertisement candidate{};
  162. std::uint64_t generation = 0;
  163. const Skip skip =
  164. build_candidate(source, regionIndex, kQueriedMemberSlot, candidate, generation);
  165. if (generation != 0) {
  166. group::release_host_session(generation);
  167. }
  168. switch (skip) {
  169. case Skip::none:
  170. return AdvertisementState::ready;
  171. case Skip::noHostSession:
  172. return AdvertisementState::pending;
  173. default:
  174. return AdvertisementState::absent;
  175. }
  176. }
  177. /** Source-less publishers remain wire-silent and do not claim a host row. */
  178. void build_advertisement(std::int32_t regionIndex,
  179. RegionSource regionSource,
  180. std::uint8_t localMemberSlot,
  181. message::CitizenAdvertisement& output) noexcept {
  182. static_cast<void>(localMemberSlot);
  183. output = {};
  184. report_outcome(Skip::noSource, regionIndex, regionSource, 0);
  185. }
  186. /** Source-less readiness queries remain absent and do not claim a host row. */
  187. AdvertisementState advertisement_state(std::int32_t regionIndex) noexcept {
  188. static_cast<void>(regionIndex);
  189. return AdvertisementState::absent;
  190. }
  191. } // namespace sunrise::server::gameplay