account sync hooks
Feature/account sync hooks
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "Account.hh"
|
||||
#include "AccountSync.hh"
|
||||
|
||||
std::shared_ptr<DCNTELicense> DCNTELicense::from_json(const phosg::JSON& json) {
|
||||
auto ret = std::make_shared<DCNTELicense>();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
#include "AccountSync.hh"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
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<microseconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
static Config get_config() {
|
||||
std::lock_guard<std::mutex> g(config_mutex);
|
||||
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);
|
||||
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<char>(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<std::mutex> 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\":{},\"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),
|
||||
static_cast<unsigned int>(account_id),
|
||||
static_cast<unsigned int>(account_id));
|
||||
}
|
||||
|
||||
void configure(const Config& cfg) {
|
||||
{
|
||||
std::lock_guard<std::mutex> g(config_mutex);
|
||||
current_config = cfg;
|
||||
}
|
||||
|
||||
if (cfg.enabled) {
|
||||
std::fprintf(stderr,
|
||||
"[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(),
|
||||
cfg.enable_login_locks ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
void configure_from_json(const phosg::JSON& json) {
|
||||
Config cfg;
|
||||
cfg.enabled = json.get_bool("Enabled", false);
|
||||
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
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 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<unsigned int>(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) {
|
||||
auto cfg = get_config();
|
||||
if (!cfg.enabled || !cfg.notify_backup_saves) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fprintf(stderr,
|
||||
"[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<unsigned int>(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(
|
||||
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 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<unsigned int>(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(
|
||||
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.notify_bb_sessions) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fprintf(stderr,
|
||||
"[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<unsigned int>(account_id),
|
||||
bb_username.c_str(),
|
||||
static_cast<long long>(character_slot),
|
||||
static_cast<unsigned int>(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<long long>(character_slot),
|
||||
static_cast<unsigned int>(connection_phase));
|
||||
append_spool_line(cfg, line);
|
||||
}
|
||||
|
||||
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.notify_bb_sessions) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fprintf(stderr,
|
||||
"[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<unsigned int>(account_id),
|
||||
bb_username.c_str(),
|
||||
static_cast<long long>(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<long long>(character_slot));
|
||||
append_spool_line(cfg, line);
|
||||
}
|
||||
|
||||
} // namespace AccountSync
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <phosg/JSON.hh>
|
||||
|
||||
namespace AccountSync {
|
||||
|
||||
struct Config {
|
||||
bool enabled = false;
|
||||
|
||||
// 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;
|
||||
bool fail_open = false;
|
||||
bool notify_account_saves = true;
|
||||
bool notify_player_saves = true;
|
||||
bool notify_backup_saves = true;
|
||||
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);
|
||||
void configure_from_json(const phosg::JSON& json);
|
||||
|
||||
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);
|
||||
|
||||
void notify_player_state_saved(
|
||||
const char* reason,
|
||||
uint32_t account_id,
|
||||
const std::string& bb_username,
|
||||
const std::string& filename);
|
||||
|
||||
void notify_bb_login_start(
|
||||
uint32_t account_id,
|
||||
const std::string& bb_username,
|
||||
int64_t character_slot,
|
||||
uint8_t connection_phase);
|
||||
|
||||
void notify_bb_login_end(
|
||||
uint32_t account_id,
|
||||
const std::string& bb_username,
|
||||
int64_t character_slot);
|
||||
|
||||
} // namespace AccountSync
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "Server.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
#include "AccountSync.hh"
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Tools
|
||||
@@ -491,12 +492,22 @@ static asio::awaitable<void> 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<void> 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<void> 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());
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "SendCommands.hh"
|
||||
#include "Server.hh"
|
||||
#include "Version.hh"
|
||||
#include "AccountSync.hh"
|
||||
|
||||
const uint64_t CLIENT_CONFIG_MAGIC = 0x8399AC32;
|
||||
|
||||
@@ -232,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");
|
||||
}
|
||||
@@ -503,6 +511,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 +557,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 +656,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 +863,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) {
|
||||
|
||||
@@ -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<void> on_93_BB(std::shared_ptr<Client> 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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user