switch to coroutine execution model

This commit is contained in:
Martin Michelsen
2025-04-30 21:43:06 -07:00
parent f65b1f1c14
commit cc99050964
160 changed files with 269127 additions and 227736 deletions
+224 -325
View File
@@ -1,6 +1,5 @@
#include "ShellCommands.hh"
#include <event2/event.h>
#include <stdio.h>
#include <string.h>
@@ -8,42 +7,70 @@
#include <phosg/Strings.hh>
#include "ChatCommands.hh"
#include "GameServer.hh"
#include "ReceiveCommands.hh"
#include "ReplaySession.hh"
#include "SendCommands.hh"
#include "ServerState.hh"
#include "StaticGameData.hh"
using namespace std;
std::vector<const ShellCommand*> ShellCommand::commands_by_order;
std::unordered_map<std::string, const ShellCommand*> ShellCommand::commands_by_name;
vector<const ShellCommand*> ShellCommand::commands_by_order;
unordered_map<string, const ShellCommand*> ShellCommand::commands_by_name;
exit_shell::exit_shell() : runtime_error("shell exited") {}
shared_ptr<Client> ShellCommand::Args::get_client() const {
if (!s->game_server) {
throw logic_error("game server is missing");
}
shared_ptr<Client> c;
if (this->session_name.empty()) {
return this->s->game_server->get_client();
} else {
auto clients = this->s->game_server->get_clients_by_identifier(this->session_name);
if (clients.empty()) {
throw runtime_error("no such client");
}
if (clients.size() > 1) {
throw runtime_error("multiple clients found");
}
return clients[0];
}
}
shared_ptr<Client> ShellCommand::Args::get_proxy_client() const {
auto c = this->get_client();
if (!c->proxy_session) {
throw runtime_error("client is not in a proxy session");
}
return c;
}
ShellCommand::ShellCommand(
const char* name,
const char* help_text,
bool run_on_event_thread,
std::deque<std::string> (*run)(Args&))
asio::awaitable<deque<string>> (*run)(Args&))
: name(name),
help_text(help_text),
run_on_event_thread(run_on_event_thread),
run(run) {
ShellCommand::commands_by_order.emplace_back(this);
ShellCommand::commands_by_name.emplace(this->name, this);
}
std::deque<std::string> ShellCommand::dispatch_str(std::shared_ptr<ServerState> s, const std::string& command) {
asio::awaitable<deque<string>> ShellCommand::dispatch_str(shared_ptr<ServerState> s, const string& command) {
size_t command_end = phosg::skip_non_whitespace(command, 0);
size_t args_begin = phosg::skip_whitespace(command, command_end);
Args args;
args.s = s;
args.command = command.substr(0, command_end);
args.args = command.substr(args_begin);
return ShellCommand::dispatch(args);
co_return co_await ShellCommand::dispatch(args);
}
std::deque<std::string> ShellCommand::dispatch(Args& args) {
asio::awaitable<deque<string>> ShellCommand::dispatch(Args& args) {
const ShellCommand* def = nullptr;
try {
def = commands_by_name.at(args.command);
@@ -51,24 +78,11 @@ std::deque<std::string> ShellCommand::dispatch(Args& args) {
}
if (!def) {
throw runtime_error("no such command; try 'help'");
} else if (def->run_on_event_thread) {
return args.s->call_on_event_thread<std::deque<std::string>>([def, &args]() -> std::deque<std::string> {
return def->run(args);
});
} else {
return def->run(args);
}
}
static shared_ptr<ProxyServer::LinkedSession> get_proxy_session(std::shared_ptr<ServerState> s, const string& name) {
if (!s->proxy_server.get()) {
throw runtime_error("the proxy server is disabled");
}
return name.empty()
? s->proxy_server->get_session()
: s->proxy_server->get_session_by_name(name);
}
static string get_quoted_string(string& s) {
string ret;
char end_char = (s.at(0) == '\"') ? '\"' : ' ';
@@ -95,20 +109,19 @@ static string get_quoted_string(string& s) {
return ret;
}
static auto empty_handler = +[](ShellCommand::Args&) -> std::deque<std::string> {
return {};
};
static asio::awaitable<deque<string>> empty_handler(ShellCommand::Args&) {
co_return deque<string>();
}
ShellCommand c_nop1("", nullptr, false, empty_handler);
ShellCommand c_nop2("//", nullptr, false, empty_handler);
ShellCommand c_nop3("#", nullptr, false, empty_handler);
ShellCommand c_nop1("", nullptr, empty_handler);
ShellCommand c_nop2("//", nullptr, empty_handler);
ShellCommand c_nop3("#", nullptr, empty_handler);
ShellCommand c_help(
"help", "help\n\
You\'re reading it now.",
false,
+[](ShellCommand::Args&) -> std::deque<std::string> {
std::deque<std::string> ret({"Commands:"});
+[](ShellCommand::Args&) -> asio::awaitable<deque<string>> {
deque<string> ret({"Commands:"});
for (const auto& def : ShellCommand::commands_by_order) {
if (def->help_text) {
// TODO: It's not great that we copy the text here.
@@ -116,13 +129,12 @@ ShellCommand c_help(
s += def->help_text;
}
}
return ret;
co_return ret;
});
ShellCommand c_exit(
"exit", "exit (or ctrl+d)\n\
Shut down the server.",
false,
+[](ShellCommand::Args&) -> std::deque<std::string> {
+[](ShellCommand::Args&) -> asio::awaitable<deque<string>> {
throw exit_shell();
});
ShellCommand c_on(
@@ -134,8 +146,7 @@ ShellCommand c_on(
gamertag, or a BB account username. For proxy commands, SESSION should be\n\
the session ID, which generally is the same as the player\'s account ID\n\
and appears after \"LinkedSession:\" in the log output.",
false,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
size_t session_name_end = phosg::skip_non_whitespace(args.args, 0);
size_t command_begin = phosg::skip_whitespace(args.args, session_name_end);
size_t command_end = phosg::skip_non_whitespace(args.args, command_begin);
@@ -176,82 +187,76 @@ ShellCommand c_reload(
disconnect or reload the battle parameters, so if these are changed without\n\
restarting, clients may see (for example) EXP messages inconsistent with\n\
the amounts of EXP actually received.",
false,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto types = phosg::split(args.args, ' ');
for (const auto& type : types) {
if (type == "all") {
args.s->call_on_event_thread<void>([s = args.s]() {
s->load_all();
});
args.s->load_all(true);
} else if (type == "bb-keys") {
args.s->load_bb_private_keys(true);
args.s->load_bb_private_keys();
} else if (type == "accounts") {
args.s->load_accounts(true);
args.s->load_accounts();
} else if (type == "maps") {
args.s->load_maps(true);
args.s->load_maps();
} else if (type == "caches") {
args.s->clear_file_caches(true);
args.s->clear_file_caches();
} else if (type == "patch-files") {
args.s->load_patch_indexes(true);
args.s->load_patch_indexes();
} else if (type == "ep3-cards") {
args.s->load_ep3_cards(true);
args.s->load_ep3_cards();
} else if (type == "ep3-maps") {
args.s->load_ep3_maps(true);
args.s->load_ep3_maps();
} else if (type == "ep3-tournaments") {
args.s->load_ep3_tournament_state(true);
args.s->load_ep3_tournament_state();
} else if (type == "functions") {
args.s->compile_functions(true);
args.s->compile_functions();
} else if (type == "dol-files") {
args.s->load_dol_files(true);
args.s->load_dol_files();
} else if (type == "set-tables") {
args.s->load_set_data_tables(true);
args.s->load_set_data_tables();
} else if (type == "battle-params") {
args.s->load_battle_params(true);
args.s->load_battle_params();
} else if (type == "level-tables") {
args.s->load_level_tables(true);
args.s->load_level_tables();
} else if (type == "text-index") {
args.s->load_text_index(true);
args.s->load_text_index();
} else if (type == "word-select") {
args.s->load_word_select_table(true);
args.s->load_word_select_table();
} else if (type == "item-definitions") {
args.s->load_item_definitions(true);
args.s->load_item_definitions();
} else if (type == "item-name-index") {
args.s->load_item_name_indexes(true);
args.s->load_item_name_indexes();
} else if (type == "drop-tables") {
args.s->load_drop_tables(true);
args.s->load_drop_tables();
} else if (type == "config") {
args.s->forward_to_event_thread([s = args.s]() {
s->load_config_early();
s->load_config_late();
});
args.s->load_config_early();
args.s->load_config_late();
} else if (type == "teams") {
args.s->load_teams(true);
args.s->load_teams();
} else if (type == "quests") {
args.s->load_quest_index(true);
args.s->load_quest_index();
} else {
throw runtime_error("invalid data type: " + type);
}
}
return {};
co_return deque<string>{};
});
ShellCommand c_list_accounts(
"list-accounts", "list-accounts\n\
List all accounts registered on the server.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
deque<string> ret;
auto accounts = args.s->account_index->all();
if (accounts.empty()) {
return {"No accounts registered"};
ret.emplace_back("No accounts registered");
} else {
std::deque<std::string> ret;
for (const auto& a : accounts) {
ret.emplace_back(a->str());
}
return ret;
}
co_return ret;
});
uint32_t parse_account_flags(const string& flags_str) {
@@ -360,21 +365,20 @@ ShellCommand c_add_account(
IS_SHARED_ACCOUNT: Account is a shared serial (disables Access Key and\n\
password checks; players will get Guild Cards based on their player\n\
names)",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto account = make_shared<Account>();
for (const string& token : phosg::split(args.args, ' ')) {
if (phosg::starts_with(token, "id=")) {
if (token.starts_with("id=")) {
account->account_id = stoul(token.substr(3), nullptr, 16);
} else if (phosg::starts_with(token, "ep3-current-meseta=")) {
} else if (token.starts_with("ep3-current-meseta=")) {
account->ep3_current_meseta = stoul(token.substr(19), nullptr, 0);
} else if (phosg::starts_with(token, "ep3-total-meseta=")) {
} else if (token.starts_with("ep3-total-meseta=")) {
account->ep3_total_meseta_earned = stoul(token.substr(17), nullptr, 0);
} else if (token == "temporary") {
account->is_temporary = true;
} else if (phosg::starts_with(token, "flags=")) {
} else if (token.starts_with("flags=")) {
account->flags = parse_account_flags(token.substr(6));
} else if (phosg::starts_with(token, "user-flags=")) {
} else if (token.starts_with("user-flags=")) {
account->user_flags = parse_account_user_flags(token.substr(11));
} else {
throw invalid_argument("invalid account field: " + token);
@@ -382,7 +386,7 @@ ShellCommand c_add_account(
}
args.s->account_index->add(account);
account->save();
return {phosg::string_printf("Account %08" PRIX32 " added", account->account_id)};
co_return deque<string>{format("Account {:08X} added", account->account_id)};
});
ShellCommand c_update_account(
"update-account", "update-account ACCOUNT-ID PARAMETERS...\n\
@@ -401,8 +405,7 @@ ShellCommand c_update_account(
temporary: Marks the account as temporary; it is not saved to disk and\n\
therefore will be deleted when the server shuts down.\n\
permanent: If the account was temporary, makes it non-temporary.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto tokens = phosg::split(args.args, ' ');
if (tokens.size() < 2) {
throw runtime_error("not enough arguments");
@@ -419,35 +422,35 @@ ShellCommand c_update_account(
uint8_t new_is_temporary = 0xFF;
int64_t new_ban_duration = -1;
for (const string& token : tokens) {
if (phosg::starts_with(token, "ep3-current-meseta=")) {
if (token.starts_with("ep3-current-meseta=")) {
new_ep3_current_meseta = stoul(token.substr(19), nullptr, 0);
} else if (phosg::starts_with(token, "ep3-total-meseta=")) {
} else if (token.starts_with("ep3-total-meseta=")) {
new_ep3_total_meseta = stoul(token.substr(17), nullptr, 0);
} else if (token == "temporary") {
new_is_temporary = 1;
} else if (token == "permanent") {
new_is_temporary = 0;
} else if (phosg::starts_with(token, "flags=")) {
} else if (token.starts_with("flags=")) {
new_flags = parse_account_flags(token.substr(6));
} else if (phosg::starts_with(token, "user-flags=")) {
} else if (token.starts_with("user-flags=")) {
new_user_flags = parse_account_user_flags(token.substr(11));
} else if (token == "unban") {
new_ban_duration = 0;
} else if (phosg::starts_with(token, "ban-duration=")) {
} else if (token.starts_with("ban-duration=")) {
auto duration_str = token.substr(13);
if (phosg::ends_with(duration_str, "s")) {
if (duration_str.ends_with("s")) {
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 1000000LL;
} else if (phosg::ends_with(duration_str, "m")) {
} else if (duration_str.ends_with("m")) {
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 60000000LL;
} else if (phosg::ends_with(duration_str, "h")) {
} else if (duration_str.ends_with("h")) {
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 3600000000LL;
} else if (phosg::ends_with(duration_str, "d")) {
} else if (duration_str.ends_with("d")) {
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 86400000000LL;
} else if (phosg::ends_with(duration_str, "w")) {
} else if (duration_str.ends_with("w")) {
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 604800000000LL;
} else if (phosg::ends_with(duration_str, "mo")) {
} else if (duration_str.ends_with("mo")) {
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 2)) * 2952000000000LL;
} else if (phosg::ends_with(duration_str, "y")) {
} else if (duration_str.ends_with("y")) {
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 31536000000000LL;
} else {
throw runtime_error("invalid time unit");
@@ -481,19 +484,18 @@ ShellCommand c_update_account(
args.s->disconnect_all_banned_clients();
}
return {phosg::string_printf("Account %08" PRIX32 " updated", account->account_id)};
co_return deque<string>{format("Account {:08X} updated", account->account_id)};
});
ShellCommand c_delete_account(
"delete-account", "delete-account ACCOUNT-ID\n\
Delete an account from the server. If a player is online with the deleted\n\
account, they will not be automatically disconnected.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto account = args.s->account_index->from_account_id(stoul(args.args, nullptr, 16));
args.s->account_index->remove(account->account_id);
account->is_temporary = true;
account->delete_file();
return {"Account deleted"};
co_return deque<string>{"Account deleted"};
});
ShellCommand c_add_license(
@@ -512,8 +514,7 @@ ShellCommand c_add_license(
add-license 385A92C4 DC 107862F9 d38XTu2p\n\
add-license 385A92C4 GC 0418572923 282949185033 hunter2\n\
add-license 385A92C4 BB user1 trustno1",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto tokens = phosg::split(args.args, ' ');
if (tokens.size() < 3) {
throw runtime_error("not enough arguments");
@@ -583,7 +584,7 @@ ShellCommand c_add_license(
}
account->save();
return {phosg::string_printf("Account %08" PRIX32 " updated", account->account_id)};
co_return deque<string>{format("Account {:08X} updated", account->account_id)};
});
ShellCommand c_delete_license(
"delete-license", "delete-license ACCOUNT-ID TYPE PRIMARY-CREDENTIAL\n\
@@ -602,8 +603,7 @@ ShellCommand c_delete_license(
delete-license 385A92C4 GC 0418572923\n\
delete-license 385A92C4 XB 7E29A2950019EB20\n\
delete-license 385A92C4 BB user1",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto tokens = phosg::split(args.args, ' ');
if (tokens.size() != 3) {
throw runtime_error("incorrect argument count");
@@ -629,21 +629,20 @@ ShellCommand c_delete_license(
}
account->save();
return {phosg::string_printf("Account %08" PRIX32 " updated", account->account_id)};
co_return deque<string>{format("Account {:08X} updated", account->account_id)};
});
ShellCommand c_lookup(
"lookup", "lookup USER\n\
Find the account for a logged-in user.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto target = args.s->find_client(&args.args);
if (target->login) {
return {phosg::string_printf("Found client %s with account ID %08" PRIX32,
target->channel.name.c_str(), target->login->account->account_id)};
co_return deque<string>{format("Found client {} with account ID {:08X}",
target->channel->name, target->login->account->account_id)};
} else {
// This should be impossible
throw std::logic_error("find_client found user who is not logged in");
throw logic_error("find_client found user who is not logged in");
}
});
ShellCommand c_kick(
@@ -651,29 +650,26 @@ ShellCommand c_kick(
Disconnect a user from the server. USER may be an account ID, player name,\n\
or client ID (beginning with \"C-\"). This does not ban the user; they are\n\
free to reconnect after doing this.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto target = args.s->find_client(&args.args);
send_message_box(target, "$C6You have been kicked off the server.");
args.s->game_server->disconnect_client(target);
return {phosg::string_printf("Client C-%" PRIX64 " disconnected from server", target->id)};
target->channel->disconnect();
co_return deque<string>{format("Client C-{:X} disconnected from server", target->id)};
});
ShellCommand c_announce(
"announce", "announce MESSAGE\n\
Send an announcement message to all players.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
send_text_or_scrolling_message(args.s, args.args, args.args);
return {};
co_return deque<string>{};
});
ShellCommand c_announce_mail(
"announce-mail", "announce-mail MESSAGE\n\
Send an announcement message via Simple Mail to all players.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
send_simple_mail(args.s, 0, args.s->name, args.args);
return {};
co_return deque<string>{};
});
ShellCommand c_create_tournament(
@@ -704,8 +700,7 @@ ShellCommand c_create_tournament(
dialogue=ON/OFF: Enable/disable dialogue\n\
dice-exchange=ATK/DEF/NONE: Set dice exchange mode\n\
dice-boost=ON/OFF: Enable/disable dice boost",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string name = get_quoted_string(args.args);
string map_name = get_quoted_string(args.args);
auto map = args.s->ep3_map_index->for_name(map_name);
@@ -725,7 +720,7 @@ ShellCommand c_create_tournament(
flags |= Episode3::Tournament::Flag::SHUFFLE_ENTRIES;
} else if (token == "resize") {
flags |= Episode3::Tournament::Flag::RESIZE_ON_START;
} else if (phosg::starts_with(token, "dice=")) {
} else if (token.starts_with("dice=")) {
auto parse_range_c = +[](const string& s) -> uint8_t {
auto tokens = phosg::split(s, '-');
if (tokens.size() != 2) {
@@ -768,7 +763,7 @@ ShellCommand c_create_tournament(
rules.atk_dice_value_range_2v1 = 0;
rules.def_dice_value_range_2v1 = 0;
}
} else if (phosg::starts_with(token, "overall-time-limit=")) {
} else if (token.starts_with("overall-time-limit=")) {
uint32_t limit = stoul(token.substr(19));
if (limit > 600) {
throw runtime_error("overall-time-limit must be 600 or fewer minutes");
@@ -777,9 +772,9 @@ ShellCommand c_create_tournament(
throw runtime_error("overall-time-limit must be a multiple of 5 minutes");
}
rules.overall_time_limit = limit;
} else if (phosg::starts_with(token, "phase-time-limit=")) {
} else if (token.starts_with("phase-time-limit=")) {
rules.phase_time_limit = stoul(token.substr(17));
} else if (phosg::starts_with(token, "hp=")) {
} else if (token.starts_with("hp=")) {
rules.char_hp = stoul(token.substr(3));
} else if (token == "allowed-cards=all") {
rules.allowed_cards = Episode3::AllowedCards::ALL;
@@ -826,24 +821,23 @@ ShellCommand c_create_tournament(
}
}
}
std::deque<std::string> ret;
deque<string> ret;
if (rules.check_and_reset_invalid_fields()) {
ret.emplace_back("Warning: Some rules were invalid and reset to defaults");
}
auto tourn = args.s->ep3_tournament_index->create_tournament(name, map, rules, num_teams, flags);
ret.emplace_back(phosg::string_printf("Created tournament \"%s\"", tourn->get_name().c_str()));
return ret;
ret.emplace_back(format("Created tournament \"{}\"", tourn->get_name()));
co_return ret;
});
ShellCommand c_delete_tournament(
"delete-tournament", "delete-tournament TOURNAMENT-NAME\n\
Delete a tournament. Quotes are required around the tournament name unless\n\
the name contains no spaces.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string name = get_quoted_string(args.args);
if (args.s->ep3_tournament_index->delete_tournament(name)) {
return {"Deleted tournament"};
co_return deque<string>{"Deleted tournament"};
} else {
throw runtime_error("tournament does not exist");
}
@@ -851,28 +845,26 @@ ShellCommand c_delete_tournament(
ShellCommand c_list_tournaments(
"list-tournaments", "list-tournaments\n\
List the names and numbers of all existing tournaments.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
std::deque<std::string> ret;
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
deque<string> ret;
for (const auto& it : args.s->ep3_tournament_index->all_tournaments()) {
ret.emplace_back(" " + it.second->get_name());
}
return ret;
co_return ret;
});
ShellCommand c_start_tournament(
"start-tournament", "start-tournament TOURNAMENT-NAME\n\
End registration for a tournament and allow matches to begin. Quotes are\n\
required around the tournament name unless the name contains no spaces.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string name = get_quoted_string(args.args);
auto tourn = args.s->ep3_tournament_index->get_tournament(name);
if (tourn) {
tourn->start();
args.s->ep3_tournament_index->save();
tourn->send_all_state_updates();
send_ep3_text_message_printf(args.s, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str());
return {"Tournament started"};
send_ep3_text_message_fmt(args.s, "$C7The tournament\n$C6{}$C7\nhas begun", tourn->get_name());
co_return deque<string>{"Tournament started"};
} else {
throw runtime_error("tournament does not exist");
}
@@ -881,12 +873,11 @@ ShellCommand c_describe_tournament(
"describe-tournament", "describe-tournament TOURNAMENT-NAME\n\
Show the current state of a tournament. Quotes are required around the\n\
tournament name unless the name contains no spaces.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string name = get_quoted_string(args.args);
auto tourn = args.s->ep3_tournament_index->get_tournament(name);
if (tourn) {
return {tourn->bracket_str()};
co_return deque<string>{tourn->bracket_str()};
} else {
throw runtime_error("tournament does not exist");
}
@@ -902,126 +893,65 @@ ShellCommand c_cc(
via this command are exempt from permission checks, so commands that\n\
require cheat mode or debug mode are always available via cc even if the\n\
player cannot normamlly use them.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
shared_ptr<ProxyServer::LinkedSession> ses;
try {
ses = get_proxy_session(args.s, args.session_name);
} catch (const exception&) {
}
if (ses.get()) {
on_chat_command(ses, args.args, false);
} else {
shared_ptr<Client> c;
if (args.session_name.empty()) {
c = args.s->game_server->get_client();
} else {
auto clients = args.s->game_server->get_clients_by_identifier(args.session_name);
if (clients.empty()) {
throw runtime_error("no such client");
}
if (clients.size() > 1) {
throw runtime_error("multiple clients found");
}
c = std::move(clients[0]);
}
if (c) {
on_chat_command(c, args.args, false);
} else {
throw runtime_error("no client available");
}
}
return {};
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto c = args.get_client();
co_await on_chat_command(c, args.args, false);
co_return deque<string>{};
});
std::deque<std::string> f_sc_ss(ShellCommand::Args& args) {
asio::awaitable<deque<string>> f_sc_ss(ShellCommand::Args& args) {
string data = phosg::parse_data_string(args.args, nullptr, phosg::ParseDataFlags::ALLOW_FILES);
if (data.size() == 0) {
throw invalid_argument("no data given");
}
data.resize((data.size() + 3) & (~3));
shared_ptr<ProxyServer::LinkedSession> ses;
try {
ses = get_proxy_session(args.s, args.session_name);
} catch (const exception&) {
}
if (ses.get()) {
if (args.command[1] == 's') {
ses->server_channel.send(data);
} else {
ses->client_channel.send(data);
}
auto c = args.get_client();
if (args.command[1] == 's') {
co_await on_command_with_header(c, data);
} else {
shared_ptr<Client> c;
if (args.session_name.empty()) {
c = args.s->game_server->get_client();
} else {
auto clients = args.s->game_server->get_clients_by_identifier(args.session_name);
if (clients.empty()) {
throw runtime_error("no such client");
}
if (clients.size() > 1) {
throw runtime_error("multiple clients found");
}
c = std::move(clients[0]);
}
if (c) {
if (args.command[1] == 's') {
on_command_with_header(c, data);
} else {
send_command_with_header(c->channel, data.data(), data.size());
}
} else {
throw runtime_error("no client available");
}
send_command_with_header(c->channel, data.data(), data.size());
}
return {};
co_return deque<string>{};
}
ShellCommand c_sc("sc", "sc DATA\n\
Send a command to the client. This command also can be used to send data to\n\
a client on the game server.",
true, f_sc_ss);
Send a network command to the client.",
f_sc_ss);
ShellCommand c_ss("ss", "ss DATA\n\
Send a command to the remote server.",
true, f_sc_ss);
Send a network command to the server.",
f_sc_ss);
ShellCommand c_show_slots(
"show-slots", "show-slots\n\
Show the player names, Guild Card numbers, and client IDs of all players in\n\
the current lobby or game.",
true,
+[](ShellCommand::Args& args) -> std::deque<std::string> {
std::deque<std::string> ret;
auto ses = get_proxy_session(args.s, args.session_name);
for (size_t z = 0; z < ses->lobby_players.size(); z++) {
const auto& player = ses->lobby_players[z];
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto c = args.get_proxy_client();
deque<string> ret;
for (size_t z = 0; z < c->proxy_session->lobby_players.size(); z++) {
const auto& player = c->proxy_session->lobby_players[z];
if (player.guild_card_number) {
ret.emplace_back(phosg::string_printf(" %zu: %" PRIu32 " => %s (%c, %s, %s)",
z, player.guild_card_number, player.name.c_str(),
ret.emplace_back(format(" {}: {} => {} ({}, {}, {})",
z, player.guild_card_number, player.name,
char_for_language_code(player.language),
name_for_char_class(player.char_class),
name_for_section_id(player.section_id)));
} else {
ret.emplace_back(phosg::string_printf(" %zu: (no player)", z));
ret.emplace_back(format(" {}: (no player)", z));
}
}
return ret;
co_return ret;
});
std::deque<std::string> fn_chat(ShellCommand::Args& args) {
auto ses = get_proxy_session(args.s, args.session_name);
asio::awaitable<deque<string>> fn_chat(ShellCommand::Args& args) {
auto c = args.get_proxy_client();
bool is_dchat = (args.command == "dchat");
if (!is_dchat && uses_utf16(ses->version())) {
send_chat_message_from_client(ses->server_channel, args.args, 0);
if (!is_dchat && uses_utf16(c->version())) {
send_chat_message_from_client(c->proxy_session->server_channel, args.args, 0);
} else {
string data(8, '\0');
data.push_back('\x09');
@@ -1033,22 +963,22 @@ std::deque<std::string> fn_chat(ShellCommand::Args& args) {
data.push_back('\0');
}
data.resize((data.size() + 3) & (~3));
ses->server_channel.send(0x06, 0x00, data);
c->proxy_session->server_channel->send(0x06, 0x00, data);
}
return {};
co_return deque<string>{};
}
ShellCommand c_c("c", "c TEXT", true, fn_chat);
ShellCommand c_c("c", "c TEXT", fn_chat);
ShellCommand c_chat("chat", "chat TEXT\n\
Send a chat message to the server.",
true, fn_chat);
fn_chat);
ShellCommand c_dchat("dchat", "dchat DATA\n\
Send a chat message to the server with arbitrary data in it.",
true, fn_chat);
fn_chat);
std::deque<std::string> fn_wchat(ShellCommand::Args& args) {
auto ses = get_proxy_session(args.s, args.session_name);
if (!is_ep3(ses->version())) {
asio::awaitable<deque<string>> fn_wchat(ShellCommand::Args& args) {
auto c = args.get_proxy_client();
if (!is_ep3(c->version())) {
throw runtime_error("wchat can only be used on Episode 3");
}
string data(8, '\0');
@@ -1058,43 +988,42 @@ std::deque<std::string> fn_wchat(ShellCommand::Args& args) {
data += args.args;
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
ses->server_channel.send(0x06, 0x00, data);
return {};
c->proxy_session->server_channel->send(0x06, 0x00, data);
co_return deque<string>{};
}
ShellCommand c_wc("wc", "wc TEXT", true, fn_wchat);
ShellCommand c_wc("wc", "wc TEXT", fn_wchat);
ShellCommand c_wchat("wchat", "wchat TEXT\n\
Send a chat message with private_flags on Episode 3.",
true, fn_wchat);
fn_wchat);
ShellCommand c_marker(
"marker", "marker COLOR-ID\n\
Change your lobby marker color.",
true, +[](ShellCommand::Args& args) -> std::deque<std::string> {
auto ses = get_proxy_session(args.s, args.session_name);
ses->server_channel.send(0x89, stoul(args.args));
return {};
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto c = args.get_proxy_client();
c->proxy_session->server_channel->send(0x89, stoul(args.args));
co_return deque<string>{};
});
std::deque<std::string> fn_warp(ShellCommand::Args& args) {
auto ses = get_proxy_session(args.s, args.session_name);
asio::awaitable<deque<string>> fn_warp(ShellCommand::Args& args) {
auto c = args.get_proxy_client();
uint8_t floor = stoul(args.args);
send_warp(ses->client_channel, ses->lobby_client_id, floor, true);
send_warp(c->channel, c->lobby_client_id, floor, true);
if (args.command == "warpall") {
send_warp(ses->server_channel, ses->lobby_client_id, floor, false);
send_warp(c->proxy_session->server_channel, c->lobby_client_id, floor, false);
}
return {};
co_return deque<string>{};
}
ShellCommand c_warp("warp", "warp FLOOR-ID", true, fn_warp);
ShellCommand c_warp("warp", "warp FLOOR-ID", fn_warp);
ShellCommand c_warpme("warpme", "warpme FLOOR-ID\n\
Send yourself to a specific floor.",
true, fn_warp);
fn_warp);
ShellCommand c_warpall("warpall", "warpall FLOOR-ID\n\
Send everyone to a specific floor.",
true, fn_warp);
fn_warp);
std::deque<std::string> fn_info_board(ShellCommand::Args& args) {
auto ses = get_proxy_session(args.s, args.session_name);
asio::awaitable<deque<string>> fn_info_board(ShellCommand::Args& args) {
auto c = args.get_proxy_client();
string data;
if (args.command == "info-board-data") {
@@ -1105,83 +1034,53 @@ std::deque<std::string> fn_info_board(ShellCommand::Args& args) {
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
ses->server_channel.send(0xD9, 0x00, data);
return {};
c->proxy_session->server_channel->send(0xD9, 0x00, data);
co_return deque<string>{};
}
ShellCommand c_info_board("info-board", "info-board TEXT\n\
Set your info board contents. This will affect the current session only,\n\
and will not be saved for future sessions.",
true, fn_info_board);
fn_info_board);
ShellCommand c_info_board_data("info-board-data", "info-board-data DATA\n\
Set your info board contents with arbitrary data. Like the above, affects\n\
the current session only.",
true, fn_info_board);
fn_info_board);
ShellCommand c_set_challenge_rank_title(
"set-challenge-rank-title", "set-challenge-rank-title TEXT\n\
Set the player\'s override Challenge rank text.",
true, +[](ShellCommand::Args& args) -> std::deque<std::string> {
auto ses = get_proxy_session(args.s, args.session_name);
ses->challenge_rank_title_override = args.args;
return {};
});
ShellCommand c_set_challenge_rank_color(
"set-challenge-rank-color", "set-challenge-rank-color RRGGBBAA\n\
Set the player\'s override Challenge rank color.",
true, +[](ShellCommand::Args& args) -> std::deque<std::string> {
auto ses = get_proxy_session(args.s, args.session_name);
ses->challenge_rank_color_override = stoul(args.args, nullptr, 16);
return {};
});
std::deque<std::string> fn_create_item(ShellCommand::Args& args) {
auto ses = get_proxy_session(args.s, args.session_name);
if (ses->version() == Version::BB_V4) {
throw runtime_error("proxy session is BB");
}
if (!ses->is_in_game) {
throw runtime_error("proxy session is not in a game");
}
if (ses->lobby_client_id != ses->leader_client_id) {
throw runtime_error("proxy session is not game leader");
}
auto s = ses->require_server_state();
ItemData item = s->parse_item_description(ses->version(), args.args);
item.id = phosg::random_object<uint32_t>() | 0x80000000;
if (args.command == "set-next-item") {
ses->next_drop_item = item;
string name = s->describe_item(ses->version(), ses->next_drop_item, true);
send_text_message(ses->client_channel, "$C7Next drop:\n" + name);
} else {
send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->pos);
send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->pos);
string name = s->describe_item(ses->version(), ses->next_drop_item, true);
send_text_message(ses->client_channel, "$C7Item created:\n" + name);
}
return {};
}
ShellCommand c_create_item("create-item", "create-item DATA\n\
ShellCommand c_create_item(
"create-item", "create-item DATA\n\
Create an item as if the client had run the $item command.",
true, fn_create_item);
ShellCommand c_set_next_item("set-next-item", "set-next-item DATA\n\
Set the next item to be dropped.",
true, fn_create_item);
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
auto c = args.get_proxy_client();
ShellCommand c_close_idle_sessions(
"close-idle-sessions", "close-idle-sessions\n\
Close all proxy sessions that don\'t have a client and server connected.",
true, +[](ShellCommand::Args& args) -> std::deque<std::string> {
if (args.s->proxy_server) {
size_t count = args.s->proxy_server->delete_disconnected_sessions();
return {phosg::string_printf("%zu sessions closed\n", count)};
} else {
throw runtime_error("the proxy server is disabled");
if (c->version() == Version::BB_V4) {
throw runtime_error("proxy session is BB");
}
if (!c->proxy_session->is_in_game) {
throw runtime_error("proxy session is not in a game");
}
if (c->lobby_client_id != c->proxy_session->leader_client_id) {
throw runtime_error("proxy session is not game leader");
}
ItemData item = args.s->parse_item_description(c->version(), args.args);
item.id = phosg::random_object<uint32_t>() | 0x80000000;
send_drop_stacked_item_to_channel(args.s, c->channel, item, c->floor, c->pos);
send_drop_stacked_item_to_channel(args.s, c->proxy_session->server_channel, item, c->floor, c->pos);
string name = args.s->describe_item(c->version(), item, true);
send_text_message(c->channel, "$C7Item created:\n" + name);
co_return deque<string>{};
});
ShellCommand c_replay_log(
"replay-log", nullptr,
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
if (args.s->allow_saving_accounts) {
throw runtime_error("Replays cannot be run when account saving is enabled");
}
auto log_f = phosg::fopen_shared(args.args, "rt");
auto replay_session = make_shared<ReplaySession>(args.s, log_f.get(), true);
co_await replay_session->run();
co_return deque<string>{};
});