#include #include #include #include #include "../../../../core/logging/log.h" #include "../../../../middleware/bap/account_translation/account_translation_response.h" #include "../../../../middleware/bap/activity_host/activity_host_response.h" #include "../../../../middleware/bap/certificate.h" #include "../../../../middleware/bap/client_config/client_config_response.h" #include "../../../../middleware/bap/family_subscription.h" #include "../../../../middleware/bap/family_unsubscription.h" #include "../../../../middleware/bap/user_message/user_message_response.h" #include "../../../../middleware/encoding/byte_order.h" #include "../../../../middleware/web_service/messages/opcode501_request_codec.h" #include "../../../../middleware/web_service/messages/opcode502.h" #include "../../../../middleware/web_service/messages/opcode505/opcode505_codec.h" #include "../../../../state/activity/membership/activity_membership_query.h" #include "../../../../state/runtime/runtime.h" #include "../../../gameplay/group/group_host_sessions.h" #include "../../../web_service/opcode_routes.h" #include "../../../web_service/web_service_runtime.h" #include "../activity_host_manager/activity_host_manager_route.h" #include "../activity_message/activity_message_route.h" #include "../internal.h" #include "../matchmaking/matchmaking_route.h" #include "../queuez/queuez_state_validation.h" #include "state/investment/store_internal.h" namespace sunrise::server::bap::encrypted::body { namespace { /** Process-wide identity paired with the single account SOID. */ std::atomic g_translatedIdentity{0}; /** Replaces an optimistic Web Service reply when its deferred transaction cannot be staged. */ [[nodiscard]] bool refuse_web_action(const middleware::web_service::Message& message, std::span output, std::size_t& written) noexcept { // The refusal is the same record as the success, so it takes the opcode's own shape. middleware::web_service::ResponseShape shape{}; sunrise::server::web_service::resolve_response_shape(message.opcode, shape); middleware::web_service::StatusResponse status{}; status.code = 1; status.value = middleware::web_service::kNoFamily4Publication; return middleware::web_service::encode_response(message, shape, status, output, written); } /** Accepts only the first account-translation identity and its retries. */ [[nodiscard]] bool pairs_identity(std::uint64_t identity) noexcept { std::uint64_t claimed = 0; // A repeat of the same identity still pairs: the peer re-asks until the flag sticks. return g_translatedIdentity.compare_exchange_strong( claimed, identity, std::memory_order_relaxed) || claimed == identity; } } // namespace /** * Processes the body for one authenticated service route. * @param route Service route data found earlier. * @param queuezState Queuez versions and residents set up by this BAP peer. * @param activity Exact ActivityClient generation owned by this BAP session. * @param matchmakingContext State-owned logical context for this BAP session. * @param requestBody Borrowed decrypted request body. * @param output Caller-owned response-body storage. * @param written Receives encoded body bytes. * @param outcome Receives one validated transport action or deferred State transaction. * @return True when the chosen body codec succeeds. */ bool process(const ServiceRoute& route, const queuez::SessionState& queuezState, const ActivityClientBinding& activity, const RosterDecodeMap& rosterDecode, state::matchmaking::ContextHandle matchmakingContext, std::span requestBody, std::span output, std::size_t& written, ServiceOutcome& outcome, std::span presentation) noexcept { outcome = {}; switch (route.bodyCodec) { case BodyCodec::empty: written = 0; return true; case BodyCodec::accountTranslationResponse: { const state::AccountState account = state::account_snapshot(); std::uint64_t identity = 0; server::gameplay::group::HostSessionBinding host{}; const bool parsed = middleware::bap::account_translation::request_identity(requestBody, identity); // The primary host asks with identity zero and receives the live region session. Remote // activity rows ask by activity-session id. Only other nonzero identities pair to account. const std::uint64_t liveSession = state::activity::membership::live_region_session(state::activity::kAbsentSessionId); const bool zeroHandle = parsed && identity == 0 && liveSession != state::activity::kAbsentSessionId; const bool logicalHost = parsed && identity != 0 && server::gameplay::group::host_session_for_activity(identity, host) && host.target.sessionId == identity; const bool accountIdentity = parsed && identity != 0 && !logicalHost && pairs_identity(identity); const std::uint64_t soid = zeroHandle ? liveSession : logicalHost ? host.target.sessionId : accountIdentity ? account.primarySoid : 0; std::array line{}; const int count = std::snprintf(line.data(), line.size(), "ev=queuez stage=translate result=%s identity=0x%016llX " "soid=0x%016llX", zeroHandle ? "paired kind=zero_handle" : logicalHost ? "paired kind=logical_host" : accountIdentity ? "paired kind=account" : "unpaired", static_cast(identity), static_cast(soid)); if (count > 0) { core::log::write(core::log::Channel::server, core::log::Level::info, {line.data(), static_cast(count)}); } return middleware::bap::account_translation::encode_response( requestBody, soid, output, written); } case BodyCodec::activityHostManagerResponse: { auto* allocation = emplace_transaction(outcome); if (allocation == nullptr) { return false; } bool hasAllocation = false; const bool encoded = activity_host_manager::encode_response( requestBody, output, written, *allocation, hasAllocation); if (!encoded || !hasAllocation) { clear_transaction(outcome); } return encoded; } case BodyCodec::activityMessageRequest: { written = 0; auto* plan = emplace_transaction(outcome); if (plan == nullptr) { return false; } bool hasTransaction = false; const bool processed = activity_message::process(activity, rosterDecode, requestBody, *plan, hasTransaction); if (!processed || !hasTransaction) { clear_transaction(outcome); } return processed; } case BodyCodec::activityHostResponse: { const state::SignOnState& signOn = state::sign_on(); return middleware::bap::activity_host::encode_response( requestBody, signOn.relayAddress, signOn.relayPort, output, written); } case BodyCodec::clientConfigResponse: return middleware::bap::client_config::encode_minimal_response(output, written); case BodyCodec::familySubscription: { written = 0; outcome.hasSubscription = middleware::bap::family_subscription::parse(requestBody, outcome.subscription); return outcome.hasSubscription; } case BodyCodec::familyUnsubscription: { written = 0; outcome.hasUnsubscription = middleware::bap::family_unsubscription::parse(requestBody, outcome.unsubscription); return outcome.hasUnsubscription; } case BodyCodec::matchmakingResponse: { auto* mutation = emplace_transaction(outcome); if (mutation == nullptr) { return false; } bool hasMutation = false; const bool encoded = matchmaking::encode_response( matchmakingContext, requestBody, output, written, *mutation, hasMutation); if (!hasMutation) { clear_transaction(outcome); } return encoded; } case BodyCodec::steamCertificate: return middleware::bap::certificate::encode_response(requestBody, output, written); case BodyCodec::userMessageResponse: return middleware::bap::user_message::encode_minimal_response(output, written); case BodyCodec::webService: { middleware::web_service::Message message; if (middleware::web_service::parse_request(requestBody, message) && message.opcode == middleware::web_service::messages::opcode505::kOpcode) { auto* changeCharacter = emplace_transaction(outcome); if (!middleware::web_service::messages::opcode505::parse_request(message) || changeCharacter == nullptr || !queuez::stage_change_character(queuezState, *changeCharacter) || !middleware::web_service::messages::opcode505::encode_response( message, changeCharacter->after.family4Version, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws505 stage=change result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } outcome.hasChangeCharacter = true; return true; } // Character deletion. A refusal still answers with the status pair: the roster screen // waits on the echoed transaction id, so a missing reply hangs it for the rest of the run. if (middleware::web_service::parse_request(requestBody, message) && message.opcode == middleware::web_service::messages::opcode502::kOpcode) { const auto refuse = [&]() noexcept { middleware::web_service::StatusResponse status{}; status.code = middleware::web_service::kRefusedStatusCode; // A refused delete stages nothing, so the version wait must be told not to wait. status.value = middleware::web_service::kNoFamily4Publication; return middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written); }; middleware::web_service::messages::opcode502::Request deletion{}; if (!middleware::web_service::messages::opcode502::parse_request(message, deletion) || deletion.characterSoid == 0 || !state::delete_character(deletion.characterSoid)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws502 stage=delete result=fail"); return refuse(); } // The account is already changed and written (delete_character persists through the // investment store internally), but the caller can still drop the frame afterwards -- // a stale entity lease, a refused commit, or a response that does not fit -- and the // outcome flag at the end of this file only lands on the handled path. Without this // the character is gone from disk while the client keeps a roster that names it. sunrise::server::bap::arm_account_resync_everywhere(); // The roster shrank and delete_character has already moved the selection onto a // survivor, so the client needs the Family-4 character object moved with it. Opcode // 502 feeds the Client's Family-4 version wait: the reply has to name the revision the // wait should expect, the staged one when a publication is coming and the no-wait // constant when none is -- left at its default the field reads as revision zero, which // the wait blocks on, and the roster screen never finishes the delete it just asked for. std::int32_t deletionVersion = middleware::web_service::kNoFamily4Publication; const std::uint64_t survivorSoid = state::account::selected_character_soid(state::account_snapshot()); auto* deletionSelect = emplace_transaction(outcome); if (survivorSoid != 0 && deletionSelect != nullptr && queuez::stage_select_character(queuezState, survivorSoid, *deletionSelect)) { outcome.hasSelectCharacter = true; deletionVersion = deletionSelect->after.family4Version; } else { clear_transaction(outcome); sunrise::server::bap::arm_account_resync_everywhere(); } core::log::write( core::log::Channel::server, core::log::Level::info, "ev=ws502 stage=delete result=ok"); middleware::web_service::StatusResponse deletionStatus{}; deletionStatus.value = deletionVersion; return middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, deletionStatus, output, written); } // Character creation. Both outcomes answer in opcode 501's own response shape, which // carries the new SOID: the generic status pair omits that field and the Client fatally // disconnects trying to decode it. if (middleware::web_service::parse_request(requestBody, message) && message.opcode == middleware::web_service::messages::opcode501::kOpcode) { middleware::web_service::StatusResponse creationRefusal{}; creationRefusal.code = middleware::web_service::kRefusedStatusCode; creationRefusal.value = middleware::web_service::kNoFamily4Publication; middleware::web_service::messages::opcode501::Request creation{}; std::uint64_t characterSoid = 0; if (!middleware::web_service::messages::opcode501::parse_request(message, creation) || !state::create_character( creation.characterClass, creation.gender, creation.race, characterSoid)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws501 stage=create result=fail"); return middleware::web_service::messages::opcode501::encode_response( message, creationRefusal, 0, output, written); } // As in the delete branch: the account is already changed and written, but the caller // can still drop the frame, and the outcome flag only lands on the handled path. sunrise::server::bap::arm_account_resync_everywhere(); // create_character marks the new character selected, and the client is waiting for the // Family-4 character object to move onto it. A change-character patch cannot do that // here: it refuses unless family three is in its normal phase, and the creation flow // leaves it outside that phase, so the client would wait forever on a publication that // was never staged. Selecting is both ungated and what the client actually asked for. std::int32_t creationVersion = middleware::web_service::kNoFamily4Publication; auto* creationSelect = emplace_transaction(outcome); if (creationSelect != nullptr && queuez::stage_select_character(queuezState, characterSoid, *creationSelect)) { outcome.hasSelectCharacter = true; creationVersion = creationSelect->after.family4Version; } else { clear_transaction(outcome); sunrise::server::bap::arm_account_resync_everywhere(); } core::log::write(core::log::Channel::server, core::log::Level::info, "ev=ws501 stage=create result=ok"); middleware::web_service::StatusResponse creationStatus{}; creationStatus.value = creationVersion; return middleware::web_service::messages::opcode501::encode_response( message, creationStatus, characterSoid, output, written); } state::investment::store::Transaction investmentTransaction; if (!investmentTransaction.ready()) { return false; } web_service::Outcome webOutcome; if (!sunrise::server::web_service::consume( requestBody, output, written, webOutcome, presentation)) { return false; } if (webOutcome.hasTitleEquip) { // Promise the revision that refreshes the Seals widget's action binding. if (queuezState.family4Version != (std::numeric_limits::max)()) { middleware::web_service::StatusResponse status{}; status.value = queuezState.family4Version + 1; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=title_equip stage=response result=fail"); sunrise::server::bap::arm_account_resync_everywhere(); return false; } } sunrise::server::bap::arm_account_resync_everywhere(); } outcome.hasSubscription = webOutcome.hasSubscription; outcome.hasRecordClaim = webOutcome.hasRecordClaim; outcome.hasArtifactReset = webOutcome.hasArtifactReset; outcome.artifactReset = webOutcome.artifactReset; outcome.subscription = webOutcome.subscription; const auto* equipmentSwap = web_service::mutation_if(webOutcome); const auto* subclassSelection = web_service::mutation_if(webOutcome); const auto* socketPlug = web_service::mutation_if(webOutcome); const auto* itemState = web_service::mutation_if(webOutcome); const auto* artifactPurchase = web_service::mutation_if(webOutcome); const auto* itemAcquisition = web_service::mutation_if(webOutcome); const auto* profileItemAcquisition = web_service::mutation_if(webOutcome); const auto* itemDismantle = web_service::mutation_if(webOutcome); const auto* recordRewardGrant = web_service::mutation_if(webOutcome); auto* seasonPassReward = web_service::mutation_if(webOutcome); if (webOutcome.hasArtifactReset) { if (queuezState.family4Version == (std::numeric_limits::max)()) { return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = queuezState.family4Version + 1; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPairWithBool, status, output, written)) { return false; } } if (artifactPurchase != nullptr) { auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_equipment_swap( queuezState, artifactPurchase->characterSoid, transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws901 stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; status.trailingBool = true; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPairWithBool, status, output, written)) { clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } const auto* settingsUpdate = web_service::mutation_if(webOutcome); if (settingsUpdate != nullptr) { // WS-701 promises no immediate QueueZ after-image, so State alone is delayed. if (emplace_transaction(outcome, *settingsUpdate) == nullptr) { return refuse_web_action(message, output, written); } } if (equipmentSwap != nullptr) { // Promise the Family-4 revision carrying this optimistic equip. auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_equipment_swap( queuezState, equipmentSwap->characterSoid, transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws403 stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws403 stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (subclassSelection != nullptr) { // Promise the Family-4 revision carrying the subclass socket change. auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_subclass_selection(queuezState, subclassSelection->accountSoid, subclassSelection->characterSoid, subclassSelection->subclassInstanceSoid, transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=subclass_select stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=subclass_select stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (socketPlug != nullptr) { // Promise the Family-4 revision carrying the changed item instance. auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_socket_plug(queuezState, socketPlug->accountSoid, socketPlug->characterSoid, socketPlug->targetInstanceSoid, socketPlug->profileChanged, transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=socket_plug stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=socket_plug stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (itemState != nullptr) { // Promise the Family-4 revision carrying the changed row flags. auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_equipment_swap( queuezState, itemState->characterSoid, transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=item_state stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=item_state stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (itemAcquisition != nullptr) { // Promise the revision adding both the inventory row and resident instance. auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_item_acquisition(queuezState, itemAcquisition->accountSoid, itemAcquisition->characterSoid, itemAcquisition->acquiredInstanceSoid, itemAcquisition->updates_account(), transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=acquire stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=acquire stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (profileItemAcquisition != nullptr) { // Actionable profile stacks may add a resident in the same revision. auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_profile_item_acquisition( queuezState, profileItemAcquisition->accountSoid, profileItemAcquisition->acquiredInstanceSoid, profileItemAcquisition->actionSource, profileItemAcquisition->appended, transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=profile_acquire stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=profile_acquire stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (itemDismantle != nullptr) { // Promise the revision carrying the character update and resident release. auto* transaction = emplace_transaction(outcome); if (transaction == nullptr || !queuez::stage_item_dismantle(queuezState, itemDismantle->accountSoid, itemDismantle->characterSoid, itemDismantle->dismantledInstanceSoid, itemDismantle->profileChanged, transaction->update)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=dismantle stage=queuez_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=dismantle stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (recordRewardGrant != nullptr) { auto* transaction = emplace_transaction(outcome); std::array residents{}; std::size_t residentCount = 0; for (std::size_t index = 0; index < recordRewardGrant->rewardCount; ++index) { const auto& reward = recordRewardGrant->rewards[index]; if (reward.kind == state::RecordRewardKind::characterInstance || reward.appendedProfileResident) { residents[residentCount++] = reward.instanceSoid; } } const bool staged = transaction != nullptr && queuez::stage_record_reward_grant(queuezState, recordRewardGrant->accountSoid, recordRewardGrant->characterSoid, std::span(residents).first(residentCount), transaction->update); if (!staged) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws1801 stage=reward_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = transaction->update.after.family4Version; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws1801 stage=reward_response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (seasonPassReward != nullptr) { auto* transaction = emplace_transaction(outcome); if (transaction == nullptr) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws2400 stage=reward_preflight result=fail"); return refuse_web_action(message, output, written); } bool staged = false; std::int32_t stagedVersion = 0; if (const auto* itemGrant = std::get_if(&seasonPassReward->grant)) { auto& update = transaction->update.emplace(); staged = queuez::stage_item_acquisition(queuezState, itemGrant->accountSoid, itemGrant->characterSoid, itemGrant->acquiredInstanceSoid, true, update); stagedVersion = update.after.family4Version; } else if (const auto* profileGrant = std::get_if( &seasonPassReward->grant)) { auto& update = transaction->update.emplace(); staged = queuez::stage_profile_item_acquisition(queuezState, profileGrant->accountSoid, profileGrant->acquiredInstanceSoid, profileGrant->actionSource, profileGrant->appended, update); stagedVersion = update.after.family4Version; } else if (const auto* bundle = std::get_if(&seasonPassReward->grant)) { staged = queuez::stage_direct_item_bundle(queuezState, bundle->accountSoid, bundle->characterSoid, bundle->firstInstanceSoid, bundle->itemCount, stagedVersion); } else if (const auto* resources = std::get_if(&seasonPassReward->grant)) { auto& update = transaction->update.emplace(); staged = queuez::stage_record_reward_grant( queuezState, resources->accountSoid, resources->characterSoid, {}, update); stagedVersion = update.after.family4Version; } if (!staged) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws2400 stage=reward_preflight result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } middleware::web_service::StatusResponse status{}; status.value = stagedVersion; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws2400 stage=response result=fail"); clear_transaction(outcome); return refuse_web_action(message, output, written); } transaction->pending = web_service::take_mutation(webOutcome); } if (webOutcome.hasRecordClaim && !has_transaction(outcome) && queuezState.family4Version != (std::numeric_limits::max)()) { // A plain claim is made authoritative by the following account resync. middleware::web_service::StatusResponse status{}; status.value = queuezState.family4Version + 1; if (!middleware::web_service::encode_response( message, middleware::web_service::ResponseShape::statusPair, status, output, written)) { core::log::write(core::log::Channel::server, core::log::Level::warn, "ev=ws1801 stage=response result=fail"); sunrise::server::bap::arm_account_resync_everywhere(); return false; } } // Every pick is answered, the current character included; character select waits on it. if (webOutcome.hasSelectedCharacter) { auto* selectCharacter = emplace_transaction(outcome); if (selectCharacter != nullptr && queuez::stage_select_character( queuezState, webOutcome.selectedCharacterSoid, *selectCharacter)) { outcome.hasSelectCharacter = true; } else { clear_transaction(outcome); sunrise::server::bap::arm_account_resync_everywhere(); } } return investmentTransaction.commit(); } } written = 0; return false; } } // namespace sunrise::server::bap::encrypted::body