diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index d027757e..c18188ab 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -6262,12 +6262,17 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag case 0x0FEA: { // Set team flag auto team = c->team(); if (team && team->members.at(c->login->account->account_id).check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) { + const auto& cmd = check_size_t(msg.data); if (TeamSync::relay_team_actions_enabled()) { - // TeamSync phase 1 is membership-only. Do not mutate local-only team - // metadata that would be erased by the next authority apply. + if (TeamSync::enqueue_team_flag_update(team->team_id, &cmd.flag_data, sizeof(cmd.flag_data)) && + co_await TeamSync::exchange_once_now()) { + auto refreshed_team = s->team_index->get_by_id(team->team_id); + if (refreshed_team) { + send_team_metadata_change_notifications(s, refreshed_team, 0, TeamMetadataChange::FLAG_DATA); + } + } break; } - const auto& cmd = check_size_t(msg.data); s->team_index->set_flag_data(team->team_id, cmd.flag_data); send_team_metadata_change_notifications(s, team, 0, TeamMetadataChange::FLAG_DATA); } @@ -6277,9 +6282,12 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag auto team = c->team(); if (team && team->members.at(c->login->account->account_id).check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) { if (TeamSync::relay_team_actions_enabled()) { - // TeamSync phase 1 is membership-only. Disband is not yet routed - // through the authority. - send_command(c, 0x10EA, 0x00000001); + if (TeamSync::enqueue_team_disband(team->team_id) && co_await TeamSync::exchange_once_now()) { + send_command(c, 0x10EA, 0x00000000); + send_team_metadata_change_notifications(s, team, 0, TeamMetadataChange::TEAM_DISBANDED); + } else { + send_command(c, 0x10EA, 0x00000001); + } break; } s->team_index->disband(team->team_id); @@ -6297,9 +6305,39 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag } if (TeamSync::relay_team_actions_enabled()) { - // TeamSync phase 1 is membership-only. Privilege/master changes are - // not yet routed through the authority. - send_command(c, 0x11EA, 0x00000005); + if (!team->members.at(c->login->account->account_id).check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) { + send_command(c, 0x11EA, 0x00000005); + break; + } + + bool enqueued = false; + switch (msg.flag) { + case 0x00: + enqueued = TeamSync::enqueue_team_member_flags_update(cmd.guild_card_number, 0); + break; + case 0x30: + enqueued = TeamSync::enqueue_team_member_flags_update(cmd.guild_card_number, 0x02); + break; + case 0x40: + enqueued = TeamSync::enqueue_team_master_transfer(c->login->account->account_id, cmd.guild_card_number); + break; + default: + throw std::runtime_error("invalid privilege level"); + } + + if (enqueued && co_await TeamSync::exchange_once_now()) { + send_command(c, 0x11EA, 0x00000000); + auto refreshed_team = s->team_index->get_by_id(team->team_id); + if (refreshed_team) { + send_team_metadata_change_notifications( + s, + refreshed_team, + cmd.guild_card_number, + (msg.flag == 0x40) ? TeamMetadataChange::TEAM_MASTER : static_cast(0)); + } + } else { + send_command(c, 0x11EA, 0x00000005); + } break; } @@ -6361,8 +6399,23 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag } if (TeamSync::relay_team_actions_enabled()) { - // TeamSync phase 1 is membership-only. Rewards/points are not yet - // routed through the authority. + if (!TeamSync::enqueue_team_reward_purchase( + team->team_id, + reward.key, + reward.team_points, + static_cast(reward.reward_flag)) || + !co_await TeamSync::exchange_once_now()) { + break; + } + + auto refreshed_team = s->team_index->get_by_id(team->team_id); + if (reward.reward_flag != TeamIndex::Team::RewardFlag::NONE && refreshed_team) { + send_team_metadata_change_notifications(s, refreshed_team, 0, TeamMetadataChange::REWARD_FLAGS); + } + if (!reward.reward_item.empty()) { + c->bank_file()->add_item(reward.reward_item, *s->item_stack_limits(c->version())); + c->print_bank(); + } break; } @@ -6391,9 +6444,15 @@ static asio::awaitable on_EA_BB(std::shared_ptr c, Channel::Messag } else if (s->team_index->get_by_name(new_team_name)) { send_command(c, 0x1FEA, 0x00000002); } else if (TeamSync::relay_team_actions_enabled()) { - // TeamSync phase 1 is membership-only. Rename is not yet routed - // through the authority. - send_command(c, 0x1FEA, 0x00000001); + if (TeamSync::enqueue_team_rename(team->team_id, new_team_name) && co_await TeamSync::exchange_once_now()) { + send_command(c, 0x1FEA, 0x00000000); + auto refreshed_team = s->team_index->get_by_id(team->team_id); + if (refreshed_team) { + send_team_metadata_change_notifications(s, refreshed_team, 0, TeamMetadataChange::TEAM_NAME); + } + } else { + send_command(c, 0x1FEA, 0x00000001); + } } else { s->team_index->rename(team->team_id, new_team_name); send_command(c, 0x1FEA, 0x00000000); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index e1bbb6e7..bf125628 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -4787,8 +4787,8 @@ static void on_exchange_item_for_team_points_bb(std::shared_ptr c, Subco auto s = c->require_server_state(); if (TeamSync::relay_team_actions_enabled() && !TeamSync::relay_team_points_enabled()) { - // TeamSync phase 1 is membership-only. Team points are not yet routed - // through the authority, so do not consume the item locally. + // TeamSync points relay is disabled, so do not consume the item locally + // for a local-only point mutation that authority sync may overwrite. return; } diff --git a/src/ServerState.cc b/src/ServerState.cc index 18a22f3b..75e9ab72 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -1664,6 +1664,8 @@ void ServerState::load_teams() { std::string canonical_signature; canonical_signature += "next_team_id=" + std::to_string(canonical_team_state.get_int("next_team_id", canonical_team_state.get_int("NextTeamID", 0))) + "\n"; + bool empty_is_authoritative = canonical_team_state.get_bool("empty_is_authoritative", false); + canonical_signature += "empty_is_authoritative=" + std::to_string(empty_is_authoritative ? 1 : 0) + "\n"; canonical_signature += "teams=" + std::to_string(teams_json.size()) + "\n"; std::unordered_map account_id_to_team_id; @@ -1675,6 +1677,13 @@ void ServerState::load_teams() { } canonical_signature += "team=" + std::to_string(team_id) + "\n"; + canonical_signature += "name=" + team_json.get_string("name", team_json.get_string("Name", "")) + "\n"; + canonical_signature += "reward_flags=" + std::to_string(team_json.get_int("reward_flags", team_json.get_int("RewardFlags", 0))) + "\n"; + canonical_signature += "spent_points=" + std::to_string(team_json.get_int("spent_points", team_json.get_int("SpentPoints", 0))) + "\n"; + canonical_signature += "flag_data_hex=" + team_json.get_string("flag_data_hex", "") + "\n"; + for (const auto& key_json_p : team_json.get("reward_keys", phosg::JSON::list()).as_list()) { + canonical_signature += "reward_key=" + key_json_p->as_string() + "\n"; + } const auto& members_json = team_json.get("members", phosg::JSON::list()).as_list(); for (const auto& member_json_p : members_json) { @@ -1682,10 +1691,11 @@ void ServerState::load_teams() { uint32_t account_id = member_json.get_int("account_id", member_json.get_int("AccountID", 0)); uint32_t flags = member_json.get_int("flags", member_json.get_int("Flags", 0)); uint32_t points = member_json.get_int("points", member_json.get_int("Points", 0)); + std::string name = member_json.get_string("name", member_json.get_string("Name", "")); if (account_id) { account_id_to_team_id.emplace(account_id, team_id); canonical_signature += "member=" + std::to_string(account_id) + ":" + - std::to_string(flags) + ":" + std::to_string(points) + "\n"; + std::to_string(flags) + ":" + std::to_string(points) + ":" + name + "\n"; } } } @@ -1700,7 +1710,7 @@ void ServerState::load_teams() { } if (teams_json.empty()) { - if (local_team_count || local_account_team_refs) { + if (!empty_is_authoritative && (local_team_count || local_account_team_refs)) { std::string rejection_signature = canonical_signature + "local_team_count=" + std::to_string(local_team_count) + ";local_account_team_refs=" + std::to_string(local_account_team_refs); diff --git a/src/TeamIndex.cc b/src/TeamIndex.cc index d78654be..456f0215 100644 --- a/src/TeamIndex.cc +++ b/src/TeamIndex.cc @@ -1,5 +1,6 @@ #include "TeamIndex.hh" +#include #include #include #include @@ -11,6 +12,27 @@ #include "Loggers.hh" #include "StaticGameData.hh" +static uint8_t team_sync_hex_value(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 void team_sync_decode_hex_to_bytes(const std::string& hex, void* data, size_t size) { + if (hex.size() != size * 2) { + throw std::runtime_error("incorrect hex data size"); + } + auto* out = reinterpret_cast(data); + for (size_t z = 0; z < size; z++) { + out[z] = (team_sync_hex_value(hex[z * 2]) << 4) | team_sync_hex_value(hex[z * 2 + 1]); + } +} + TeamIndex::Team::Member::Member(const phosg::JSON& json) : flags(json.get_int("Flags", 0)), points(json.get_int("Points", 0)), name(json.get_string("Name", "")) { try { @@ -482,6 +504,12 @@ void TeamIndex::replace_all_from_authority(const phosg::JSON& canonical_team_sta team->spent_points = team_json.get_int("spent_points", 0); team->points = 0; + std::string flag_data_hex = team_json.get_string("flag_data_hex", ""); + if (!flag_data_hex.empty()) { + team->flag_data.reset(new parray()); + team_sync_decode_hex_to_bytes(flag_data_hex, team->flag_data.get(), sizeof(*team->flag_data)); + } + team->reward_keys.clear(); for (const auto& key_json_p : team_json.get("reward_keys", phosg::JSON::list()).as_list()) { team->reward_keys.emplace(key_json_p->as_string()); @@ -546,6 +574,9 @@ void TeamIndex::replace_all_from_authority(const phosg::JSON& canonical_team_sta for (const auto& [team_id, team] : this->id_to_team) { team->save_config(); + if (team->flag_data) { + team->save_flag(); + } } } diff --git a/src/TeamSync.cc b/src/TeamSync.cc index b45ac02b..34fe72eb 100644 --- a/src/TeamSync.cc +++ b/src/TeamSync.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,15 @@ struct OutboundEvent { std::string creator_name; int64_t points_delta = 0; + + std::string new_name; + uint8_t flags = 0; + uint32_t master_account_id = 0; + uint32_t new_master_account_id = 0; + std::string key; + uint32_t points = 0; + uint32_t reward_flag = 0; + std::string flag_data_hex; }; static constexpr size_t MAX_OUTBOUND_EVENTS = 256; @@ -196,6 +206,18 @@ static std::string json_escape(const std::string& s) { return ret; } +static std::string hex_encode(const void* data, size_t size) { + static const char* chars = "0123456789abcdef"; + const uint8_t* bytes = reinterpret_cast(data); + std::string ret; + ret.resize(size * 2); + for (size_t z = 0; z < size; z++) { + ret[z * 2] = chars[bytes[z] >> 4]; + ret[z * 2 + 1] = chars[bytes[z] & 0x0F]; + } + return ret; +} + static asio::awaitable read_http_line( asio::ip::tcp::socket& sock, std::string& pending_data, @@ -557,6 +579,121 @@ bool enqueue_team_member_update(uint32_t account_id, const std::string& name, in return true; } +bool enqueue_team_rename(uint32_t team_id, const std::string& new_name) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.relay_team_actions || team_id == 0 || new_name.empty()) { + return false; + } + + std::lock_guard g(exchange_state_mutex); + if (outbound_events.size() >= MAX_OUTBOUND_EVENTS) { + return false; + } + + OutboundEvent ev; + ev.type = "team_rename"; + ev.team_id = team_id; + ev.new_name = new_name; + outbound_events.emplace_back(std::move(ev)); + return true; +} + +bool enqueue_team_disband(uint32_t team_id) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.relay_team_actions || team_id == 0) { + return false; + } + + std::lock_guard g(exchange_state_mutex); + if (outbound_events.size() >= MAX_OUTBOUND_EVENTS) { + return false; + } + + OutboundEvent ev; + ev.type = "team_disband"; + ev.team_id = team_id; + outbound_events.emplace_back(std::move(ev)); + return true; +} + +bool enqueue_team_member_flags_update(uint32_t account_id, uint8_t flags) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.relay_team_actions || account_id == 0) { + return false; + } + + std::lock_guard g(exchange_state_mutex); + if (outbound_events.size() >= MAX_OUTBOUND_EVENTS) { + return false; + } + + OutboundEvent ev; + ev.type = "team_member_flags_update"; + ev.account_id = account_id; + ev.flags = flags; + outbound_events.emplace_back(std::move(ev)); + return true; +} + +bool enqueue_team_master_transfer(uint32_t master_account_id, uint32_t new_master_account_id) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.relay_team_actions || master_account_id == 0 || new_master_account_id == 0) { + return false; + } + + std::lock_guard g(exchange_state_mutex); + if (outbound_events.size() >= MAX_OUTBOUND_EVENTS) { + return false; + } + + OutboundEvent ev; + ev.type = "team_master_transfer"; + ev.master_account_id = master_account_id; + ev.new_master_account_id = new_master_account_id; + outbound_events.emplace_back(std::move(ev)); + return true; +} + +bool enqueue_team_reward_purchase(uint32_t team_id, const std::string& key, uint32_t points, uint32_t reward_flag) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.relay_team_actions || team_id == 0 || key.empty()) { + return false; + } + + std::lock_guard g(exchange_state_mutex); + if (outbound_events.size() >= MAX_OUTBOUND_EVENTS) { + return false; + } + + OutboundEvent ev; + ev.type = "team_reward_purchase"; + ev.team_id = team_id; + ev.key = key; + ev.points = points; + ev.reward_flag = reward_flag; + outbound_events.emplace_back(std::move(ev)); + return true; +} + +bool enqueue_team_flag_update(uint32_t team_id, const void* flag_data, size_t size) { + auto cfg = get_config(); + if (!cfg.enabled || !cfg.relay_team_actions || team_id == 0 || !flag_data || !size) { + return false; + } + + std::lock_guard g(exchange_state_mutex); + if (outbound_events.size() >= MAX_OUTBOUND_EVENTS) { + return false; + } + + OutboundEvent ev; + ev.type = "team_flag_update"; + ev.team_id = team_id; + ev.flag_data_hex = hex_encode(flag_data, size); + outbound_events.emplace_back(std::move(ev)); + return true; +} + bool enqueue_team_member_remove(uint32_t account_id) { auto cfg = get_config(); if (!cfg.enabled || !cfg.relay_team_actions) { @@ -643,6 +780,55 @@ static std::string exchange_body_for_current_state(const Config& cfg) { json_escape(ev.name), ev.points_delta); + } else if (ev.type == "team_rename") { + parts += std::format( + "{{\"seq\":{},\"type\":\"team_rename\",\"team_namespace\":\"{}\",\"team_id\":{},\"new_name\":\"{}\"}}", + ev.seq, + json_escape(cfg.team_namespace), + ev.team_id, + json_escape(ev.new_name)); + + } else if (ev.type == "team_disband") { + parts += std::format( + "{{\"seq\":{},\"type\":\"team_disband\",\"team_namespace\":\"{}\",\"team_id\":{}}}", + ev.seq, + json_escape(cfg.team_namespace), + ev.team_id); + + } else if (ev.type == "team_member_flags_update") { + parts += std::format( + "{{\"seq\":{},\"type\":\"team_member_flags_update\",\"team_namespace\":\"{}\",\"account_id\":{},\"flags\":{}}}", + ev.seq, + json_escape(cfg.team_namespace), + ev.account_id, + ev.flags); + + } else if (ev.type == "team_master_transfer") { + parts += std::format( + "{{\"seq\":{},\"type\":\"team_master_transfer\",\"team_namespace\":\"{}\",\"master_account_id\":{},\"new_master_account_id\":{}}}", + ev.seq, + json_escape(cfg.team_namespace), + ev.master_account_id, + ev.new_master_account_id); + + } else if (ev.type == "team_reward_purchase") { + parts += std::format( + "{{\"seq\":{},\"type\":\"team_reward_purchase\",\"team_namespace\":\"{}\",\"team_id\":{},\"key\":\"{}\",\"points\":{},\"reward_flag\":{}}}", + ev.seq, + json_escape(cfg.team_namespace), + ev.team_id, + json_escape(ev.key), + ev.points, + ev.reward_flag); + + } else if (ev.type == "team_flag_update") { + parts += std::format( + "{{\"seq\":{},\"type\":\"team_flag_update\",\"team_namespace\":\"{}\",\"team_id\":{},\"flag_data_hex\":\"{}\"}}", + ev.seq, + json_escape(cfg.team_namespace), + ev.team_id, + ev.flag_data_hex); + } else if (ev.type == "team_member_remove") { parts += std::format( "{{\"seq\":{},\"type\":\"team_member_remove\",\"team_namespace\":\"{}\",\"account_id\":{}}}", diff --git a/src/TeamSync.hh b/src/TeamSync.hh index 5bd7e633..f5c6f26f 100644 --- a/src/TeamSync.hh +++ b/src/TeamSync.hh @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -39,6 +40,12 @@ bool enqueue_team_create(const std::string& team_name, uint32_t creator_account_ 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); bool enqueue_team_member_remove(uint32_t account_id); +bool enqueue_team_rename(uint32_t team_id, const std::string& new_name); +bool enqueue_team_disband(uint32_t team_id); +bool enqueue_team_member_flags_update(uint32_t account_id, uint8_t flags); +bool enqueue_team_master_transfer(uint32_t master_account_id, uint32_t new_master_account_id); +bool enqueue_team_reward_purchase(uint32_t team_id, const std::string& key, uint32_t points, uint32_t reward_flag); +bool enqueue_team_flag_update(uint32_t team_id, const void* flag_data, size_t size); asio::awaitable exchange_once_now(); using CanonicalTeamStateCallback = std::function;