| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- #include <array>
- #include <cstdio>
- #include <span>
- #include "../../../../../core/logging/log.h"
- #include "../../../../../middleware/datagen/definitions.h"
- #include "../../../../../middleware/secure_channel/runtime.h"
- #include "../../../../../state/account/account_state.h"
- #include "../../../../../state/build_data/runtime.h"
- #include "../../../../../state/runtime/runtime.h"
- #include "../../queuez/queuez_state_validation.h"
- #include "../snapshot/snapshot.h"
- #include "queuez_push_reporting.h"
- #include "queuez_update_frame.h"
- namespace sunrise::server::bap::encrypted::push {
- namespace {
- /** One line carries the refusal key and nothing else. */
- constexpr std::size_t kSkipLineCapacity = 96;
- /**
- * Reports the banner move declining to build a frame.
- * The move has 4 separate refusals and all leave the emblem where it is, so the key is the only
- * way to tell a pick owing nothing from a ladder that was never seeded.
- * @param reason Key naming the refusal.
- */
- void report_skip(const char* reason) noexcept {
- std::array<char, kSkipLineCapacity> line{};
- const int written = std::snprintf(
- line.data(), line.size(), "ev=queuez stage=banner_move result=skip reason=%s", reason);
- if (written > 0) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- {line.data(), static_cast<std::size_t>(written)});
- }
- }
- /** Validates and appends one same-character Family-0 appearance refresh. */
- [[nodiscard]] bool append_appearance_frame(Scratch& scratch,
- const queuez::CharacterAppearanceRefresh& refresh,
- snapshot::Prepared& prepared,
- const char* stage,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- const std::size_t objectCount = prepared.family.objects.size();
- const std::size_t beforeBytes = written;
- const bool replacement =
- objectCount >= 2
- && prepared.family.objects.front().id == middleware::datagen::kBannerCharacterObjectId
- && prepared.family.objects.front().version == refresh.characterSoid
- && prepared.family.objects.front().payload.empty();
- const std::size_t characterIndex = replacement ? 1U : 0U;
- const bool hasAnchor = objectCount == characterIndex + 2U;
- if ((objectCount != characterIndex + 1U && !hasAnchor)
- || prepared.family.type != queuez::kBannerFamilyType
- || prepared.family.rootSoid != refresh.after.family4RootSoid
- || prepared.family.version != refresh.after.family0Version || prepared.family.flags != 0
- || (replacement
- && prepared.family.objects.front().encoding != middleware::queuez::Encoding::raw)
- || prepared.family.objects[characterIndex].id
- != middleware::datagen::kBannerCharacterObjectId
- || prepared.family.objects[characterIndex].version != refresh.characterSoid
- || prepared.family.objects[characterIndex].encoding != middleware::queuez::Encoding::oodle
- || prepared.family.objects[characterIndex].payload.empty()
- || (hasAnchor
- && (prepared.family.objects.back().id != middleware::datagen::kBannerAnchorObjectId
- || prepared.family.objects.back().version != refresh.after.family4RootSoid
- || prepared.family.objects.back().encoding != middleware::queuez::Encoding::oodle
- || prepared.family.objects.back().payload.empty()))
- || !queuez_frame::append(scratch,
- prepared.family,
- prepared.rawClearSize,
- prepared.compressedClearSize,
- key,
- nonce,
- response,
- written)) {
- return false;
- }
- middleware::secure_channel::advance_nonce(nonce);
- queuez_report::push(stage, queuez::kBannerFamilyType, objectCount, written - beforeBytes, 1);
- return true;
- }
- /** Validates and appends one incremental Family-3 appearance frame. */
- [[nodiscard]] bool
- append_roster_appearance_frame(Scratch& scratch,
- const queuez::RosterAppearanceRefresh& refresh,
- snapshot::Prepared& prepared,
- const char* stage,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- const std::size_t expectedObjects = refresh.includeRoster ? 2U : 1U;
- const std::size_t objectCount = prepared.family.objects.size();
- const std::size_t beforeBytes = written;
- if (objectCount != expectedObjects || prepared.family.type != queuez::kRosterFamilyType
- || prepared.family.rootSoid != refresh.after.family3RootSoid
- || prepared.family.version != refresh.after.family3Version || prepared.family.flags != 0
- || prepared.family.objects.front().id != middleware::datagen::kRosterCharacterObjectId
- || prepared.family.objects.front().version != refresh.characterSoid
- || prepared.family.objects.front().encoding != middleware::queuez::Encoding::oodle
- || prepared.family.objects.front().payload.empty()
- || (refresh.includeRoster
- && (prepared.family.objects.back().id != middleware::datagen::kRosterObjectId
- || prepared.family.objects.back().version != refresh.after.family3RootSoid
- || prepared.family.objects.back().encoding != middleware::queuez::Encoding::oodle
- || prepared.family.objects.back().payload.empty()))
- || !queuez_frame::append(scratch,
- prepared.family,
- prepared.rawClearSize,
- prepared.compressedClearSize,
- key,
- nonce,
- response,
- written)) {
- return false;
- }
- middleware::secure_channel::advance_nonce(nonce);
- queuez_report::push(stage, queuez::kRosterFamilyType, objectCount, written - beforeBytes, 1);
- return true;
- }
- } // namespace
- /**
- * Appends the unsolicited family-zero banner pair that follows a family-three subscription.
- * Sent twice per boot at the same version: family zero hits the state-1 race with no svc-12
- * re-push behind it, so a rejected first pair would blank the banner for the run.
- * @param scratch Lock-owned transform buffers.
- * @param familyRootSoid Root the Client subscribed for Family 3.
- * @param key Active AES-GCM session key.
- * @param nonce Push-direction nonce, advanced only by a complete frame.
- * @param response Caller-owned output containing prior frames.
- * @param written Existing byte count, updated by a complete frame.
- * @return True when the banner frame is appended.
- */
- bool append_banner_notification(Scratch& scratch,
- const queuez::SessionState& before,
- std::uint64_t familyRootSoid,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written,
- queuez::SessionState& after) noexcept {
- after = before;
- // The pair names the first character when none is picked yet. The client's family-zero record
- // accepts a snapshot for about ten seconds, then clears the family and refuses every later
- // one, so holding the pair for the pick spends that window and the subscription times out.
- if (state::account::banner_character_soid(state::account_snapshot()) == 0) {
- core::log::write(core::log::Channel::server,
- core::log::Level::info,
- "ev=queuez stage=banner result=skip reason=nocharacter");
- return false;
- }
- snapshot::Prepared prepared{};
- // The unsolicited pair is the family's first delivery, so it carries the full-snapshot flag.
- if (!snapshot::prepare_banner(
- scratch, familyRootSoid, queuez::kInitialFamilyVersion, 0, prepared)) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=banner result=fail reason=prepare");
- return false;
- }
- const std::size_t objectCount = prepared.family.objects.size();
- const std::size_t beforeBytes = written;
- if (!queuez_frame::append(scratch,
- prepared.family,
- prepared.rawClearSize,
- prepared.compressedClearSize,
- key,
- nonce,
- response,
- written)) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=banner result=fail reason=frame");
- return false;
- }
- middleware::secure_channel::advance_nonce(nonce);
- // The Client now holds this pair, so the ladder owns it. Without this an unsubscribe leaves
- // family zero unrecorded and the next pick has no previous record to release.
- const std::uint64_t delivered =
- state::account::banner_character_soid(state::account_snapshot());
- if (!after.family0Active && delivered != 0) {
- after.family0Active = true;
- after.family0Character = delivered;
- after.family0Version = queuez::kInitialFamilyVersion;
- }
- queuez_report::push("banner",
- prepared.family.type,
- objectCount,
- written - beforeBytes,
- queuez_report::kNoRecordOutcome);
- return true;
- }
- /**
- * Appends the family-zero move that follows an opcode-504 pick.
- * The Client holds the objIdx-1 buffer for one character at a time, allocated from the character
- * the anchor names, so the pair moves with the pick or the banner keeps the old emblem.
- * @param scratch Lock-owned transform buffers.
- * @param before Queuez state after the family-four move.
- * @param selectedCharacter Character the pick named.
- * @param key Active AES-GCM session key.
- * @param nonce Push-direction nonce, advanced only by a complete frame.
- * @param response Caller-owned output containing prior frames.
- * @param written Existing byte count, updated by a complete frame.
- * @param after Receives the state published once the frame is copied.
- * @return True when a frame went out and `after` carries the advanced ladder.
- */
- bool append_banner_move_notification(Scratch& scratch,
- const queuez::SessionState& before,
- std::uint64_t selectedCharacter,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written,
- queuez::SessionState& after) noexcept {
- bool publish = false;
- bool incremental = false;
- after = before;
- // A pick that names the character the pair already holds owes nothing, and so does a family
- // zero that has not had its first delivery yet.
- const char* reason = nullptr;
- if (!queuez::stage_family0_subscription(
- before, selectedCharacter, publish, incremental, after)) {
- reason = "stage";
- } else if (!publish) {
- reason = "unchanged";
- } else if (before.family4RootSoid == 0) {
- reason = "no_root";
- }
- if (reason != nullptr) {
- report_skip(reason);
- after = before;
- return false;
- }
- snapshot::Prepared prepared{};
- // A first delivery releases nothing: the Client holds no record for this family yet, so the
- // pair goes out as its own full snapshot instead of as a move off a previous character.
- if (!snapshot::prepare_banner(scratch,
- before.family4RootSoid,
- after.family0Version,
- incremental ? before.family0Character : 0,
- prepared)) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=banner_move result=fail reason=prepare");
- after = before;
- return false;
- }
- const std::size_t objectCount = prepared.family.objects.size();
- const std::size_t beforeBytes = written;
- if (!queuez_frame::append(scratch,
- prepared.family,
- prepared.rawClearSize,
- prepared.compressedClearSize,
- key,
- nonce,
- response,
- written)) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=banner_move result=fail reason=frame");
- after = before;
- return false;
- }
- middleware::secure_channel::advance_nonce(nonce);
- queuez_report::push("banner_move",
- prepared.family.type,
- objectCount,
- written - beforeBytes,
- queuez_report::kNoRecordOutcome);
- return true;
- }
- /** Appends one same-character Family-0 appearance-record upsert after an equipment swap. */
- bool append_equipment_appearance_refresh_notification(
- Scratch& scratch,
- const queuez::CharacterAppearanceRefresh& refresh,
- const state::PendingEquipmentSwap& mutation,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- if (!mutation.prepared || mutation.characterSoid != refresh.characterSoid) {
- return false;
- }
- snapshot::Prepared prepared{};
- if (!snapshot::prepare_character_appearance_refresh(scratch,
- refresh,
- mutation.afterCharacter,
- mutation.characterIndex,
- mutation.nativeEquipmentSlot,
- false,
- prepared)) {
- return false;
- }
- return append_appearance_frame(
- scratch, refresh, prepared, "equip_appearance", key, nonce, response, written);
- }
- /** Appends one Family-0 record upsert after a socket change on an equipped item. */
- bool append_socket_appearance_refresh_notification(
- Scratch& scratch,
- const queuez::CharacterAppearanceRefresh& refresh,
- const state::PendingSocketPlug& mutation,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- if (!mutation.prepared || !mutation.targetEquipped
- || mutation.characterSoid != refresh.characterSoid
- || mutation.itemIndex >= mutation.afterCharacter.equipment.slots.size()
- || !mutation.afterCharacter.equipment.slots[mutation.itemIndex].has_value()) {
- return false;
- }
- const state::account::inventory::Item& target =
- *mutation.afterCharacter.equipment.slots[mutation.itemIndex];
- if (target.instanceSoid != mutation.targetInstanceSoid) {
- return false;
- }
- state::build_data::items::details::Definition detail{};
- if (!state::build_data::find_configured_item_detail(mutation.targetDefinitionIndex, detail)
- || detail.definitionIndex != mutation.targetDefinitionIndex
- || detail.definitionHash != mutation.targetDefinitionHash
- || detail.bucketId != mutation.targetBucketId || !detail.equipmentSlot.has_value()
- || *detail.equipmentSlot < 0
- || static_cast<std::size_t>(*detail.equipmentSlot)
- >= state::build_data::items::details::kEquipmentSlotCount) {
- return false;
- }
- snapshot::Prepared prepared{};
- if (!snapshot::prepare_character_appearance_refresh(
- scratch,
- refresh,
- mutation.afterCharacter,
- mutation.characterIndex,
- static_cast<std::uint8_t>(*detail.equipmentSlot),
- true,
- prepared)) {
- return false;
- }
- return append_appearance_frame(
- scratch, refresh, prepared, "socket_appearance", key, nonce, response, written);
- }
- /** Appends the Family-3 character-then-roster refresh owed by one equipment mutation. */
- bool append_equipment_roster_refresh_notification(
- Scratch& scratch,
- const queuez::RosterAppearanceRefresh& refresh,
- const state::PendingEquipmentSwap& mutation,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- if (!mutation.prepared || !refresh.includeRoster
- || mutation.characterSoid != refresh.characterSoid) {
- return false;
- }
- snapshot::Prepared prepared{};
- if (!snapshot::prepare_roster_appearance_refresh(
- scratch, refresh, mutation.afterCharacter, mutation.characterIndex, prepared)) {
- return false;
- }
- return append_roster_appearance_frame(
- scratch, refresh, prepared, "equip_roster", key, nonce, response, written);
- }
- /** Appends the Family-3 character-only refresh owed by a socket change on equipped gear. */
- bool append_socket_roster_refresh_notification(Scratch& scratch,
- const queuez::RosterAppearanceRefresh& refresh,
- const state::PendingSocketPlug& mutation,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- if (!mutation.prepared || !mutation.targetEquipped || refresh.includeRoster
- || mutation.characterSoid != refresh.characterSoid
- || mutation.itemIndex >= mutation.afterCharacter.equipment.slots.size()
- || !mutation.afterCharacter.equipment.slots[mutation.itemIndex].has_value()
- || mutation.afterCharacter.equipment.slots[mutation.itemIndex]->instanceSoid
- != mutation.targetInstanceSoid) {
- return false;
- }
- snapshot::Prepared prepared{};
- if (!snapshot::prepare_roster_appearance_refresh(
- scratch, refresh, mutation.afterCharacter, mutation.characterIndex, prepared)) {
- return false;
- }
- return append_roster_appearance_frame(
- scratch, refresh, prepared, "socket_roster", key, nonce, response, written);
- }
- /** Refreshes the selected character's complete Family-0 appearance from committed State. */
- bool append_account_resync_appearance_notification(
- Scratch& scratch,
- const queuez::SessionState& before,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written,
- queuez::SessionState& after) noexcept {
- after = before;
- if (!before.family0Active) {
- return true;
- }
- const state::AccountState account = state::account_snapshot();
- const std::uint64_t selected = state::account::selected_character_soid(account);
- if (selected == 0) {
- return false;
- }
- if (before.family0Character != selected) {
- return append_banner_move_notification(
- scratch, before, selected, key, nonce, response, written, after);
- }
- std::size_t characterIndex = account.characterCount;
- for (std::size_t index = 0; index < account.characterCount; ++index) {
- if (account.characters[index].soid == selected) {
- characterIndex = index;
- break;
- }
- }
- queuez::CharacterAppearanceRefresh refresh{};
- snapshot::Prepared prepared{};
- if (characterIndex >= account.characterCount
- || !queuez::stage_character_appearance_refresh(before, selected, refresh)
- || !snapshot::prepare_character_appearance_refresh(
- scratch, refresh, account.characters[characterIndex], characterIndex, 0, true, prepared)
- || !append_appearance_frame(
- scratch, refresh, prepared, "peer_resync_appearance", key, nonce, response, written)) {
- return false;
- }
- after = refresh.after;
- return true;
- }
- /** Refreshes the selected character and its account roster from committed State. */
- bool append_account_resync_roster_notification(Scratch& scratch,
- const queuez::SessionState& before,
- std::span<const std::byte, state::kAesKeySize> key,
- std::array<std::byte, state::kBapNonceSize>& nonce,
- std::span<std::byte> response,
- std::size_t& written,
- queuez::SessionState& after) noexcept {
- after = before;
- if (!before.family3Active) {
- return true;
- }
- const state::AccountState account = state::account_snapshot();
- const std::uint64_t selected = state::account::selected_character_soid(account);
- std::size_t characterIndex = account.characterCount;
- for (std::size_t index = 0; index < account.characterCount; ++index) {
- if (account.characters[index].soid == selected) {
- characterIndex = index;
- break;
- }
- }
- queuez::RosterAppearanceRefresh refresh{};
- snapshot::Prepared prepared{};
- if (selected == 0 || characterIndex >= account.characterCount
- || !queuez::stage_roster_appearance_refresh(before, selected, true, refresh)
- || !snapshot::prepare_roster_appearance_refresh(
- scratch, refresh, account.characters[characterIndex], characterIndex, prepared)
- || !append_roster_appearance_frame(
- scratch, refresh, prepared, "peer_resync_roster", key, nonce, response, written)) {
- return false;
- }
- after = refresh.after;
- return true;
- }
- } // namespace sunrise::server::bap::encrypted::push
|