From f8d50b3ab7f7c5f9540bd917092a6ada0c6d3d57 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Mon, 8 Jun 2026 03:56:50 -0400 Subject: [PATCH 1/7] Add log-only account sync save hooks --- src/Account.cc | 3 +++ src/AccountSync.hh | 38 ++++++++++++++++++++++++++++++++++++++ src/ChatCommands.cc | 13 +++++++++++++ src/Client.cc | 29 +++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 src/AccountSync.hh diff --git a/src/Account.cc b/src/Account.cc index 7df886a8..a3fc939c 100644 --- a/src/Account.cc +++ b/src/Account.cc @@ -9,6 +9,7 @@ #include #include "Account.hh" +#include "AccountSync.hh" std::shared_ptr DCNTELicense::from_json(const phosg::JSON& json) { auto ret = std::make_shared(); @@ -400,6 +401,8 @@ void Account::save() const { phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS); std::string filename = std::format("system/licenses/{:010}.json", this->account_id); phosg::save_file(filename, json_data); + + AccountSync::notify_account_saved(this->account_id, filename); } } diff --git a/src/AccountSync.hh b/src/AccountSync.hh new file mode 100644 index 00000000..3662caf5 --- /dev/null +++ b/src/AccountSync.hh @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +namespace AccountSync { + +inline void notify_account_saved(uint32_t account_id, const std::string& filename) { + std::fprintf(stderr, + "[AccountSync] event=account_saved account_id=%010u filename=%s\n", + static_cast(account_id), + filename.c_str()); +} + +inline void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename) { + std::fprintf(stderr, + "[AccountSync] event=backup_saved account_id=%010u slot=%zu filename=%s\n", + static_cast(account_id), + slot, + filename.c_str()); +} + +inline void notify_player_state_saved( + const char* reason, + uint32_t account_id, + const std::string& bb_username, + const std::string& filename) { + std::fprintf(stderr, + "[AccountSync] event=player_state_saved reason=%s account_id=%010u bb_username=%s filename=%s\n", + reason, + static_cast(account_id), + bb_username.c_str(), + filename.c_str()); +} + +} // namespace AccountSync diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 2a911c20..7ca6f917 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -22,6 +22,7 @@ #include "Server.hh" #include "StaticGameData.hh" #include "Text.hh" +#include "AccountSync.hh" /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Tools @@ -491,12 +492,22 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool ? Client::character_filename(dest_bb_license->username, dest_character_index) : Client::backup_character_filename(dest_account->account_id, dest_character_index, is_ep3(a.c->version())); + auto log_account_sync_backup_saved = [&]() -> void { + if (!is_bb_conversion) { + AccountSync::notify_backup_saved( + dest_account->account_id, + dest_character_index, + filename); + } + }; + if (ch.is_full_info) { // Client sent 30; ch contains the verbatim save file from the client if (ch.ep3_character) { try { Client::save_ep3_character_file(filename, *ch.ep3_character); send_text_message(a.c, "$C7Character data saved\n(full save file)"); + log_account_sync_backup_saved(); } catch (const std::exception& e) { send_text_message_fmt(a.c, "$C6Character data could\nnot be saved:\n{}", e.what()); } @@ -505,6 +516,7 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool try { Client::save_character_file(filename, a.c->system_file(), ch.character); send_text_message(a.c, "$C7Character data saved\n(full save file)"); + log_account_sync_backup_saved(); } catch (const std::exception& e) { send_text_message_fmt(a.c, "$C6Character data could\nnot be saved:\n{}", e.what()); } @@ -546,6 +558,7 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool try { Client::save_character_file(filename, a.c->system_file(), bb_player); send_text_message(a.c, "$C7Character data saved\n(basic only)"); + log_account_sync_backup_saved(); } catch (const std::exception& e) { send_text_message_fmt(a.c, "$C6Character data could\nnot be saved:\n{}", e.what()); } diff --git a/src/Client.cc b/src/Client.cc index a27c682c..cca299f4 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -15,6 +15,7 @@ #include "SendCommands.hh" #include "Server.hh" #include "Version.hh" +#include "AccountSync.hh" const uint64_t CLIENT_CONFIG_MAGIC = 0x8399AC32; @@ -503,6 +504,13 @@ void Client::save_system_file() const { std::string filename = this->system_filename(); phosg::save_object_file(filename, *this->system_data); this->log.info_f("Saved system file {}", filename); + if (this->login && this->login->account && this->login->bb_license) { + AccountSync::notify_player_state_saved( + "system_saved", + this->login->account->account_id, + this->login->bb_license->username, + filename); + } } // Guild Card file @@ -542,6 +550,13 @@ void Client::save_guild_card_file() const { std::string filename = this->guild_card_filename(); phosg::save_object_file(filename, *this->guild_card_data); this->log.info_f("Saved Guild Card file {}", filename); + if (this->login && this->login->account && this->login->bb_license) { + AccountSync::notify_player_state_saved( + "guild_card_saved", + this->login->account->account_id, + this->login->bb_license->username, + filename); + } } // Character file @@ -634,6 +649,13 @@ void Client::save_character_file() { auto filename = this->character_filename(); this->save_character_file(filename, this->system_data, this->character_data); this->log.info_f("Saved character file {}", filename); + if (this->login && this->login->account && this->login->bb_license) { + AccountSync::notify_player_state_saved( + "character_saved", + this->login->account->account_id, + this->login->bb_license->username, + filename); + } } void Client::create_character_file( @@ -834,6 +856,13 @@ void Client::save_bank_file() const { auto filename = this->bank_filename(); this->save_bank_file(filename, *this->bank_data); this->log.info_f("Saved bank file {}", filename); + if (this->login && this->login->account && this->login->bb_license) { + AccountSync::notify_player_state_saved( + "bank_saved", + this->login->account->account_id, + this->login->bb_license->username, + filename); + } } void Client::change_bank(ssize_t index) { From fe97a0dda4f3a96a285ba30b39cc05c7d0e6d175 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Mon, 8 Jun 2026 03:59:27 -0400 Subject: [PATCH 2/7] Add log-only BB account sync login hook --- src/AccountSync.hh | 13 +++++++++++++ src/ReceiveCommands.cc | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/src/AccountSync.hh b/src/AccountSync.hh index 3662caf5..5b797f34 100644 --- a/src/AccountSync.hh +++ b/src/AccountSync.hh @@ -35,4 +35,17 @@ inline void notify_player_state_saved( filename.c_str()); } +inline void notify_bb_login_start( + uint32_t account_id, + const std::string& bb_username, + int64_t character_slot, + uint8_t connection_phase) { + std::fprintf(stderr, + "[AccountSync] event=bb_login_start account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", + static_cast(account_id), + bb_username.c_str(), + static_cast(character_slot), + static_cast(connection_phase)); +} + } // namespace AccountSync diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 79aeeb8e..acf99d35 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -27,6 +27,7 @@ #include "StaticGameData.hh" #include "Text.hh" #include "BrutalPeeps.hh" +#include "AccountSync.hh" const char* BATTLE_TABLE_DISCONNECT_HOOK_NAME = "battle_table_state"; const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME = "quest_barrier"; @@ -1672,6 +1673,14 @@ static asio::awaitable on_93_BB(std::shared_ptr c, Channel::Messag co_return; } + if (c->login->account && c->login->bb_license) { + AccountSync::notify_bb_login_start( + c->login->account->account_id, + c->login->bb_license->username, + c->bb_character_index, + c->bb_connection_phase); + } + std::string version_string = c->bb_client_config.as_string(); phosg::strip_trailing_zeroes(version_string); // If the version std::string starts with "Ver.", assume it's Sega and apply the normal version encoding logic. Otherwise, From 94250d21eb56425be64555bdea0e5948b59bd40d Mon Sep 17 00:00:00 2001 From: James Osborne Date: Mon, 8 Jun 2026 04:02:11 -0400 Subject: [PATCH 3/7] Add log-only BB account sync logout hook --- src/AccountSync.hh | 11 +++++++++++ src/Client.cc | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/AccountSync.hh b/src/AccountSync.hh index 5b797f34..0ab3b578 100644 --- a/src/AccountSync.hh +++ b/src/AccountSync.hh @@ -48,4 +48,15 @@ inline void notify_bb_login_start( static_cast(connection_phase)); } +inline void notify_bb_login_end( + uint32_t account_id, + const std::string& bb_username, + int64_t character_slot) { + std::fprintf(stderr, + "[AccountSync] event=bb_login_end account_id=%010u bb_username=%s character_slot=%lld\n", + static_cast(account_id), + bb_username.c_str(), + static_cast(character_slot)); +} + } // namespace AccountSync diff --git a/src/Client.cc b/src/Client.cc index cca299f4..fad4c7c7 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -233,6 +233,13 @@ Client::~Client() { if ((this->version() == Version::BB_V4) && (this->character_data.get())) { this->save_all(); + + if (this->login && this->login->account && this->login->bb_license) { + AccountSync::notify_bb_login_end( + this->login->account->account_id, + this->login->bb_license->username, + this->bb_character_index); + } } this->log.info_f("Deleted"); } From 70dd22ee8c611d4f6a8df182e9172b0431e530cc Mon Sep 17 00:00:00 2001 From: James Osborne Date: Mon, 8 Jun 2026 04:43:21 -0400 Subject: [PATCH 4/7] Make account sync hooks config-aware --- src/AccountSync.hh | 86 +++++++++++++++++++++++++++++++++++++++++++--- src/ServerState.cc | 2 ++ 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/AccountSync.hh b/src/AccountSync.hh index 0ab3b578..4eeb12a5 100644 --- a/src/AccountSync.hh +++ b/src/AccountSync.hh @@ -5,18 +5,79 @@ #include #include +#include + namespace AccountSync { +struct Config { + bool enabled = false; + std::string region; + std::string coordinator_url; + std::string shared_secret; + uint64_t request_timeout_usecs = 3000000; + bool fail_open = false; + bool notify_account_saves = true; + bool notify_player_saves = true; + bool notify_backup_saves = true; + bool enable_login_locks = false; +}; + +inline Config& mutable_config() { + static Config cfg; + return cfg; +} + +inline const Config& config() { + return mutable_config(); +} + +inline void configure(const Config& cfg) { + mutable_config() = cfg; + + if (cfg.enabled) { + std::fprintf(stderr, + "[AccountSync] config enabled region=%s coordinator_url=%s login_locks=%s\n", + cfg.region.c_str(), + cfg.coordinator_url.c_str(), + cfg.enable_login_locks ? "true" : "false"); + } +} + +inline void configure_from_json(const phosg::JSON& json) { + Config cfg; + cfg.enabled = json.get_bool("Enabled", false); + cfg.region = json.get_string("Region", ""); + cfg.coordinator_url = json.get_string("CoordinatorURL", ""); + cfg.shared_secret = json.get_string("SharedSecret", ""); + cfg.request_timeout_usecs = json.get_int("RequestTimeoutUsecs", 3000000); + cfg.fail_open = json.get_bool("FailOpen", false); + cfg.notify_account_saves = json.get_bool("NotifyAccountSaves", true); + cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true); + cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true); + cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false); + configure(cfg); +} + inline void notify_account_saved(uint32_t account_id, const std::string& filename) { + const auto& cfg = config(); + if (!cfg.enabled || !cfg.notify_account_saves) { + return; + } std::fprintf(stderr, - "[AccountSync] event=account_saved account_id=%010u filename=%s\n", + "[AccountSync] event=account_saved region=%s account_id=%010u filename=%s\n", + cfg.region.c_str(), static_cast(account_id), filename.c_str()); } inline void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename) { + const auto& cfg = config(); + if (!cfg.enabled || !cfg.notify_backup_saves) { + return; + } std::fprintf(stderr, - "[AccountSync] event=backup_saved account_id=%010u slot=%zu filename=%s\n", + "[AccountSync] event=backup_saved region=%s account_id=%010u slot=%zu filename=%s\n", + cfg.region.c_str(), static_cast(account_id), slot, filename.c_str()); @@ -27,8 +88,13 @@ inline void notify_player_state_saved( uint32_t account_id, const std::string& bb_username, const std::string& filename) { + const auto& cfg = config(); + if (!cfg.enabled || !cfg.notify_player_saves) { + return; + } std::fprintf(stderr, - "[AccountSync] event=player_state_saved reason=%s account_id=%010u bb_username=%s filename=%s\n", + "[AccountSync] event=player_state_saved region=%s reason=%s account_id=%010u bb_username=%s filename=%s\n", + cfg.region.c_str(), reason, static_cast(account_id), bb_username.c_str(), @@ -40,8 +106,13 @@ inline void notify_bb_login_start( const std::string& bb_username, int64_t character_slot, uint8_t connection_phase) { + const auto& cfg = config(); + if (!cfg.enabled || !cfg.enable_login_locks) { + return; + } std::fprintf(stderr, - "[AccountSync] event=bb_login_start account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", + "[AccountSync] event=bb_login_start region=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", + cfg.region.c_str(), static_cast(account_id), bb_username.c_str(), static_cast(character_slot), @@ -52,8 +123,13 @@ inline void notify_bb_login_end( uint32_t account_id, const std::string& bb_username, int64_t character_slot) { + const auto& cfg = config(); + if (!cfg.enabled || !cfg.enable_login_locks) { + return; + } std::fprintf(stderr, - "[AccountSync] event=bb_login_end account_id=%010u bb_username=%s character_slot=%lld\n", + "[AccountSync] event=bb_login_end region=%s account_id=%010u bb_username=%s character_slot=%lld\n", + cfg.region.c_str(), static_cast(account_id), bb_username.c_str(), static_cast(character_slot)); diff --git a/src/ServerState.cc b/src/ServerState.cc index c56f3477..0384ede7 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -18,6 +18,7 @@ #include "SendCommands.hh" #include "Text.hh" #include "TextIndex.hh" +#include "AccountSync.hh" #ifdef PHOSG_WINDOWS static constexpr bool IS_WINDOWS = true; @@ -879,6 +880,7 @@ void ServerState::load_config_early() { this->client_ping_interval_usecs = this->config_json->get_int("ClientPingInterval", 30000000); this->client_idle_timeout_usecs = this->config_json->get_int("ClientIdleTimeout", 60000000); this->patch_client_idle_timeout_usecs = this->config_json->get_int("PatchClientIdleTimeout", 300000000); + AccountSync::configure_from_json(this->config_json->get("AccountSync", phosg::JSON::dict())); this->psopeeps_dcv2_exp_multiplier = this->config_json->get_int("PsoPeepsDCV2EXPMultiplier", 5); if ((this->psopeeps_dcv2_exp_multiplier != 5) && (this->psopeeps_dcv2_exp_multiplier != 10)) { throw std::runtime_error("PsoPeepsDCV2EXPMultiplier must be 5 or 10"); From 8f80005cb1e09afb9ee0bd4eb8ad5a126ae11672 Mon Sep 17 00:00:00 2001 From: James Osborne Date: Mon, 8 Jun 2026 05:55:11 -0400 Subject: [PATCH 5/7] Move account sync implementation out of header --- CMakeLists.txt | 1 + src/AccountSync.cc | 123 +++++++++++++++++++++++++++++++++++++++++++++ src/AccountSync.hh | 111 ++++------------------------------------ 3 files changed, 134 insertions(+), 101 deletions(-) create mode 100644 src/AccountSync.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 874aaaa8..8c29c49d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ add_custom_target( set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc src/Account.cc + src/AccountSync.cc src/AddressTranslator.cc src/AFSArchive.cc src/AsyncHTTPServer.cc diff --git a/src/AccountSync.cc b/src/AccountSync.cc new file mode 100644 index 00000000..0123d529 --- /dev/null +++ b/src/AccountSync.cc @@ -0,0 +1,123 @@ +#include "AccountSync.hh" + +#include +#include + +namespace AccountSync { + +static std::mutex config_mutex; +static Config current_config; + +static Config get_config() { + std::lock_guard g(config_mutex); + return current_config; +} + +void configure(const Config& cfg) { + { + std::lock_guard g(config_mutex); + current_config = cfg; + } + + if (cfg.enabled) { + std::fprintf(stderr, + "[AccountSync] config enabled region=%s coordinator_url=%s login_locks=%s\n", + cfg.region.c_str(), + cfg.coordinator_url.c_str(), + cfg.enable_login_locks ? "true" : "false"); + } +} + +void configure_from_json(const phosg::JSON& json) { + Config cfg; + cfg.enabled = json.get_bool("Enabled", false); + cfg.region = json.get_string("Region", ""); + cfg.coordinator_url = json.get_string("CoordinatorURL", ""); + cfg.shared_secret = json.get_string("SharedSecret", ""); + cfg.request_timeout_usecs = json.get_int("RequestTimeoutUsecs", 3000000); + cfg.fail_open = json.get_bool("FailOpen", false); + cfg.notify_account_saves = json.get_bool("NotifyAccountSaves", true); + cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true); + cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true); + cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false); + configure(cfg); +} + +void notify_account_saved(uint32_t account_id, const std::string& filename) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.notify_account_saves) { + return; + } + std::fprintf(stderr, + "[AccountSync] event=account_saved region=%s account_id=%010u filename=%s\n", + cfg.region.c_str(), + static_cast(account_id), + filename.c_str()); +} + +void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.notify_backup_saves) { + return; + } + std::fprintf(stderr, + "[AccountSync] event=backup_saved region=%s account_id=%010u slot=%zu filename=%s\n", + cfg.region.c_str(), + static_cast(account_id), + slot, + filename.c_str()); +} + +void notify_player_state_saved( + const char* reason, + uint32_t account_id, + const std::string& bb_username, + const std::string& filename) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.notify_player_saves) { + return; + } + std::fprintf(stderr, + "[AccountSync] event=player_state_saved region=%s reason=%s account_id=%010u bb_username=%s filename=%s\n", + cfg.region.c_str(), + reason, + static_cast(account_id), + bb_username.c_str(), + filename.c_str()); +} + +void notify_bb_login_start( + uint32_t account_id, + const std::string& bb_username, + int64_t character_slot, + uint8_t connection_phase) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.enable_login_locks) { + return; + } + std::fprintf(stderr, + "[AccountSync] event=bb_login_start region=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", + cfg.region.c_str(), + static_cast(account_id), + bb_username.c_str(), + static_cast(character_slot), + static_cast(connection_phase)); +} + +void notify_bb_login_end( + uint32_t account_id, + const std::string& bb_username, + int64_t character_slot) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.enable_login_locks) { + return; + } + std::fprintf(stderr, + "[AccountSync] event=bb_login_end region=%s account_id=%010u bb_username=%s character_slot=%lld\n", + cfg.region.c_str(), + static_cast(account_id), + bb_username.c_str(), + static_cast(character_slot)); +} + +} // namespace AccountSync diff --git a/src/AccountSync.hh b/src/AccountSync.hh index 4eeb12a5..dd5ab6c4 100644 --- a/src/AccountSync.hh +++ b/src/AccountSync.hh @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -22,117 +21,27 @@ struct Config { bool enable_login_locks = false; }; -inline Config& mutable_config() { - static Config cfg; - return cfg; -} +void configure(const Config& cfg); +void configure_from_json(const phosg::JSON& json); -inline const Config& config() { - return mutable_config(); -} +void notify_account_saved(uint32_t account_id, const std::string& filename); +void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename); -inline void configure(const Config& cfg) { - mutable_config() = cfg; - - if (cfg.enabled) { - std::fprintf(stderr, - "[AccountSync] config enabled region=%s coordinator_url=%s login_locks=%s\n", - cfg.region.c_str(), - cfg.coordinator_url.c_str(), - cfg.enable_login_locks ? "true" : "false"); - } -} - -inline void configure_from_json(const phosg::JSON& json) { - Config cfg; - cfg.enabled = json.get_bool("Enabled", false); - cfg.region = json.get_string("Region", ""); - cfg.coordinator_url = json.get_string("CoordinatorURL", ""); - cfg.shared_secret = json.get_string("SharedSecret", ""); - cfg.request_timeout_usecs = json.get_int("RequestTimeoutUsecs", 3000000); - cfg.fail_open = json.get_bool("FailOpen", false); - cfg.notify_account_saves = json.get_bool("NotifyAccountSaves", true); - cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true); - cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true); - cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false); - configure(cfg); -} - -inline void notify_account_saved(uint32_t account_id, const std::string& filename) { - const auto& cfg = config(); - if (!cfg.enabled || !cfg.notify_account_saves) { - return; - } - std::fprintf(stderr, - "[AccountSync] event=account_saved region=%s account_id=%010u filename=%s\n", - cfg.region.c_str(), - static_cast(account_id), - filename.c_str()); -} - -inline void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename) { - const auto& cfg = config(); - if (!cfg.enabled || !cfg.notify_backup_saves) { - return; - } - std::fprintf(stderr, - "[AccountSync] event=backup_saved region=%s account_id=%010u slot=%zu filename=%s\n", - cfg.region.c_str(), - static_cast(account_id), - slot, - filename.c_str()); -} - -inline void notify_player_state_saved( +void notify_player_state_saved( const char* reason, uint32_t account_id, const std::string& bb_username, - const std::string& filename) { - const auto& cfg = config(); - if (!cfg.enabled || !cfg.notify_player_saves) { - return; - } - std::fprintf(stderr, - "[AccountSync] event=player_state_saved region=%s reason=%s account_id=%010u bb_username=%s filename=%s\n", - cfg.region.c_str(), - reason, - static_cast(account_id), - bb_username.c_str(), - filename.c_str()); -} + const std::string& filename); -inline void notify_bb_login_start( +void notify_bb_login_start( uint32_t account_id, const std::string& bb_username, int64_t character_slot, - uint8_t connection_phase) { - const auto& cfg = config(); - if (!cfg.enabled || !cfg.enable_login_locks) { - return; - } - std::fprintf(stderr, - "[AccountSync] event=bb_login_start region=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", - cfg.region.c_str(), - static_cast(account_id), - bb_username.c_str(), - static_cast(character_slot), - static_cast(connection_phase)); -} + uint8_t connection_phase); -inline void notify_bb_login_end( +void notify_bb_login_end( uint32_t account_id, const std::string& bb_username, - int64_t character_slot) { - const auto& cfg = config(); - if (!cfg.enabled || !cfg.enable_login_locks) { - return; - } - std::fprintf(stderr, - "[AccountSync] event=bb_login_end region=%s account_id=%010u bb_username=%s character_slot=%lld\n", - cfg.region.c_str(), - static_cast(account_id), - bb_username.c_str(), - static_cast(character_slot)); -} + int64_t character_slot); } // namespace AccountSync From cfbe1fda279f0c3f305bb4775493be54dd7d2b7e Mon Sep 17 00:00:00 2001 From: James Osborne Date: Mon, 8 Jun 2026 05:59:55 -0400 Subject: [PATCH 6/7] Spool account sync events to disk --- src/AccountSync.cc | 131 +++++++++++++++++++++++++++++++++++++++++++-- src/AccountSync.hh | 4 +- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/src/AccountSync.cc b/src/AccountSync.cc index 0123d529..b98f7a92 100644 --- a/src/AccountSync.cc +++ b/src/AccountSync.cc @@ -1,18 +1,102 @@ #include "AccountSync.hh" +#include #include +#include +#include #include +#include +#include namespace AccountSync { static std::mutex config_mutex; +static std::mutex spool_mutex; static Config current_config; +static uint64_t now_usecs() { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); +} + static Config get_config() { std::lock_guard g(config_mutex); return current_config; } +static std::string json_escape(const std::string& s) { + std::string ret; + ret.reserve(s.size() + 8); + for (unsigned char ch : s) { + switch (ch) { + case '\\': + ret += "\\\\"; + break; + case '"': + ret += "\\\""; + break; + case '\b': + ret += "\\b"; + break; + case '\f': + ret += "\\f"; + break; + case '\n': + ret += "\\n"; + break; + case '\r': + ret += "\\r"; + break; + case '\t': + ret += "\\t"; + break; + default: + if (ch < 0x20) { + char buf[8]; + std::snprintf(buf, sizeof(buf), "\\u%04X", ch); + ret += buf; + } else { + ret += static_cast(ch); + } + } + } + return ret; +} + +static void append_spool_line(const Config& cfg, const std::string& line) { + if (cfg.spool_directory.empty()) { + return; + } + + try { + std::lock_guard g(spool_mutex); + std::filesystem::create_directories(cfg.spool_directory); + std::filesystem::path path = std::filesystem::path(cfg.spool_directory) / "events.jsonl"; + + std::ofstream f(path, std::ios::app); + if (!f) { + throw std::runtime_error("failed to open spool file"); + } + f << line << '\n'; + + } catch (const std::exception& e) { + std::fprintf(stderr, + "[AccountSync] warning failed_to_write_spool directory=%s error=%s\n", + cfg.spool_directory.c_str(), + e.what()); + } +} + +static std::string base_event_json(const Config& cfg, const char* event, uint32_t account_id) { + return std::format( + "{{\"timestamp_usecs\":{},\"source\":\"newserv\",\"event\":\"{}\",\"region\":\"{}\",\"account_id\":{},\"account_id_str\":\"{:010}\"", + now_usecs(), + json_escape(event), + json_escape(cfg.region), + static_cast(account_id), + static_cast(account_id)); +} + void configure(const Config& cfg) { { std::lock_guard g(config_mutex); @@ -21,9 +105,11 @@ void configure(const Config& cfg) { if (cfg.enabled) { std::fprintf(stderr, - "[AccountSync] config enabled region=%s coordinator_url=%s login_locks=%s\n", + "[AccountSync] config enabled region=%s coordinator_url=%s notify_bb_sessions=%s spool_directory=%s login_locks=%s\n", cfg.region.c_str(), cfg.coordinator_url.c_str(), + cfg.notify_bb_sessions ? "true" : "false", + cfg.spool_directory.c_str(), cfg.enable_login_locks ? "true" : "false"); } } @@ -40,6 +126,12 @@ void configure_from_json(const phosg::JSON& json) { cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true); cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true); cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false); + + // Backward-compatible during transition: old test configs with EnableLoginLocks=true + // still emit BB session events, but this does not enforce locks. + cfg.notify_bb_sessions = json.get_bool("NotifyBBSessions", cfg.enable_login_locks); + + cfg.spool_directory = json.get_string("SpoolDirectory", "system/account-sync-spool"); configure(cfg); } @@ -48,11 +140,16 @@ void notify_account_saved(uint32_t account_id, const std::string& filename) { if (!cfg.enabled || !cfg.notify_account_saves) { return; } + std::fprintf(stderr, "[AccountSync] event=account_saved region=%s account_id=%010u filename=%s\n", cfg.region.c_str(), static_cast(account_id), filename.c_str()); + + auto line = base_event_json(cfg, "account_saved", account_id) + + std::format(",\"filename\":\"{}\"}}", json_escape(filename)); + append_spool_line(cfg, line); } void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename) { @@ -60,12 +157,17 @@ void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& fi if (!cfg.enabled || !cfg.notify_backup_saves) { return; } + std::fprintf(stderr, "[AccountSync] event=backup_saved region=%s account_id=%010u slot=%zu filename=%s\n", cfg.region.c_str(), static_cast(account_id), slot, filename.c_str()); + + auto line = base_event_json(cfg, "backup_saved", account_id) + + std::format(",\"slot\":{},\"filename\":\"{}\"}}", slot, json_escape(filename)); + append_spool_line(cfg, line); } void notify_player_state_saved( @@ -77,6 +179,7 @@ void notify_player_state_saved( if (!cfg.enabled || !cfg.notify_player_saves) { return; } + std::fprintf(stderr, "[AccountSync] event=player_state_saved region=%s reason=%s account_id=%010u bb_username=%s filename=%s\n", cfg.region.c_str(), @@ -84,6 +187,13 @@ void notify_player_state_saved( static_cast(account_id), bb_username.c_str(), filename.c_str()); + + auto line = base_event_json(cfg, "player_state_saved", account_id) + + std::format(",\"reason\":\"{}\",\"bb_username\":\"{}\",\"filename\":\"{}\"}}", + json_escape(reason), + json_escape(bb_username), + json_escape(filename)); + append_spool_line(cfg, line); } void notify_bb_login_start( @@ -92,9 +202,10 @@ void notify_bb_login_start( int64_t character_slot, uint8_t connection_phase) { auto cfg = get_config(); - if (!cfg.enabled || !cfg.enable_login_locks) { + if (!cfg.enabled || !cfg.notify_bb_sessions) { return; } + std::fprintf(stderr, "[AccountSync] event=bb_login_start region=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", cfg.region.c_str(), @@ -102,6 +213,13 @@ void notify_bb_login_start( bb_username.c_str(), static_cast(character_slot), static_cast(connection_phase)); + + auto line = base_event_json(cfg, "bb_login_start", account_id) + + std::format(",\"bb_username\":\"{}\",\"character_slot\":{},\"connection_phase\":{}}}", + json_escape(bb_username), + static_cast(character_slot), + static_cast(connection_phase)); + append_spool_line(cfg, line); } void notify_bb_login_end( @@ -109,15 +227,22 @@ void notify_bb_login_end( const std::string& bb_username, int64_t character_slot) { auto cfg = get_config(); - if (!cfg.enabled || !cfg.enable_login_locks) { + if (!cfg.enabled || !cfg.notify_bb_sessions) { return; } + std::fprintf(stderr, "[AccountSync] event=bb_login_end region=%s account_id=%010u bb_username=%s character_slot=%lld\n", cfg.region.c_str(), static_cast(account_id), bb_username.c_str(), static_cast(character_slot)); + + auto line = base_event_json(cfg, "bb_login_end", account_id) + + std::format(",\"bb_username\":\"{}\",\"character_slot\":{}}}", + json_escape(bb_username), + static_cast(character_slot)); + append_spool_line(cfg, line); } } // namespace AccountSync diff --git a/src/AccountSync.hh b/src/AccountSync.hh index dd5ab6c4..3e01cffd 100644 --- a/src/AccountSync.hh +++ b/src/AccountSync.hh @@ -18,7 +18,9 @@ struct Config { bool notify_account_saves = true; bool notify_player_saves = true; bool notify_backup_saves = true; - bool enable_login_locks = false; + bool notify_bb_sessions = false; + bool enable_login_locks = false; // Reserved for future blocking lock behavior + std::string spool_directory = "system/account-sync-spool"; }; void configure(const Config& cfg); From 127288c3494fa48a6e19c68f65663fce0cf5e4ba Mon Sep 17 00:00:00 2001 From: James Osborne Date: Mon, 8 Jun 2026 06:06:20 -0400 Subject: [PATCH 7/7] Add explicit account sync source identity --- src/AccountSync.cc | 88 ++++++++++++++++++++++++++++++++++++---------- src/AccountSync.hh | 9 ++++- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/AccountSync.cc b/src/AccountSync.cc index b98f7a92..a6680947 100644 --- a/src/AccountSync.cc +++ b/src/AccountSync.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,22 @@ static Config get_config() { return current_config; } +static std::string source_label(const Config& cfg) { + if (!cfg.source.empty()) { + return cfg.source; + } + if (!cfg.source_region.empty() && !cfg.source_ship.empty()) { + return cfg.source_region + "-" + cfg.source_ship; + } + if (!cfg.source_region.empty()) { + return cfg.source_region; + } + if (!cfg.source_ship.empty()) { + return cfg.source_ship; + } + return "unknown"; +} + static std::string json_escape(const std::string& s) { std::string ret; ret.reserve(s.size() + 8); @@ -89,10 +106,13 @@ static void append_spool_line(const Config& cfg, const std::string& line) { static std::string base_event_json(const Config& cfg, const char* event, uint32_t account_id) { return std::format( - "{{\"timestamp_usecs\":{},\"source\":\"newserv\",\"event\":\"{}\",\"region\":\"{}\",\"account_id\":{},\"account_id_str\":\"{:010}\"", + "{{\"timestamp_usecs\":{},\"producer\":\"newserv\",\"source\":\"{}\",\"source_region\":\"{}\",\"source_ship\":\"{}\",\"account_store\":\"{}\",\"event\":\"{}\",\"account_id\":{},\"account_id_str\":\"{:010}\"", now_usecs(), + json_escape(source_label(cfg)), + json_escape(cfg.source_region), + json_escape(cfg.source_ship), + json_escape(cfg.account_store), json_escape(event), - json_escape(cfg.region), static_cast(account_id), static_cast(account_id)); } @@ -105,8 +125,11 @@ void configure(const Config& cfg) { if (cfg.enabled) { std::fprintf(stderr, - "[AccountSync] config enabled region=%s coordinator_url=%s notify_bb_sessions=%s spool_directory=%s login_locks=%s\n", - cfg.region.c_str(), + "[AccountSync] config enabled source=%s source_region=%s source_ship=%s account_store=%s coordinator_url=%s notify_bb_sessions=%s spool_directory=%s login_locks=%s\n", + source_label(cfg).c_str(), + cfg.source_region.c_str(), + cfg.source_ship.c_str(), + cfg.account_store.c_str(), cfg.coordinator_url.c_str(), cfg.notify_bb_sessions ? "true" : "false", cfg.spool_directory.c_str(), @@ -117,7 +140,23 @@ void configure(const Config& cfg) { void configure_from_json(const phosg::JSON& json) { Config cfg; cfg.enabled = json.get_bool("Enabled", false); - cfg.region = json.get_string("Region", ""); + + const std::string legacy_region = json.get_string("Region", ""); + cfg.source = json.get_string("Source", legacy_region); + cfg.source_region = json.get_string("SourceRegion", ""); + cfg.source_ship = json.get_string("SourceShip", ""); + cfg.account_store = json.get_string("AccountStore", "shared"); + + if (cfg.source_region.empty() && cfg.source_ship.empty() && !legacy_region.empty()) { + size_t dash_offset = legacy_region.find('-'); + if (dash_offset != std::string::npos) { + cfg.source_region = legacy_region.substr(0, dash_offset); + cfg.source_ship = legacy_region.substr(dash_offset + 1); + } else { + cfg.source_region = legacy_region; + } + } + cfg.coordinator_url = json.get_string("CoordinatorURL", ""); cfg.shared_secret = json.get_string("SharedSecret", ""); cfg.request_timeout_usecs = json.get_int("RequestTimeoutUsecs", 3000000); @@ -126,11 +165,7 @@ void configure_from_json(const phosg::JSON& json) { cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true); cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true); cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false); - - // Backward-compatible during transition: old test configs with EnableLoginLocks=true - // still emit BB session events, but this does not enforce locks. cfg.notify_bb_sessions = json.get_bool("NotifyBBSessions", cfg.enable_login_locks); - cfg.spool_directory = json.get_string("SpoolDirectory", "system/account-sync-spool"); configure(cfg); } @@ -142,8 +177,11 @@ void notify_account_saved(uint32_t account_id, const std::string& filename) { } std::fprintf(stderr, - "[AccountSync] event=account_saved region=%s account_id=%010u filename=%s\n", - cfg.region.c_str(), + "[AccountSync] event=account_saved source=%s source_region=%s source_ship=%s account_store=%s account_id=%010u filename=%s\n", + source_label(cfg).c_str(), + cfg.source_region.c_str(), + cfg.source_ship.c_str(), + cfg.account_store.c_str(), static_cast(account_id), filename.c_str()); @@ -159,8 +197,11 @@ void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& fi } std::fprintf(stderr, - "[AccountSync] event=backup_saved region=%s account_id=%010u slot=%zu filename=%s\n", - cfg.region.c_str(), + "[AccountSync] event=backup_saved source=%s source_region=%s source_ship=%s account_store=%s account_id=%010u slot=%zu filename=%s\n", + source_label(cfg).c_str(), + cfg.source_region.c_str(), + cfg.source_ship.c_str(), + cfg.account_store.c_str(), static_cast(account_id), slot, filename.c_str()); @@ -181,8 +222,11 @@ void notify_player_state_saved( } std::fprintf(stderr, - "[AccountSync] event=player_state_saved region=%s reason=%s account_id=%010u bb_username=%s filename=%s\n", - cfg.region.c_str(), + "[AccountSync] event=player_state_saved source=%s source_region=%s source_ship=%s account_store=%s reason=%s account_id=%010u bb_username=%s filename=%s\n", + source_label(cfg).c_str(), + cfg.source_region.c_str(), + cfg.source_ship.c_str(), + cfg.account_store.c_str(), reason, static_cast(account_id), bb_username.c_str(), @@ -207,8 +251,11 @@ void notify_bb_login_start( } std::fprintf(stderr, - "[AccountSync] event=bb_login_start region=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", - cfg.region.c_str(), + "[AccountSync] event=bb_login_start source=%s source_region=%s source_ship=%s account_store=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n", + source_label(cfg).c_str(), + cfg.source_region.c_str(), + cfg.source_ship.c_str(), + cfg.account_store.c_str(), static_cast(account_id), bb_username.c_str(), static_cast(character_slot), @@ -232,8 +279,11 @@ void notify_bb_login_end( } std::fprintf(stderr, - "[AccountSync] event=bb_login_end region=%s account_id=%010u bb_username=%s character_slot=%lld\n", - cfg.region.c_str(), + "[AccountSync] event=bb_login_end source=%s source_region=%s source_ship=%s account_store=%s account_id=%010u bb_username=%s character_slot=%lld\n", + source_label(cfg).c_str(), + cfg.source_region.c_str(), + cfg.source_ship.c_str(), + cfg.account_store.c_str(), static_cast(account_id), bb_username.c_str(), static_cast(character_slot)); diff --git a/src/AccountSync.hh b/src/AccountSync.hh index 3e01cffd..cbfeb78d 100644 --- a/src/AccountSync.hh +++ b/src/AccountSync.hh @@ -10,7 +10,14 @@ namespace AccountSync { struct Config { bool enabled = false; - std::string region; + + // Source identity. Region is no longer enough because account JSON is shared + // across live/test/hardcore in a region. + std::string source; + std::string source_region; + std::string source_ship; + std::string account_store = "shared"; + std::string coordinator_url; std::string shared_secret; uint64_t request_timeout_usecs = 3000000;