Add TeamSync canonical team state apply scaffold
This commit is contained in:
@@ -1648,6 +1648,18 @@ void ServerState::load_accounts() {
|
|||||||
void ServerState::load_teams() {
|
void ServerState::load_teams() {
|
||||||
config_log.info_f("Indexing teams");
|
config_log.info_f("Indexing teams");
|
||||||
this->team_index = std::make_shared<TeamIndex>("system/teams", this->team_reward_defs_json);
|
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() {
|
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();
|
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) {
|
void TeamIndex::add_to_indexes(std::shared_ptr<Team> team) {
|
||||||
if (!this->id_to_team.emplace(team->team_id, team).second) {
|
if (!this->id_to_team.emplace(team->team_id, team).second) {
|
||||||
throw std::runtime_error("team ID is already in use");
|
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 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);
|
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:
|
protected:
|
||||||
std::string directory;
|
std::string directory;
|
||||||
uint32_t next_team_id;
|
uint32_t next_team_id;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <random>
|
#include <random>
|
||||||
@@ -32,6 +33,9 @@ struct ExchangeState {
|
|||||||
static std::mutex exchange_state_mutex;
|
static std::mutex exchange_state_mutex;
|
||||||
static ExchangeState exchange_state;
|
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) {
|
static std::string source_label(const Config& cfg) {
|
||||||
if (!cfg.source.empty()) {
|
if (!cfg.source.empty()) {
|
||||||
return cfg.source;
|
return cfg.source;
|
||||||
@@ -431,6 +435,22 @@ bool relay_team_chat_enabled() {
|
|||||||
return cfg.enabled && cfg.relay_team_chat;
|
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.
|
// 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
|
// Do not extend the format-string JSON path for player-controlled data; use a
|
||||||
// real JSON object/serializer when team chat events are added.
|
// 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);
|
bool truncated = response.get_bool("truncated", false);
|
||||||
size_t inbound_events = response.get("events", phosg::JSON::list()).as_list().size();
|
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);
|
std::lock_guard<std::mutex> g(exchange_state_mutex);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <asio.hpp>
|
#include <asio.hpp>
|
||||||
@@ -32,6 +33,9 @@ void configure_from_json(const phosg::JSON& json);
|
|||||||
bool enabled();
|
bool enabled();
|
||||||
bool relay_team_chat_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);
|
void start_exchange_task(asio::io_context& io_context);
|
||||||
|
|
||||||
} // namespace TeamSync
|
} // namespace TeamSync
|
||||||
|
|||||||
Reference in New Issue
Block a user