From 133041f09b3ac6353f93d1f90e661ce2922cd488 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 12 Jun 2026 20:40:15 -0400 Subject: [PATCH 1/3] Relay TeamSync team chat events --- src/ReceiveCommands.cc | 10 +++++++ src/ServerState.cc | 63 ++++++++++++++++++++++++++++++++++++++++ src/TeamSync.cc | 65 +++++++++++++++++++++++++++++++++++++++++- src/TeamSync.hh | 4 +++ 4 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index c18188ab..9157db8c 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -6241,6 +6241,16 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag } catch (const std::out_of_range&) { } } + + if (TeamSync::relay_team_chat_enabled()) { + TeamSync::enqueue_team_chat( + team->team_id, + c->login->account->account_id, + c->character_file()->disp.visual.name.decode(c->language()), + msg.data.data(), + msg.data.size()); + co_await TeamSync::exchange_once_now(); + } } } break; diff --git a/src/ServerState.cc b/src/ServerState.cc index 75e9ab72..dbe8f3fb 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -27,6 +27,31 @@ static constexpr bool IS_WINDOWS = true; static constexpr bool IS_WINDOWS = false; #endif +static uint8_t team_sync_hex_value_server_state(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } else if (ch >= 'a' && ch <= 'f') { + return 10 + ch - 'a'; + } else if (ch >= 'A' && ch <= 'F') { + return 10 + ch - 'A'; + } + throw std::runtime_error("invalid hex character"); +} + +static std::string team_sync_decode_hex_string_server_state(const std::string& hex) { + if (hex.size() & 1) { + throw std::runtime_error("hex string has odd length"); + } + std::string ret; + ret.resize(hex.size() / 2); + for (size_t z = 0; z < ret.size(); z++) { + ret[z] = static_cast( + (team_sync_hex_value_server_state(hex[z * 2]) << 4) | + team_sync_hex_value_server_state(hex[z * 2 + 1])); + } + return ret; +} + CheatFlags::CheatFlags(const phosg::JSON& json) : CheatFlags() { std::unordered_set enabled_keys; for (const auto& it : json.as_list()) { @@ -1758,6 +1783,44 @@ void ServerState::load_teams() { config_log.warning_f("Failed to apply canonical TeamSync team state: {}", e.what()); } }); + + TeamSync::set_inbound_event_callback([this](const phosg::JSON& event) -> void { + try { + if (!this->team_index) { + return; + } + + std::string type = event.get_string("type", ""); + if (type != "team_chat") { + return; + } + + uint32_t team_id = event.get_int("team_id", 0); + uint32_t sender_account_id = event.get_int("sender_account_id", 0); + std::string message_data_hex = event.get_string("message_data_hex", ""); + std::string message_data = team_sync_decode_hex_string_server_state(message_data_hex); + + auto team = this->team_index->get_by_id(team_id); + if (!team) { + return; + } + + for (const auto& it : team->members) { + uint32_t target_account_id = it.second.account_id; + if (target_account_id == sender_account_id) { + continue; + } + try { + auto target_c = this->find_client(nullptr, target_account_id); + send_command(target_c, 0x07EA, 0x00000000, message_data); + } catch (const std::out_of_range&) { + } + } + + } catch (const std::exception& e) { + config_log.warning_f("Failed to apply inbound TeamSync event: {}", e.what()); + } + }); } void ServerState::load_patch_indexes() { diff --git a/src/TeamSync.cc b/src/TeamSync.cc index 34fe72eb..9b646440 100644 --- a/src/TeamSync.cc +++ b/src/TeamSync.cc @@ -40,6 +40,10 @@ struct OutboundEvent { uint32_t account_id = 0; std::string name; + uint32_t sender_account_id = 0; + std::string sender_name; + std::string message_data_hex; + std::string team_name; uint32_t creator_account_id = 0; std::string creator_name; @@ -65,6 +69,9 @@ static std::deque outbound_events; static std::mutex canonical_team_state_callback_mutex; static CanonicalTeamStateCallback canonical_team_state_callback; +static std::mutex inbound_event_callback_mutex; +static InboundEventCallback inbound_event_callback; + static std::string source_label(const Config& cfg) { if (!cfg.source.empty()) { return cfg.source; @@ -486,6 +493,31 @@ bool relay_team_actions_enabled() { return cfg.enabled && cfg.relay_team_actions; } +bool enqueue_team_chat(uint32_t team_id, uint32_t sender_account_id, const std::string& sender_name, const void* data, size_t size) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.relay_team_chat || team_id == 0 || sender_account_id == 0 || !data || !size || size > 2048) { + return false; + } + + std::lock_guard g(exchange_state_mutex); + if (outbound_events.size() >= MAX_OUTBOUND_EVENTS) { + std::fprintf(stderr, + "[TeamSync] warning outbound_event_dropped reason=queue_full source=%s team_namespace=%s type=team_chat\n", + source_label(cfg).c_str(), + cfg.team_namespace.c_str()); + return false; + } + + OutboundEvent ev; + ev.type = "team_chat"; + ev.team_id = team_id; + ev.sender_account_id = sender_account_id; + ev.sender_name = sender_name; + ev.message_data_hex = hex_encode(data, size); + outbound_events.emplace_back(std::move(ev)); + return true; +} + bool enqueue_team_create(const std::string& team_name, uint32_t creator_account_id, const std::string& creator_name) { auto cfg = get_config(); if (!cfg.enabled || !cfg.relay_team_actions) { @@ -724,6 +756,22 @@ void set_canonical_team_state_callback(CanonicalTeamStateCallback cb) { canonical_team_state_callback = std::move(cb); } +void set_inbound_event_callback(InboundEventCallback cb) { + std::lock_guard g(inbound_event_callback_mutex); + inbound_event_callback = std::move(cb); +} + +static void apply_inbound_event(const phosg::JSON& event) { + InboundEventCallback cb; + { + std::lock_guard g(inbound_event_callback_mutex); + cb = inbound_event_callback; + } + if (cb) { + cb(event); + } +} + static void apply_canonical_team_state(const phosg::JSON& canonical_team_state) { CanonicalTeamStateCallback cb; { @@ -753,7 +801,17 @@ static std::string exchange_body_for_current_state(const Config& cfg) { } first = false; - if (ev.type == "team_create") { + if (ev.type == "team_chat") { + parts += std::format( + "{{\"seq\":{},\"type\":\"team_chat\",\"team_namespace\":\"{}\",\"team_id\":{},\"sender_account_id\":{},\"sender_name\":\"{}\",\"message_data_hex\":\"{}\"}}", + ev.seq, + json_escape(cfg.team_namespace), + ev.team_id, + ev.sender_account_id, + json_escape(ev.sender_name), + ev.message_data_hex); + + } else if (ev.type == "team_create") { parts += std::format( "{{\"seq\":{},\"type\":\"team_create\",\"team_namespace\":\"{}\",\"creator_account_id\":{},\"creator_name\":\"{}\",\"team_name\":\"{}\"}}", ev.seq, @@ -873,6 +931,11 @@ static asio::awaitable run_empty_exchange_once(const Config& cfg) { bool truncated = response.get_bool("truncated", false); size_t inbound_events = response.get("events", phosg::JSON::list()).as_list().size(); + const auto& events_json = response.get("events", phosg::JSON::list()).as_list(); + for (const auto& event_json_p : events_json) { + apply_inbound_event(*event_json_p); + } + const auto& canonical_team_state = response.get("canonical_team_state", phosg::JSON::dict()); if (!canonical_team_state.as_dict().empty()) { apply_canonical_team_state(canonical_team_state); diff --git a/src/TeamSync.hh b/src/TeamSync.hh index f5c6f26f..f997bf1b 100644 --- a/src/TeamSync.hh +++ b/src/TeamSync.hh @@ -36,6 +36,7 @@ bool relay_team_chat_enabled(); bool relay_team_points_enabled(); bool relay_team_actions_enabled(); +bool enqueue_team_chat(uint32_t team_id, uint32_t sender_account_id, const std::string& sender_name, const void* data, size_t size); bool enqueue_team_create(const std::string& team_name, uint32_t creator_account_id, const std::string& creator_name); bool enqueue_team_member_add(uint32_t team_id, uint32_t account_id, const std::string& name); bool enqueue_team_member_update(uint32_t account_id, const std::string& name, int64_t points_delta); @@ -51,6 +52,9 @@ asio::awaitable exchange_once_now(); using CanonicalTeamStateCallback = std::function; void set_canonical_team_state_callback(CanonicalTeamStateCallback cb); +using InboundEventCallback = std::function; +void set_inbound_event_callback(InboundEventCallback cb); + void start_exchange_task(asio::io_context& io_context); } // namespace TeamSync From 15266e0ef926fa5ce5ff44b2b80773c797122467 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 12 Jun 2026 21:36:16 -0400 Subject: [PATCH 2/3] Log TeamSync team chat relay attempts --- src/ReceiveCommands.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 9157db8c..e1264e22 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -6243,12 +6243,18 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag } if (TeamSync::relay_team_chat_enabled()) { - TeamSync::enqueue_team_chat( + const bool queued_team_chat = TeamSync::enqueue_team_chat( team->team_id, c->login->account->account_id, c->character_file()->disp.visual.name.decode(c->language()), msg.data.data(), msg.data.size()); + config_log.info_f( + "TeamSync team_chat relay attempt team_id={:08X} account_id={:08X} size={} queued={}", + team->team_id, + c->login->account->account_id, + msg.data.size(), + queued_team_chat); co_await TeamSync::exchange_once_now(); } } From 4e8253c38f6337723c7d06161848aa01021b2d84 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 12 Jun 2026 21:50:54 -0400 Subject: [PATCH 3/3] Log TeamSync chat exchange flow --- src/TeamSync.cc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/TeamSync.cc b/src/TeamSync.cc index 9b646440..4c0d702d 100644 --- a/src/TeamSync.cc +++ b/src/TeamSync.cc @@ -928,6 +928,19 @@ static asio::awaitable run_empty_exchange_once(const Config& cfg) { uint64_t ack_max_seq = response.get_int("ack_max_seq", 0); uint64_t next_cursor = response.get_int("next_cursor", 0); + + const auto& stats_json = response.get("stats", phosg::JSON::dict()); + std::fprintf(stderr, + "[TeamSync] exchange response source=%s team_namespace=%s ack_max_seq=%" PRIu64 " next_seq=%" PRIu64 " cursor=%" PRIu64 " accepted=%" PRId64 " duplicates=%" PRId64 " blocked=%" PRId64 " retained_events=%" PRId64 "\n", + source_label(cfg).c_str(), + cfg.team_namespace.c_str(), + ack_max_seq, + exchange_state.next_seq, + next_cursor, + stats_json.get_int("accepted", -1), + stats_json.get_int("duplicates", -1), + stats_json.get_int("blocked", -1), + stats_json.get_int("retained_events", -1)); bool truncated = response.get_bool("truncated", false); size_t inbound_events = response.get("events", phosg::JSON::list()).as_list().size();