social_roster_snapshot.cpp 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /**
  2. * Family-two social roster snapshot: the directory and the member record it links to.
  3. *
  4. * The Roster and Fireteam panels draw a name and a blank emblem because family two is answered
  5. * with an empty snapshot. The panel row resolves the emblem with two lookups, not one, and both
  6. * objects have to be resident at the same time for the pair to resolve:
  7. *
  8. * lookup 1: slot 0, keyed by the account soid, gives the directory
  9. * lookup 2: slot 1, keyed by the qword at directory +8, gives the member record
  10. * then the emblem definition index is read from member +36 and its variant from member +38
  11. *
  12. * A full snapshot prunes every object it does not name, so publishing one slot per message can
  13. * never satisfy that chain whichever slot is chosen. Both go out in one message.
  14. */
  15. #include <algorithm>
  16. #include <array>
  17. #include <cstdio>
  18. #include <cstring>
  19. #include <span>
  20. #include "../../../../../core/logging/log.h"
  21. #include "../../../../../middleware/datagen/definitions.h"
  22. #include "../../../../../state/account/inventory/inventory_state.h"
  23. #include "../../../../../state/build_data/items/item_catalog.h"
  24. #include "../../../../../state/runtime/runtime.h"
  25. #include "internal.h"
  26. #include "snapshot_storage.h"
  27. namespace sunrise::server::bap::encrypted::push::snapshot {
  28. namespace {
  29. /** One line carries the soid, the object count and the encoded size. */
  30. constexpr std::size_t kReportCapacity = 160;
  31. /** Where the member record carries the emblem the panel row reads. */
  32. constexpr std::size_t kEmblemDefinitionOffset = 36;
  33. constexpr std::size_t kEmblemVariantOffset = 38;
  34. /**
  35. * A missing definition index is every bit set, and the variant is always sent empty.
  36. *
  37. * The reader tries the variant first and falls back to the definition index when the variant is
  38. * the empty sentinel. Sending a real number there resolves art against a bogus variant entry: a
  39. * light value written to +38 drew a grey placeholder, and a large value stalled the client outright
  40. * because the field indexes a table.
  41. */
  42. constexpr std::uint16_t kEmptyDefinitionIndex = 0xFFFFU;
  43. /**
  44. * Resolves the selected character's equipped emblem to a native definition index.
  45. *
  46. * This has to track the live loadout rather than publish a constant. The client resolves this
  47. * account-keyed object as the account's emblem rather than as roster decoration, so a fixed index
  48. * here pins the emblem globally: character select, inventory and orbit all stop reflecting an equip
  49. * while the equip itself keeps succeeding. Publishing what the player actually has on makes that
  50. * harmless.
  51. *
  52. * @param account Account snapshot, already read under the lock by the caller.
  53. * @param index Receives the native definition index of the equipped emblem.
  54. * @return False when nothing is selected, the emblem slot is empty, or the hash is unknown. Every
  55. * one of those cases publishes the empty sentinel rather than a guess.
  56. */
  57. [[nodiscard]] bool selected_emblem_definition_index(const state::AccountState& account,
  58. std::uint16_t& index,
  59. std::uint32_t& definitionHash) noexcept {
  60. for (const state::CharacterState& character : account.characters) {
  61. if (!character.selected) {
  62. continue;
  63. }
  64. const auto& slot =
  65. character.equipment
  66. .slots[static_cast<std::size_t>(state::account::inventory::EquipmentSlot::emblem)];
  67. if (!slot.has_value()) {
  68. return false;
  69. }
  70. state::build_data::items::Definition definition{};
  71. if (!state::build_data::items::find_hash(slot->definitionHash, definition)) {
  72. return false;
  73. }
  74. index = definition.definitionIndex;
  75. definitionHash = slot->definitionHash;
  76. return true;
  77. }
  78. return false;
  79. }
  80. } // namespace
  81. /** Builds the family-two snapshot carrying the social roster directory and member record. */
  82. bool prepare_social_roster(Scratch& scratch,
  83. const middleware::queuez::Subscription& subscription,
  84. std::uint32_t objectId,
  85. const Reservation& reservation,
  86. Prepared& prepared) noexcept {
  87. // Both slot ids are resolved here, so the caller's single id is not used.
  88. (void)objectId;
  89. const state::AccountState account = state::account_snapshot();
  90. if (account.primarySoid == 0 || reservation.rawWriteOffset > scratch.plaintext.size()) {
  91. return report_failure("social_roster_state");
  92. }
  93. const auto destination = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
  94. constexpr std::size_t kTotal = middleware::datagen::kSocialRosterDirectorySize
  95. + middleware::datagen::kSocialRosterMemberSize;
  96. if (destination.size() < kTotal) {
  97. return report_failure("social_roster_storage");
  98. }
  99. std::uint16_t emblem = kEmptyDefinitionIndex;
  100. std::uint32_t emblemHash = 0;
  101. if (!selected_emblem_definition_index(account, emblem, emblemHash)) {
  102. emblem = kEmptyDefinitionIndex;
  103. }
  104. Prepared staged{};
  105. std::size_t objectCount = 0;
  106. std::size_t compressedExtent = reservation.compressedWriteOffset;
  107. std::size_t rawUsed = 0;
  108. /**
  109. * Writes one object and stages it.
  110. *
  111. * The two bodies are not interchangeable, because both lookups match on the object's first
  112. * qword. The directory leads with the account soid the row searches by and carries the link at
  113. * +8; the member record leads with that same link so the second lookup finds it. The account
  114. * soid serves as the link because it is already proven to route.
  115. *
  116. * Only the member record carries the emblem. The directory is read for two flag bits and
  117. * nothing else, so a copy of the pair there changes nothing.
  118. */
  119. const auto emit = [&](std::size_t size, std::uint32_t id, bool directory) noexcept {
  120. if (objectCount >= staged.objects.size() || size < kEmblemVariantOffset + sizeof emblem) {
  121. return false;
  122. }
  123. const auto body = destination.subspan(rawUsed, size);
  124. std::fill(body.begin(), body.end(), std::byte{});
  125. if (directory) {
  126. std::memcpy(body.data(), &account.primarySoid, sizeof account.primarySoid);
  127. std::memcpy(body.data() + sizeof account.primarySoid,
  128. &account.primarySoid,
  129. sizeof account.primarySoid);
  130. } else {
  131. std::memcpy(body.data(), &account.primarySoid, sizeof account.primarySoid);
  132. std::memcpy(body.data() + kEmblemDefinitionOffset, &emblem, sizeof emblem);
  133. std::memcpy(body.data() + kEmblemVariantOffset,
  134. &kEmptyDefinitionIndex,
  135. sizeof kEmptyDefinitionIndex);
  136. }
  137. std::size_t compressedSize = 0;
  138. if (!compress_object(scratch,
  139. body,
  140. id,
  141. account.primarySoid,
  142. compressedExtent,
  143. staged.objects[objectCount],
  144. compressedSize)) {
  145. return false;
  146. }
  147. compressedExtent += compressedSize;
  148. rawUsed += size;
  149. ++objectCount;
  150. return true;
  151. };
  152. // The directory goes first so a partial land reads as the directory surviving without a member
  153. // record, rather than as an unexplained miss.
  154. if (!emit(middleware::datagen::kSocialRosterDirectorySize,
  155. middleware::datagen::kSocialRosterDirectoryObjectId,
  156. true)) {
  157. return report_failure("social_roster_directory");
  158. }
  159. if (!emit(middleware::datagen::kSocialRosterMemberSize,
  160. middleware::datagen::kSocialRosterMemberObjectId,
  161. false)) {
  162. return report_failure("social_roster_member");
  163. }
  164. staged.rawClearSize =
  165. (std::max)(reservation.rawClearSize, reservation.rawWriteOffset + rawUsed);
  166. staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
  167. staged.family = middleware::queuez::Family{
  168. subscription.familyType,
  169. subscription.familyRootSoid,
  170. kInitialFamilyVersion,
  171. middleware::queuez::kFullSnapshotFlag,
  172. std::span(staged.objects).first(objectCount),
  173. };
  174. if (!commit(staged, prepared)) {
  175. return report_failure("social_roster_commit");
  176. }
  177. std::array<char, kReportCapacity> line{};
  178. const int written = std::snprintf(line.data(),
  179. line.size(),
  180. "ev=queuez stage=social_roster result=ok soid=0x%016llX"
  181. " objects=%zu bytes=%zu emblem=%u hash=0x%08X",
  182. static_cast<unsigned long long>(account.primarySoid),
  183. objectCount,
  184. rawUsed,
  185. static_cast<unsigned>(emblem),
  186. static_cast<unsigned>(emblemHash));
  187. if (written > 0) {
  188. core::log::write(core::log::Channel::server,
  189. core::log::Level::info,
  190. {line.data(), static_cast<std::size_t>(written)});
  191. }
  192. return true;
  193. }
  194. } // namespace sunrise::server::bap::encrypted::push::snapshot