service_outcome_commit.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. #include "service_outcome_commit.h"
  2. #include <array>
  3. #include <cstdio>
  4. #include "../../../../client/content/investment/worker.h"
  5. #include "../../../../core/logging/log.h"
  6. #include "../../../../state/activity/runtime.h"
  7. #include "../../../../state/matchmaking/matchmaking_state.h"
  8. #include "../../../../state/runtime/runtime.h"
  9. #include "../bap_connection_publication.h"
  10. #include "../internal.h"
  11. namespace sunrise::server::bap::encrypted::transactions {
  12. namespace {
  13. namespace slots = state::activity::entity_slots;
  14. /** Log names for each lease operation, in the enum's own order. */
  15. constexpr std::array<const char*, 4> kLeaseKinds = {"none", "join", "grant", "release"};
  16. /** Retains one newly committed private ActivityClient generation for its BAP link. */
  17. [[nodiscard]] bool retain_private(std::uint64_t sessionId, Publication& publication) noexcept {
  18. state::activity::SessionBinding binding{};
  19. if (!state::activity::snapshot_binding(sessionId, binding)
  20. || !state::activity::retain_binding(binding)) {
  21. return false;
  22. }
  23. publication.activity.session = binding;
  24. publication.activity.source = binding;
  25. publication.activity.role = ActivityClientRole::privateCurrent;
  26. publication.hasActivitySessionBinding = true;
  27. return true;
  28. }
  29. /** Retains one exact advertised public target before its join mutation commits. */
  30. [[nodiscard]] bool retain_public(const activity_message::ActivityPlan& plan,
  31. Publication& publication) noexcept {
  32. server::gameplay::group::HostSessionBinding current{};
  33. if (!server::gameplay::group::host_session_for_activity(plan.sessionId, current)
  34. || current.generation != plan.publicHost.generation
  35. || current.groupSessionId != plan.publicHost.groupSessionId
  36. || current.regionIndex != plan.publicHost.regionIndex
  37. || current.source.sessionId != plan.publicHost.source.sessionId
  38. || current.source.createdRevision != plan.publicHost.source.createdRevision
  39. || current.target.sessionId != plan.publicHost.target.sessionId
  40. || current.target.createdRevision != plan.publicHost.target.createdRevision
  41. || !state::activity::binding_matches(current.source)
  42. || !state::activity::binding_matches(current.target)
  43. || !server::gameplay::group::retain_host_session(current.generation)) {
  44. return false;
  45. }
  46. if (!state::activity::retain_binding(current.target)) {
  47. server::gameplay::group::release_host_session(current.generation);
  48. return false;
  49. }
  50. publication.activity.session = current.target;
  51. publication.activity.source = current.source;
  52. publication.activity.groupSessionId = current.groupSessionId;
  53. publication.activity.hostGeneration = current.generation;
  54. publication.activity.advertisedRegion = current.regionIndex;
  55. publication.activity.role = ActivityClientRole::publicTarget;
  56. publication.hasActivitySessionBinding = true;
  57. return true;
  58. }
  59. /** Releases provisional activity owners when the following State commit fails. */
  60. void discard_activity_publication(Publication& publication) noexcept {
  61. if (publication.activity.hostGeneration != 0) {
  62. server::gameplay::group::release_host_session(publication.activity.hostGeneration);
  63. }
  64. if (publication.activity.session.sessionId != state::activity::kAbsentSessionId) {
  65. state::activity::release_binding(publication.activity.session);
  66. }
  67. publication = {};
  68. }
  69. /**
  70. * Reports one entity-slot lease change.
  71. * The client prints only `failed to create` when it has no free index, and nothing else on this
  72. * path reports the lease, so a failed create reads the same as an empty grant without this line.
  73. * @param mutation Plan as it was before the commit consumed it.
  74. * @param committed Whether the commit succeeded.
  75. */
  76. void report_lease(const slots::PendingMutation& mutation, bool committed) noexcept {
  77. std::size_t held = 0;
  78. std::size_t reserved = 0;
  79. const bool known = slots::lease_counts(mutation.sessionId, held, reserved);
  80. const auto kind = static_cast<std::size_t>(mutation.kind);
  81. std::array<char, core::log::kLineCapacity> line{};
  82. const int written =
  83. std::snprintf(line.data(),
  84. line.size(),
  85. "ev=activity stage=entity_slots result=%s kind=%s soid=0x%llX "
  86. "requested=%zu picked=%zu held=%zu reserved=%zu known=%u",
  87. committed ? "ok" : "fail",
  88. kind < kLeaseKinds.size() ? kLeaseKinds[kind] : "bad",
  89. static_cast<unsigned long long>(mutation.sessionId),
  90. mutation.requestedCount,
  91. slots::slot_count(mutation.mask),
  92. held,
  93. reserved,
  94. known ? 1U : 0U);
  95. if (written > 0) {
  96. core::log::write(core::log::Channel::server,
  97. committed ? core::log::Level::debug : core::log::Level::warn,
  98. {line.data(), static_cast<std::size_t>(written)});
  99. }
  100. }
  101. } // namespace
  102. /**
  103. * Commits at most one delayed State transaction.
  104. * @param outcome Checked service result whose pending transaction is used up.
  105. * @param publication Gets connection fields to publish after the output copy.
  106. * @return True when there is no transaction, or the one transaction commits.
  107. */
  108. bool commit(ServiceOutcome& outcome, Publication& publication) noexcept {
  109. publication = {};
  110. if (auto* allocation = transaction_if<state::activity::PendingAllocation>(outcome)) {
  111. const std::uint64_t sessionId = allocation->sessionId;
  112. std::uint64_t bindingGeneration = 0;
  113. if (sessionId == state::activity::kAbsentSessionId
  114. || !reserve_activity_binding_generation(bindingGeneration)
  115. || !state::activity::commit(*allocation)) {
  116. return false;
  117. }
  118. if (!retain_private(sessionId, publication)) {
  119. static_cast<void>(state::activity::release_session(sessionId));
  120. return false;
  121. }
  122. publication.activity.bindingGeneration = bindingGeneration;
  123. return true;
  124. }
  125. if (auto* plan = transaction_if<activity_message::ActivityPlan>(outcome)) {
  126. if (plan->mutationDomain == activity_message::MutationDomain::entitySlots) {
  127. const bool joins = plan->delivery == activity_message::Delivery::joinNotifications;
  128. const bool validJoinIntent =
  129. plan->bindingIntent == activity_message::BindingIntent::preserveCurrent
  130. || plan->bindingIntent == activity_message::BindingIntent::publicTarget;
  131. if (joins && !validJoinIntent) {
  132. return false;
  133. }
  134. std::uint64_t bindingGeneration = 0;
  135. if (joins && !reserve_activity_binding_generation(bindingGeneration)) {
  136. return false;
  137. }
  138. if (joins && plan->bindingIntent == activity_message::BindingIntent::publicTarget
  139. && !retain_public(*plan, publication)) {
  140. return false;
  141. }
  142. // The commit consumes the plan, so the counts are taken from a copy of it.
  143. const slots::PendingMutation attempted = plan->entitySlotMutation;
  144. const bool committed = slots::commit(plan->entitySlotMutation);
  145. report_lease(attempted, committed);
  146. if (!committed) {
  147. discard_activity_publication(publication);
  148. return false;
  149. }
  150. // The keepalive only finds a link that is bound to a session. A link that allocated
  151. // its own session carries the same id, so this rebinds it to itself.
  152. if (joins && plan->sessionId != state::activity::kAbsentSessionId) {
  153. publication.hasActivitySessionBinding = true;
  154. if (plan->bindingIntent == activity_message::BindingIntent::preserveCurrent) {
  155. publication.preservesActivitySessionBinding = true;
  156. } else if (plan->bindingIntent != activity_message::BindingIntent::publicTarget) {
  157. discard_activity_publication(publication);
  158. return false;
  159. }
  160. publication.activity.bindingGeneration = bindingGeneration;
  161. }
  162. return true;
  163. }
  164. if (plan->mutationDomain == activity_message::MutationDomain::membership) {
  165. return state::activity::membership::commit(plan->membershipMutation);
  166. }
  167. // The retained patch epoch is connection state, so it commits nothing here.
  168. return plan->mutationDomain == activity_message::MutationDomain::patchEpoch;
  169. }
  170. if (auto* mutation = transaction_if<state::matchmaking::PendingMutation>(outcome)) {
  171. return state::matchmaking::commit(*mutation);
  172. }
  173. if (auto* transaction = transaction_if<EquipmentSwapTransaction>(outcome)) {
  174. const bool isSubclassSlot =
  175. transaction->pending.equipmentSlotIndex
  176. == static_cast<std::size_t>(state::account::inventory::EquipmentSlot::subclass);
  177. const bool committed = state::commit_equipment_swap(transaction->pending);
  178. core::log::write(core::log::Channel::server,
  179. committed ? core::log::Level::debug : core::log::Level::warn,
  180. committed ? "ev=equip stage=transaction_commit result=ok"
  181. : "ev=equip stage=transaction_commit result=fail");
  182. if (committed && isSubclassSlot) {
  183. // The equipped subclass just changed, which makes the published ability buckets
  184. // stale the same way an ability-entry pick does. Wake the investment worker so the
  185. // character screen stops showing the previous subclass's resolution.
  186. client::content::investment::worker::request_slice();
  187. }
  188. return committed;
  189. }
  190. if (auto* transaction = transaction_if<SubclassSelectionTransaction>(outcome)) {
  191. const bool committed = state::commit_subclass_selection(transaction->pending);
  192. core::log::write(core::log::Channel::server,
  193. committed ? core::log::Level::debug : core::log::Level::warn,
  194. committed ? "ev=subclass_select stage=transaction_commit result=ok"
  195. : "ev=subclass_select stage=transaction_commit result=fail");
  196. if (committed) {
  197. // The published ability buckets are keyed off the selection that just changed; wake
  198. // the investment worker so its next pump rebuilds them instead of waiting on whatever
  199. // cadence would otherwise trigger a fresh slice.
  200. client::content::investment::worker::request_slice();
  201. }
  202. return committed;
  203. }
  204. if (auto* transaction = transaction_if<ItemAcquisitionTransaction>(outcome)) {
  205. const bool committed = state::commit_item_acquisition(transaction->pending);
  206. core::log::write(core::log::Channel::server,
  207. committed ? core::log::Level::debug : core::log::Level::warn,
  208. committed ? "ev=acquire stage=transaction_commit result=ok"
  209. : "ev=acquire stage=transaction_commit result=fail");
  210. return committed;
  211. }
  212. if (auto* transaction = transaction_if<SocketPlugTransaction>(outcome)) {
  213. const bool committed = state::commit_socket_plug(transaction->pending);
  214. core::log::write(core::log::Channel::server,
  215. committed ? core::log::Level::debug : core::log::Level::warn,
  216. committed ? "ev=socket_plug stage=transaction_commit result=ok"
  217. : "ev=socket_plug stage=transaction_commit result=fail");
  218. return committed;
  219. }
  220. if (auto* transaction = transaction_if<ItemStateTransaction>(outcome)) {
  221. const bool committed = state::commit_item_state(transaction->pending);
  222. core::log::write(core::log::Channel::server,
  223. committed ? core::log::Level::debug : core::log::Level::warn,
  224. committed ? "ev=item_state stage=transaction_commit result=ok"
  225. : "ev=item_state stage=transaction_commit result=fail");
  226. return committed;
  227. }
  228. if (auto* transaction = transaction_if<ProfileItemAcquisitionTransaction>(outcome)) {
  229. const bool committed = state::commit_profile_item_acquisition(transaction->pending);
  230. core::log::write(core::log::Channel::server,
  231. committed ? core::log::Level::debug : core::log::Level::warn,
  232. committed ? "ev=profile_acquire stage=transaction_commit result=ok"
  233. : "ev=profile_acquire stage=transaction_commit result=fail");
  234. return committed;
  235. }
  236. if (auto* transaction = transaction_if<ItemDismantleTransaction>(outcome)) {
  237. const bool committed = state::commit_item_dismantle(transaction->pending);
  238. core::log::write(core::log::Channel::server,
  239. committed ? core::log::Level::debug : core::log::Level::warn,
  240. committed ? "ev=dismantle stage=transaction_commit result=ok"
  241. : "ev=dismantle stage=transaction_commit result=fail");
  242. return committed;
  243. }
  244. return true;
  245. }
  246. } // namespace sunrise::server::bap::encrypted::transactions