Add TeamSync canonical team state apply scaffold
This commit is contained in:
@@ -1648,6 +1648,18 @@ void ServerState::load_accounts() {
|
||||
void ServerState::load_teams() {
|
||||
config_log.info_f("Indexing teams");
|
||||
this->team_index = std::make_shared<TeamIndex>("system/teams", this->team_reward_defs_json);
|
||||
|
||||
TeamSync::set_canonical_team_state_callback([this](const phosg::JSON& canonical_team_state) -> void {
|
||||
if (!this->team_index) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this->team_index->replace_all_from_authority(canonical_team_state);
|
||||
config_log.info_f("Applied canonical TeamSync team state");
|
||||
} catch (const std::exception& e) {
|
||||
config_log.warning_f("Failed to apply canonical TeamSync team state: {}", e.what());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ServerState::load_patch_indexes() {
|
||||
|
||||
@@ -454,6 +454,102 @@ void TeamIndex::buy_reward(uint32_t team_id, const std::string& key, uint32_t po
|
||||
team->save_config();
|
||||
}
|
||||
|
||||
void TeamIndex::replace_all_from_authority(const phosg::JSON& canonical_team_state) {
|
||||
uint32_t new_next_team_id = canonical_team_state.get_int("next_team_id", 1);
|
||||
if (new_next_team_id < 1) {
|
||||
new_next_team_id = 1;
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Team>> new_id_to_team;
|
||||
std::unordered_map<std::string, std::shared_ptr<Team>> new_name_to_team;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Team>> new_account_id_to_team;
|
||||
|
||||
const auto& teams_json = canonical_team_state.get("teams", phosg::JSON::list()).as_list();
|
||||
for (const auto& team_json_p : teams_json) {
|
||||
const auto& team_json = *team_json_p;
|
||||
uint32_t team_id = team_json.get_int("team_id");
|
||||
if (team_id == 0) {
|
||||
throw std::runtime_error("authority team has invalid team_id");
|
||||
}
|
||||
|
||||
auto team = std::make_shared<Team>(team_id);
|
||||
team->name = team_json.get_string("name", "");
|
||||
if (team->name.empty()) {
|
||||
throw std::runtime_error("authority team has empty name");
|
||||
}
|
||||
|
||||
team->reward_flags = team_json.get_int("reward_flags", 0);
|
||||
team->spent_points = team_json.get_int("spent_points", 0);
|
||||
team->points = 0;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
const auto& members_json = team_json.get("members", phosg::JSON::list()).as_list();
|
||||
for (const auto& member_json_p : members_json) {
|
||||
const auto& member_json = *member_json_p;
|
||||
Team::Member m;
|
||||
m.account_id = member_json.get_int("account_id", member_json.get_int("AccountID", 0));
|
||||
m.flags = member_json.get_int("flags", member_json.get_int("Flags", 0));
|
||||
m.points = member_json.get_int("points", member_json.get_int("Points", 0));
|
||||
m.name = member_json.get_string("name", member_json.get_string("Name", ""));
|
||||
|
||||
if (m.account_id == 0) {
|
||||
throw std::runtime_error("authority team member has invalid account_id");
|
||||
}
|
||||
if (m.check_flag(Team::Member::Flag::IS_MASTER)) {
|
||||
team->master_account_id = m.account_id;
|
||||
}
|
||||
team->points += m.points;
|
||||
team->members.emplace(m.account_id, std::move(m));
|
||||
}
|
||||
|
||||
if (team->members.empty()) {
|
||||
throw std::runtime_error("authority team has no members");
|
||||
}
|
||||
|
||||
if (!new_id_to_team.emplace(team->team_id, team).second) {
|
||||
throw std::runtime_error("authority state has duplicate team_id");
|
||||
}
|
||||
if (!new_name_to_team.emplace(team->name, team).second) {
|
||||
throw std::runtime_error("authority state has duplicate team name");
|
||||
}
|
||||
for (const auto& [account_id, member] : team->members) {
|
||||
if (!new_account_id_to_team.emplace(account_id, team).second) {
|
||||
throw std::runtime_error("authority state has account in multiple teams");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(this->directory.c_str());
|
||||
|
||||
for (const auto& item : std::filesystem::directory_iterator(this->directory)) {
|
||||
const std::string filename = item.path().filename().string();
|
||||
if ((filename != "base.json") && (filename.ends_with(".json") || filename.ends_with(".bmp"))) {
|
||||
std::filesystem::remove(item.path());
|
||||
}
|
||||
}
|
||||
|
||||
phosg::save_file(
|
||||
this->directory + "/base.json",
|
||||
phosg::JSON::dict({{"NextTeamID", new_next_team_id}}).serialize(
|
||||
phosg::JSON::SerializeOption::FORMAT |
|
||||
phosg::JSON::SerializeOption::HEX_INTEGERS |
|
||||
phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY));
|
||||
|
||||
this->next_team_id = new_next_team_id;
|
||||
this->id_to_team.swap(new_id_to_team);
|
||||
this->name_to_team.swap(new_name_to_team);
|
||||
this->account_id_to_team.swap(new_account_id_to_team);
|
||||
|
||||
for (const auto& [team_id, team] : this->id_to_team) {
|
||||
team->save_config();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void TeamIndex::add_to_indexes(std::shared_ptr<Team> team) {
|
||||
if (!this->id_to_team.emplace(team->team_id, team).second) {
|
||||
throw std::runtime_error("team ID is already in use");
|
||||
|
||||
@@ -144,6 +144,10 @@ public:
|
||||
void change_master(uint32_t master_account_id, uint32_t new_master_account_id);
|
||||
void buy_reward(uint32_t team_id, const std::string& key, uint32_t points, Team::RewardFlag reward_flag);
|
||||
|
||||
// Replaces all local BB team state with coordinator-authoritative state.
|
||||
// This updates disk and in-memory indexes immediately.
|
||||
void replace_all_from_authority(const phosg::JSON& canonical_team_state);
|
||||
|
||||
protected:
|
||||
std::string directory;
|
||||
uint32_t next_team_id;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
@@ -32,6 +33,9 @@ struct ExchangeState {
|
||||
static std::mutex exchange_state_mutex;
|
||||
static ExchangeState exchange_state;
|
||||
|
||||
static std::mutex canonical_team_state_callback_mutex;
|
||||
static CanonicalTeamStateCallback canonical_team_state_callback;
|
||||
|
||||
static std::string source_label(const Config& cfg) {
|
||||
if (!cfg.source.empty()) {
|
||||
return cfg.source;
|
||||
@@ -431,6 +435,22 @@ bool relay_team_chat_enabled() {
|
||||
return cfg.enabled && cfg.relay_team_chat;
|
||||
}
|
||||
|
||||
void set_canonical_team_state_callback(CanonicalTeamStateCallback cb) {
|
||||
std::lock_guard<std::mutex> g(canonical_team_state_callback_mutex);
|
||||
canonical_team_state_callback = std::move(cb);
|
||||
}
|
||||
|
||||
static void apply_canonical_team_state(const phosg::JSON& canonical_team_state) {
|
||||
CanonicalTeamStateCallback cb;
|
||||
{
|
||||
std::lock_guard<std::mutex> g(canonical_team_state_callback_mutex);
|
||||
cb = canonical_team_state_callback;
|
||||
}
|
||||
if (cb) {
|
||||
cb(canonical_team_state);
|
||||
}
|
||||
}
|
||||
|
||||
// This helper only builds empty event batches from trusted config fields.
|
||||
// Do not extend the format-string JSON path for player-controlled data; use a
|
||||
// real JSON object/serializer when team chat events are added.
|
||||
@@ -467,6 +487,11 @@ static asio::awaitable<void> 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& 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);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> g(exchange_state_mutex);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <asio.hpp>
|
||||
@@ -32,6 +33,9 @@ void configure_from_json(const phosg::JSON& json);
|
||||
bool enabled();
|
||||
bool relay_team_chat_enabled();
|
||||
|
||||
using CanonicalTeamStateCallback = std::function<void(const phosg::JSON&)>;
|
||||
void set_canonical_team_state_callback(CanonicalTeamStateCallback cb);
|
||||
|
||||
void start_exchange_task(asio::io_context& io_context);
|
||||
|
||||
} // namespace TeamSync
|
||||
|
||||
Reference in New Issue
Block a user