3243 lines
120 KiB
C++
3243 lines
120 KiB
C++
#include "ChatCommands.hh"
|
|
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
|
|
#include <filesystem>
|
|
#include <phosg/Random.hh>
|
|
#include <phosg/Strings.hh>
|
|
#include <phosg/Time.hh>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include "Client.hh"
|
|
#include "GameServer.hh"
|
|
#include "Lobby.hh"
|
|
#include "Loggers.hh"
|
|
#include "ProxySession.hh"
|
|
#include "ReceiveCommands.hh"
|
|
#include "Revision.hh"
|
|
#include "SendCommands.hh"
|
|
#include "Server.hh"
|
|
#include "StaticGameData.hh"
|
|
#include "Text.hh"
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Tools
|
|
|
|
std::string str_for_flag_ranges(const std::vector<bool>& flags) {
|
|
std::string ret;
|
|
auto add_result = [&](size_t start, size_t end) {
|
|
if (!ret.empty()) {
|
|
ret.push_back(',');
|
|
}
|
|
if (start == end) {
|
|
ret += std::format("{}", start);
|
|
} else if (start == end - 1) {
|
|
ret += std::format("{},{}", start, end);
|
|
} else {
|
|
ret += std::format("{}-{}", start, end);
|
|
}
|
|
};
|
|
|
|
size_t range_start = 0;
|
|
bool in_range = false;
|
|
for (size_t z = 0; z < flags.size(); z++) {
|
|
if (flags[z] && !in_range) {
|
|
in_range = true;
|
|
range_start = z;
|
|
} else if (!flags[z] && in_range) {
|
|
in_range = false;
|
|
add_result(range_start, z - 1);
|
|
}
|
|
}
|
|
if (in_range) {
|
|
add_result(range_start, flags.size() - 1);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Checks
|
|
|
|
class precondition_failed {
|
|
public:
|
|
template <typename... ArgTs>
|
|
precondition_failed(std::format_string<ArgTs...> fmt, ArgTs&&... args)
|
|
: user_msg(std::format(fmt, std::forward<ArgTs>(args)...)) {}
|
|
|
|
~precondition_failed() = default;
|
|
|
|
const std::string& what() const {
|
|
return this->user_msg;
|
|
}
|
|
|
|
private:
|
|
std::string user_msg;
|
|
};
|
|
|
|
struct Args {
|
|
std::string text;
|
|
bool check_permissions;
|
|
std::shared_ptr<Client> c;
|
|
|
|
void check_is_proxy(bool is_proxy) const {
|
|
if (is_proxy && (this->c->proxy_session == nullptr)) {
|
|
throw precondition_failed("$C6This command can\nonly be used in\nproxy sessions.");
|
|
} else if (!is_proxy && (this->c->proxy_session != nullptr)) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in proxy\nsessions.");
|
|
}
|
|
}
|
|
|
|
void check_version(Version version) const {
|
|
if (this->c->version() != version) {
|
|
throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO.");
|
|
}
|
|
}
|
|
|
|
void check_is_ep3(bool is_ep3) const {
|
|
if (::is_ep3(this->c->version()) && !is_ep3) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in Episode 3.");
|
|
} else if (!::is_ep3(this->c->version()) && is_ep3) {
|
|
throw precondition_failed("$C6This command can only\nbe used in Episode 3.");
|
|
}
|
|
}
|
|
|
|
void check_is_game(bool is_game) const {
|
|
bool client_in_game = this->c->proxy_session
|
|
? this->c->proxy_session->is_in_game
|
|
: this->c->require_lobby()->is_game();
|
|
if (client_in_game && !is_game) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in games.");
|
|
} else if (!client_in_game && is_game) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in lobbies.");
|
|
}
|
|
}
|
|
|
|
void check_account_flag(Account::Flag flag) const {
|
|
if (!this->c->login) {
|
|
throw precondition_failed("$C6You are not\nlogged in.");
|
|
}
|
|
if (this->check_permissions && !this->c->login->account->check_flag(flag)) {
|
|
throw precondition_failed("$C6You do not have\npermission to\nrun this command.");
|
|
}
|
|
}
|
|
|
|
void check_debug_enabled() const {
|
|
if (this->check_permissions && !this->c->check_flag(Client::Flag::DEBUG_ENABLED)) {
|
|
throw precondition_failed("$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
|
}
|
|
}
|
|
|
|
void check_cheats_enabled_in_game(bool behavior_is_cheating) const {
|
|
if (behavior_is_cheating &&
|
|
this->check_permissions &&
|
|
!this->c->require_lobby()->check_flag(Lobby::Flag::CHEATS_ENABLED) &&
|
|
!this->c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) {
|
|
throw precondition_failed("$C6This command can\nonly be used in\ncheat mode.");
|
|
}
|
|
}
|
|
|
|
void check_cheat_mode_available(bool behavior_is_cheating) const {
|
|
if (behavior_is_cheating &&
|
|
this->check_permissions &&
|
|
(this->c->require_server_state()->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) &&
|
|
(!this->c->login || !this->c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) {
|
|
throw precondition_failed("$C6Cheats are disabled");
|
|
}
|
|
}
|
|
|
|
void check_cheats_enabled_or_allowed(bool behavior_is_cheating) const {
|
|
if (this->c->proxy_session) {
|
|
this->check_cheat_mode_available(behavior_is_cheating);
|
|
} else {
|
|
this->check_cheats_enabled_in_game(behavior_is_cheating);
|
|
}
|
|
}
|
|
|
|
void check_is_leader() const {
|
|
if (this->check_permissions && (this->c->require_lobby()->leader_id != this->c->lobby_client_id)) {
|
|
throw precondition_failed("$C6This command can\nonly be used by\nthe game leader.");
|
|
}
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Command definitions
|
|
|
|
struct ChatCommandDefinition {
|
|
using Handler = asio::awaitable<void> (*)(const Args& args);
|
|
|
|
std::vector<const char*> names;
|
|
Handler handler;
|
|
|
|
static std::unordered_map<std::string, const ChatCommandDefinition*> all_defs;
|
|
|
|
ChatCommandDefinition(std::initializer_list<const char*> names, Handler handler)
|
|
: names(names), handler(handler) {
|
|
for (const char* name : this->names) {
|
|
if (!this->all_defs.emplace(name, this).second) {
|
|
throw std::logic_error("duplicate command definition: " + std::string(name));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
std::unordered_map<std::string, const ChatCommandDefinition*> ChatCommandDefinition::all_defs;
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// All commands (in alphabetical order)
|
|
|
|
ChatCommandDefinition cc_allevent(
|
|
{"$allevent"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_account_flag(Account::Flag::CHANGE_EVENT);
|
|
|
|
uint8_t new_event = event_for_name(a.text);
|
|
if (new_event == 0xFF) {
|
|
throw precondition_failed("$C6No such lobby event");
|
|
}
|
|
|
|
auto s = a.c->require_server_state();
|
|
for (auto l : s->all_lobbies()) {
|
|
if (l->is_game() || !l->check_flag(Lobby::Flag::DEFAULT)) {
|
|
continue;
|
|
}
|
|
|
|
l->event = new_event;
|
|
send_change_event(l, l->event);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
static asio::awaitable<void> server_command_announce_inner(const Args& a, bool mail, bool anonymous) {
|
|
a.check_is_proxy(false);
|
|
a.check_account_flag(Account::Flag::ANNOUNCE);
|
|
|
|
auto s = a.c->require_server_state();
|
|
if (anonymous) {
|
|
if (mail) {
|
|
send_simple_mail(s, 0, s->name, a.text);
|
|
} else {
|
|
send_text_or_scrolling_message(s, a.text, a.text);
|
|
}
|
|
} else {
|
|
auto from_name = a.c->character_file()->disp.name.decode(a.c->language());
|
|
if (mail) {
|
|
send_simple_mail(s, 0, from_name, a.text);
|
|
} else {
|
|
auto message = from_name + ": " + a.text;
|
|
send_text_or_scrolling_message(s, message, message);
|
|
}
|
|
}
|
|
co_return;
|
|
}
|
|
|
|
ChatCommandDefinition cc_ann_named(
|
|
{"$ann"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return server_command_announce_inner(a, false, false);
|
|
});
|
|
ChatCommandDefinition cc_ann_anonymous(
|
|
{"$ann?"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return server_command_announce_inner(a, false, true);
|
|
});
|
|
ChatCommandDefinition cc_ann_mail_named(
|
|
{"$ann!"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return server_command_announce_inner(a, true, false);
|
|
});
|
|
ChatCommandDefinition cc_ann_mail_anonymous(
|
|
{"$ann?!", "$ann!?"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return server_command_announce_inner(a, true, true);
|
|
});
|
|
|
|
ChatCommandDefinition cc_announce_rares(
|
|
{"$announcerares"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
|
|
a.c->login->account->toggle_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST);
|
|
a.c->login->account->save();
|
|
bool enabled = a.c->login->account->check_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST);
|
|
send_text_message_fmt(a.c, "$C6Rare announcements\n{} for your\nitems", enabled ? "disabled" : "enabled");
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_arrow(
|
|
{"$arrow"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.text == "red") {
|
|
a.c->lobby_arrow_color = 0x01;
|
|
} else if (a.text == "blue") {
|
|
a.c->lobby_arrow_color = 0x02;
|
|
} else if (a.text == "green") {
|
|
a.c->lobby_arrow_color = 0x03;
|
|
} else if (a.text == "yellow") {
|
|
a.c->lobby_arrow_color = 0x04;
|
|
} else if (a.text == "purple") {
|
|
a.c->lobby_arrow_color = 0x05;
|
|
} else if (a.text == "cyan") {
|
|
a.c->lobby_arrow_color = 0x06;
|
|
} else if (a.text == "orange") {
|
|
a.c->lobby_arrow_color = 0x07;
|
|
} else if (a.text == "pink") {
|
|
a.c->lobby_arrow_color = 0x08;
|
|
} else if (a.text == "white") {
|
|
a.c->lobby_arrow_color = 0x09;
|
|
} else if (a.text == "white2") {
|
|
a.c->lobby_arrow_color = 0x0A;
|
|
} else if (a.text == "white3") {
|
|
a.c->lobby_arrow_color = 0x0B;
|
|
} else if (a.text == "black") {
|
|
a.c->lobby_arrow_color = 0x0C;
|
|
} else {
|
|
a.c->lobby_arrow_color = stoull(a.text, nullptr, 0);
|
|
}
|
|
|
|
if (a.c->proxy_session) {
|
|
a.c->proxy_session->server_channel->send(0x89, a.c->lobby_arrow_color);
|
|
} else {
|
|
auto l = a.c->require_lobby();
|
|
if (!l->is_game()) {
|
|
send_arrow_update(l);
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_auction(
|
|
{"$auction"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_account_flag(Account::Flag::DEBUG);
|
|
|
|
G_InitiateCardAuction_Ep3_6xB5x42 cmd;
|
|
cmd.header.sender_client_id = a.c->lobby_client_id;
|
|
|
|
if (a.c->proxy_session) {
|
|
a.c->channel->send(0xC9, 0x00, &cmd, sizeof(cmd));
|
|
a.c->proxy_session->server_channel->send(0xC9, 0x00, &cmd, sizeof(cmd));
|
|
} else {
|
|
auto l = a.c->require_lobby();
|
|
if (l->is_game() && l->is_ep3()) {
|
|
send_command_t(l, 0xC9, 0x00, cmd);
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
static std::string name_for_client(std::shared_ptr<Client> c) {
|
|
auto player = c->character_file(false);
|
|
if (player.get()) {
|
|
return escape_player_name(player->disp.name.decode(player->inventory.language));
|
|
}
|
|
|
|
if (c->login) {
|
|
return std::format("SN:{}", c->login->account->account_id);
|
|
}
|
|
|
|
return "Player";
|
|
}
|
|
|
|
ChatCommandDefinition cc_ban(
|
|
{"$ban"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_account_flag(Account::Flag::BAN_USER);
|
|
|
|
auto s = a.c->require_server_state();
|
|
auto l = a.c->require_lobby();
|
|
|
|
size_t space_pos = a.text.find(' ');
|
|
if (space_pos == std::string::npos) {
|
|
throw precondition_failed("$C6Incorrect arguments");
|
|
}
|
|
|
|
std::string identifier = a.text.substr(space_pos + 1);
|
|
auto target = s->find_client(&identifier);
|
|
if (!target->login) {
|
|
// This should be impossible, but I'll bet it's not actually
|
|
throw precondition_failed("$C6Client not logged in");
|
|
}
|
|
|
|
if (target->login->account->check_flag(Account::Flag::BAN_USER)) {
|
|
throw precondition_failed("$C6You do not have\nsufficient privileges.");
|
|
}
|
|
if (a.c == target) {
|
|
// This shouldn't be possible because you need BAN_USER to get here, but the target can't have BAN_USER if we
|
|
// get here, so if a.c and target are the same, one of the preceding conditions must be false.
|
|
throw std::logic_error("client attempts to ban themself");
|
|
}
|
|
|
|
uint64_t usecs = stoull(a.text, nullptr, 0) * 1000000;
|
|
|
|
size_t unit_offset = 0;
|
|
for (; isdigit(a.text[unit_offset]); unit_offset++)
|
|
;
|
|
if (a.text[unit_offset] == 'm') {
|
|
usecs *= 60;
|
|
} else if (a.text[unit_offset] == 'h') {
|
|
usecs *= 60 * 60;
|
|
} else if (a.text[unit_offset] == 'd') {
|
|
usecs *= 60 * 60 * 24;
|
|
} else if (a.text[unit_offset] == 'w') {
|
|
usecs *= 60 * 60 * 24 * 7;
|
|
} else if (a.text[unit_offset] == 'M') {
|
|
usecs *= 60 * 60 * 24 * 30;
|
|
} else if (a.text[unit_offset] == 'y') {
|
|
usecs *= 60 * 60 * 24 * 365;
|
|
}
|
|
|
|
target->login->account->ban_end_time = phosg::now() + usecs;
|
|
target->login->account->save();
|
|
send_message_box(target, "$C6You have been banned.");
|
|
send_text_message_fmt(a.c, "$C6{} banned", name_for_client(target));
|
|
target->channel->disconnect();
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_bank(
|
|
{"$bank"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_version(Version::BB_V4);
|
|
if (a.c->check_flag(Client::Flag::AT_BANK_COUNTER)) {
|
|
throw std::runtime_error("cannot change banks while at the bank counter");
|
|
}
|
|
if (a.c->has_overlay()) {
|
|
throw std::runtime_error("cannot change banks while Battle or Challenge is in progress");
|
|
}
|
|
|
|
ssize_t new_char_index = a.text.empty() ? (a.c->bb_character_index + 1) : stol(a.text, nullptr, 0);
|
|
|
|
if (new_char_index <= 0) {
|
|
a.c->change_bank(-1);
|
|
send_text_message(a.c, "$C6Using shared bank");
|
|
|
|
} else if (new_char_index <= 127) {
|
|
a.c->change_bank(new_char_index - 1);
|
|
send_text_message_fmt(a.c, "$C6Using character {}'s bank", new_char_index);
|
|
|
|
} else {
|
|
throw precondition_failed("$C6Invalid bank number");
|
|
}
|
|
|
|
auto bank = a.c->bank_file();
|
|
bank->assign_ids(0x99000000 + (a.c->lobby_client_id << 20));
|
|
a.c->log.info_f("Assigned bank item IDs");
|
|
a.c->print_bank();
|
|
|
|
send_text_message_fmt(a.c, "{} items\n{} Meseta", bank->items.size(), bank->meseta);
|
|
co_return;
|
|
});
|
|
|
|
static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool is_bb_conversion) {
|
|
// TODO: We could support this in proxy sessions; we'd just have to handle the 61/30 correctly
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(false);
|
|
|
|
auto s = a.c->require_server_state();
|
|
auto l = a.c->require_lobby();
|
|
|
|
if (is_bb_conversion && is_ep3(a.c->version())) {
|
|
throw precondition_failed("$C6Episode 3 players\ncannot be converted\nto BB format");
|
|
}
|
|
|
|
std::shared_ptr<Account> dest_account;
|
|
std::shared_ptr<BBLicense> dest_bb_license;
|
|
size_t dest_character_index = 0;
|
|
if (is_bb_conversion) {
|
|
std::vector<std::string> tokens = phosg::split(a.text, ' ');
|
|
if (tokens.size() != 3) {
|
|
throw precondition_failed("$C6Incorrect argument\ncount");
|
|
}
|
|
|
|
// username/password are tokens[0] and [1]
|
|
dest_character_index = std::stoull(tokens[2]) - 1;
|
|
if (dest_character_index >= 127) {
|
|
throw precondition_failed("$C6Player index must\nbe in range 1-127");
|
|
}
|
|
|
|
try {
|
|
auto dest_login = s->account_index->from_bb_credentials(tokens[0], &tokens[1], false);
|
|
dest_account = dest_login->account;
|
|
dest_bb_license = dest_login->bb_license;
|
|
} catch (const std::exception& e) {
|
|
throw precondition_failed("$C6Login failed: {}", e.what());
|
|
}
|
|
|
|
} else {
|
|
dest_character_index = stoull(a.text) - 1;
|
|
if (dest_character_index >= s->num_backup_character_slots) {
|
|
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
|
|
}
|
|
dest_account = a.c->login->account;
|
|
}
|
|
|
|
// If the client isn't BB, request the player info. (If they are BB, the server already has it)
|
|
GetPlayerInfoResult ch;
|
|
if (a.c->version() == Version::BB_V4) {
|
|
ch.character = a.c->character_file();
|
|
ch.is_full_info = true;
|
|
} else {
|
|
ch = co_await send_get_player_info(a.c, true);
|
|
}
|
|
|
|
std::string filename = dest_bb_license
|
|
? 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()));
|
|
|
|
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)");
|
|
} catch (const std::exception& e) {
|
|
send_text_message_fmt(a.c, "$C6Character data could\nnot be saved:\n{}", e.what());
|
|
}
|
|
}
|
|
if (ch.character) {
|
|
try {
|
|
Client::save_character_file(filename, a.c->system_file(), ch.character);
|
|
send_text_message(a.c, "$C7Character data saved\n(full save file)");
|
|
} catch (const std::exception& e) {
|
|
send_text_message_fmt(a.c, "$C6Character data could\nnot be saved:\n{}", e.what());
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// Client sent 61; generate a BB-format player from the information we have and save that instead
|
|
if (ch.character) {
|
|
auto bb_player = PSOBBCharacterFile::create_from_config(
|
|
a.c->login->account->account_id,
|
|
a.c->language(),
|
|
ch.character->disp.visual,
|
|
ch.character->disp.name.decode(a.c->language()),
|
|
s->level_table(a.c->version()));
|
|
bb_player->disp.visual.version = 4;
|
|
bb_player->disp.visual.name_color_checksum = 0x00000000;
|
|
bb_player->inventory = ch.character->inventory;
|
|
// Before V3, player stats can't be correctly computed from other fields because material usage isn't stored
|
|
// anywhere. For these versions, we have to trust the stats field from the player's data.
|
|
auto level_table = s->level_table(a.c->version());
|
|
if (is_v1_or_v2(a.c->version())) {
|
|
bb_player->disp.stats = ch.character->disp.stats;
|
|
bb_player->import_tethealla_material_usage(level_table);
|
|
} else {
|
|
level_table->advance_to_level(
|
|
bb_player->disp.stats, ch.character->disp.stats.level, bb_player->disp.visual.char_class);
|
|
bb_player->disp.stats.char_stats.atp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::POWER) * 2;
|
|
bb_player->disp.stats.char_stats.mst += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::MIND) * 2;
|
|
bb_player->disp.stats.char_stats.evp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE) * 2;
|
|
bb_player->disp.stats.char_stats.dfp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::DEF) * 2;
|
|
bb_player->disp.stats.char_stats.lck += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK) * 2;
|
|
bb_player->disp.stats.char_stats.hp += bb_player->get_material_usage(PSOBBCharacterFile::MaterialType::HP) * 2;
|
|
bb_player->disp.stats.exp = ch.character->disp.stats.exp;
|
|
bb_player->disp.stats.meseta = ch.character->disp.stats.meseta;
|
|
}
|
|
bb_player->disp.technique_levels_v1 = ch.character->disp.technique_levels_v1;
|
|
bb_player->auto_reply = ch.character->auto_reply;
|
|
bb_player->info_board = ch.character->info_board;
|
|
bb_player->battle_records = ch.character->battle_records;
|
|
bb_player->challenge_records = ch.character->challenge_records;
|
|
bb_player->choice_search_config = ch.character->choice_search_config;
|
|
|
|
try {
|
|
Client::save_character_file(filename, a.c->system_file(), bb_player);
|
|
send_text_message(a.c, "$C7Character data saved\n(basic only)");
|
|
} catch (const std::exception& e) {
|
|
send_text_message_fmt(a.c, "$C6Character data could\nnot be saved:\n{}", e.what());
|
|
}
|
|
}
|
|
}
|
|
|
|
co_return;
|
|
}
|
|
|
|
ChatCommandDefinition cc_bbchar(
|
|
{"$bbchar"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return server_command_bbchar_savechar(a, true);
|
|
});
|
|
|
|
ChatCommandDefinition cc_cheat(
|
|
{"$cheat"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_leader();
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (a.check_permissions && l->check_flag(Lobby::Flag::CANNOT_CHANGE_CHEAT_MODE)) {
|
|
throw precondition_failed("$C6Cheat mode cannot\nbe changed on this\nserver");
|
|
} else {
|
|
l->toggle_flag(Lobby::Flag::CHEATS_ENABLED);
|
|
send_text_message_fmt(l, "Cheat mode {}", l->check_flag(Lobby::Flag::CHEATS_ENABLED) ? "enabled" : "disabled");
|
|
|
|
auto s = a.c->require_server_state();
|
|
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) &&
|
|
!a.c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE) &&
|
|
s->cheat_flags.insufficient_minimum_level) {
|
|
size_t default_min_level = s->default_min_level_for_game(a.c->version(), l->episode, l->difficulty);
|
|
if (l->min_level < default_min_level) {
|
|
l->min_level = default_min_level;
|
|
send_text_message_fmt(l, "$C6Minimum level set\nto {}", l->min_level + 1);
|
|
}
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_checkchar(
|
|
{"$checkchar"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.check_permissions && a.c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) {
|
|
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
|
|
}
|
|
|
|
auto s = a.c->require_server_state();
|
|
|
|
if (a.text.empty()) {
|
|
bool is_ep3 = ::is_ep3(a.c->version());
|
|
|
|
std::vector<bool> flags;
|
|
flags.emplace_back(false);
|
|
for (size_t z = 0; z < s->num_backup_character_slots; z++) {
|
|
std::string filename = a.c->backup_character_filename(a.c->login->account->account_id, z, is_ep3);
|
|
flags.emplace_back(std::filesystem::is_regular_file(filename));
|
|
}
|
|
std::string used_str = str_for_flag_ranges(flags);
|
|
flags.flip();
|
|
flags[0] = false;
|
|
std::string free_str = str_for_flag_ranges(flags);
|
|
send_text_message_fmt(a.c, "Used: {}\nFree: {}", used_str, free_str);
|
|
|
|
} else {
|
|
size_t index = stoull(a.text, nullptr, 0) - 1;
|
|
if (index >= s->num_backup_character_slots) {
|
|
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
|
|
}
|
|
|
|
try {
|
|
if (is_ep3(a.c->version())) {
|
|
std::string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, true);
|
|
auto ch = phosg::load_object_file<PSOGCEp3CharacterFile::Character>(filename);
|
|
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nCLv: on {}.{}, off {}.{}",
|
|
index + 1, ch.disp.visual.name.decode(),
|
|
name_for_section_id(ch.disp.visual.section_id), name_for_char_class(ch.disp.visual.char_class),
|
|
(ch.ep3_config.online_clv_exp / 100) + 1, ch.ep3_config.online_clv_exp % 100,
|
|
(ch.ep3_config.offline_clv_exp / 100) + 1, ch.ep3_config.offline_clv_exp % 100);
|
|
} else {
|
|
std::string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, false);
|
|
auto ch = PSOCHARFile::load_shared(filename, false).character_file;
|
|
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nLevel {}",
|
|
index + 1, ch->disp.name.decode(),
|
|
name_for_section_id(ch->disp.visual.section_id), name_for_char_class(ch->disp.visual.char_class),
|
|
ch->disp.stats.level + 1);
|
|
}
|
|
} catch (const phosg::cannot_open_file&) {
|
|
send_text_message_fmt(a.c, "No character in\nslot {}", index + 1);
|
|
}
|
|
}
|
|
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_debug(
|
|
{"$debug"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_account_flag(Account::Flag::DEBUG);
|
|
a.c->toggle_flag(Client::Flag::DEBUG_ENABLED);
|
|
send_text_message_fmt(a.c, "Debug {}",
|
|
(a.c->check_flag(Client::Flag::DEBUG_ENABLED) ? "enabled" : "disabled"));
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_deletechar(
|
|
{"$deletechar"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.check_permissions && a.c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) {
|
|
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
|
|
}
|
|
|
|
size_t index = stoull(a.text, nullptr, 0) - 1;
|
|
if (index > 15) {
|
|
throw precondition_failed("$C6Player index must\nbe in range 1-16");
|
|
}
|
|
|
|
std::string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, is_ep3(a.c->version()));
|
|
if (std::filesystem::is_regular_file(filename)) {
|
|
std::filesystem::remove(filename);
|
|
send_text_message_fmt(a.c, "Character in slot\n{} deleted", index + 1);
|
|
} else {
|
|
send_text_message_fmt(a.c, "No character exists\nin slot {}", index + 1);
|
|
}
|
|
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_dicerange(
|
|
{"$dicerange"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_ep3(true);
|
|
a.check_is_leader();
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (l->episode != Episode::EP3) {
|
|
throw std::logic_error("non-Ep3 client in Ep3 game");
|
|
}
|
|
if (!l->ep3_server) {
|
|
throw precondition_failed("$C6Episode 3 server\nis not initialized");
|
|
}
|
|
if (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION) {
|
|
throw precondition_failed("$C6Battle is already\nin progress");
|
|
}
|
|
if (l->tournament_match) {
|
|
throw precondition_failed("$C6Cannot override\ndice ranges in a\ntournament");
|
|
}
|
|
|
|
auto parse_dice_range = +[](const std::string& spec) -> uint8_t {
|
|
auto tokens = phosg::split(spec, '-');
|
|
if (tokens.size() == 1) {
|
|
uint8_t v = stoull(spec);
|
|
return (v << 4) | (v & 0x0F);
|
|
} else if (tokens.size() == 2) {
|
|
return (stoull(tokens[0]) << 4) | (stoull(tokens[1]) & 0x0F);
|
|
} else {
|
|
throw std::runtime_error("invalid dice spec format");
|
|
}
|
|
};
|
|
|
|
uint8_t def_dice_range = 0;
|
|
uint8_t atk_dice_range_2v1 = 0;
|
|
uint8_t def_dice_range_2v1 = 0;
|
|
for (const auto& spec : phosg::split(a.text, ' ')) {
|
|
auto tokens = phosg::split(spec, ':');
|
|
if (tokens.size() != 2) {
|
|
throw precondition_failed("$C6Invalid dice spec\nformat");
|
|
}
|
|
if (tokens[0] == "d") {
|
|
def_dice_range = parse_dice_range(tokens[1]);
|
|
} else if (tokens[0] == "1") {
|
|
atk_dice_range_2v1 = parse_dice_range(tokens[1]);
|
|
def_dice_range_2v1 = atk_dice_range_2v1;
|
|
} else if (tokens[0] == "a1") {
|
|
atk_dice_range_2v1 = parse_dice_range(tokens[1]);
|
|
} else if (tokens[0] == "d1") {
|
|
def_dice_range_2v1 = parse_dice_range(tokens[1]);
|
|
}
|
|
}
|
|
|
|
l->ep3_server->def_dice_value_range_override = def_dice_range;
|
|
l->ep3_server->atk_dice_value_range_2v1_override = atk_dice_range_2v1;
|
|
l->ep3_server->def_dice_value_range_2v1_override = def_dice_range_2v1;
|
|
|
|
if (!def_dice_range && !atk_dice_range_2v1 && !def_dice_range_2v1) {
|
|
send_text_message_fmt(l, "$C7Dice ranges reset\nto defaults");
|
|
} else {
|
|
send_text_message_fmt(l, "$C7Dice ranges changed:");
|
|
if (def_dice_range) {
|
|
send_text_message_fmt(l, "$C7DEF: $C6{}-{}",
|
|
static_cast<uint8_t>(def_dice_range >> 4), static_cast<uint8_t>(def_dice_range & 0x0F));
|
|
}
|
|
if (atk_dice_range_2v1) {
|
|
send_text_message_fmt(l, "$C7ATK (1p in 2v1): $C6{}-{}",
|
|
static_cast<uint8_t>(atk_dice_range_2v1 >> 4), static_cast<uint8_t>(atk_dice_range_2v1 & 0x0F));
|
|
}
|
|
if (def_dice_range_2v1) {
|
|
send_text_message_fmt(l, "$C7DEF (1p in 2v1): $C6{}-{}",
|
|
static_cast<uint8_t>(def_dice_range_2v1 >> 4), static_cast<uint8_t>(def_dice_range_2v1 & 0x0F));
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_dropmode(
|
|
{"$dropmode"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_game(true);
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.proxy_override_drops);
|
|
|
|
if (a.c->proxy_session) {
|
|
|
|
if (a.text.empty()) {
|
|
switch (a.c->proxy_session->drop_mode) {
|
|
case ProxyDropMode::DISABLED:
|
|
send_text_message(a.c, "Drop mode: disabled");
|
|
break;
|
|
case ProxyDropMode::PASSTHROUGH:
|
|
send_text_message(a.c, "Drop mode: default");
|
|
break;
|
|
case ProxyDropMode::INTERCEPT:
|
|
send_text_message(a.c, "Drop mode: proxy");
|
|
break;
|
|
}
|
|
|
|
} else {
|
|
ProxyDropMode new_mode;
|
|
if ((a.text == "none") || (a.text == "disabled")) {
|
|
new_mode = ProxyDropMode::DISABLED;
|
|
} else if ((a.text == "default") || (a.text == "passthrough")) {
|
|
new_mode = ProxyDropMode::PASSTHROUGH;
|
|
} else if ((a.text == "proxy") || (a.text == "intercept")) {
|
|
new_mode = ProxyDropMode::INTERCEPT;
|
|
} else {
|
|
throw precondition_failed("Invalid drop mode");
|
|
}
|
|
|
|
a.c->proxy_session->set_drop_mode(s, a.c->version(), a.c->override_random_seed, new_mode);
|
|
switch (a.c->proxy_session->drop_mode) {
|
|
case ProxyDropMode::DISABLED:
|
|
send_text_message(a.c->channel, "Item drops disabled");
|
|
break;
|
|
case ProxyDropMode::PASSTHROUGH:
|
|
send_text_message(a.c->channel, "Item drops changed\nto default mode");
|
|
break;
|
|
case ProxyDropMode::INTERCEPT:
|
|
send_text_message(a.c->channel, "Item drops changed\nto proxy mode");
|
|
break;
|
|
}
|
|
}
|
|
|
|
} else { // Not proxy session
|
|
auto l = a.c->require_lobby();
|
|
if (a.text.empty()) {
|
|
switch (l->drop_mode) {
|
|
case ServerDropMode::DISABLED:
|
|
send_text_message(a.c, "Drop mode: disabled");
|
|
break;
|
|
case ServerDropMode::CLIENT:
|
|
send_text_message(a.c, "Drop mode: client");
|
|
break;
|
|
case ServerDropMode::SERVER_SHARED:
|
|
send_text_message(a.c, "Drop mode: server\nshared");
|
|
break;
|
|
case ServerDropMode::SERVER_PRIVATE:
|
|
send_text_message(a.c, "Drop mode: server\nprivate");
|
|
break;
|
|
case ServerDropMode::SERVER_DUPLICATE:
|
|
send_text_message(a.c, "Drop mode: server\nduplicate");
|
|
break;
|
|
}
|
|
|
|
} else {
|
|
a.check_is_leader();
|
|
ServerDropMode new_mode;
|
|
if ((a.text == "none") || (a.text == "disabled")) {
|
|
new_mode = ServerDropMode::DISABLED;
|
|
} else if (a.text == "client") {
|
|
new_mode = ServerDropMode::CLIENT;
|
|
} else if ((a.text == "shared") || (a.text == "server")) {
|
|
new_mode = ServerDropMode::SERVER_SHARED;
|
|
} else if ((a.text == "private") || (a.text == "priv")) {
|
|
new_mode = ServerDropMode::SERVER_PRIVATE;
|
|
} else if ((a.text == "duplicate") || (a.text == "dup")) {
|
|
new_mode = ServerDropMode::SERVER_DUPLICATE;
|
|
} else {
|
|
throw precondition_failed("Invalid drop mode");
|
|
}
|
|
|
|
if (!(l->allowed_drop_modes & (1 << static_cast<size_t>(new_mode)))) {
|
|
throw precondition_failed("Drop mode not\nallowed");
|
|
}
|
|
|
|
l->drop_mode = new_mode;
|
|
switch (l->drop_mode) {
|
|
case ServerDropMode::DISABLED:
|
|
send_text_message(l, "Item drops disabled");
|
|
break;
|
|
case ServerDropMode::CLIENT:
|
|
send_text_message(l, "Item drops changed\nto client mode");
|
|
break;
|
|
case ServerDropMode::SERVER_SHARED:
|
|
send_text_message(l, "Item drops changed\nto server shared\nmode");
|
|
break;
|
|
case ServerDropMode::SERVER_PRIVATE:
|
|
send_text_message(l, "Item drops changed\nto server private\nmode");
|
|
break;
|
|
case ServerDropMode::SERVER_DUPLICATE:
|
|
send_text_message(l, "Item drops changed\nto server duplicate\nmode");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_edit(
|
|
{"$edit"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(false);
|
|
|
|
auto s = a.c->require_server_state();
|
|
auto l = a.c->require_lobby();
|
|
if (!is_v1_or_v2(a.c->version()) && (a.c->version() != Version::BB_V4)) {
|
|
throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO.");
|
|
}
|
|
|
|
bool cheats_allowed = (!a.check_permissions ||
|
|
(s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) ||
|
|
a.c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
|
|
|
std::string encoded_args = phosg::tolower(a.text);
|
|
std::vector<std::string> tokens = phosg::split(encoded_args, ' ');
|
|
|
|
using MatType = PSOBBCharacterFile::MaterialType;
|
|
|
|
try {
|
|
auto p = a.c->character_file();
|
|
if (tokens.at(0) == "atp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.char_stats.atp = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "mst" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.char_stats.mst = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "evp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.char_stats.evp = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "hp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.char_stats.hp = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "dfp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.char_stats.dfp = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "ata" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.char_stats.ata = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "lck" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.char_stats.lck = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "meseta" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.meseta = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "exp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.exp = std::stoul(tokens.at(1));
|
|
} else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
p->disp.stats.level = std::stoul(tokens.at(1)) - 1;
|
|
p->recompute_stats(s->level_table(a.c->version()), true);
|
|
} else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(a.c->version()) && (cheats_allowed || !s->cheat_flags.reset_materials)) {
|
|
if (tokens.at(1) == "reset") {
|
|
const auto& which = tokens.at(2);
|
|
if (which == "power") {
|
|
p->set_material_usage(MatType::POWER, 0);
|
|
} else if (which == "mind") {
|
|
p->set_material_usage(MatType::MIND, 0);
|
|
} else if (which == "evade") {
|
|
p->set_material_usage(MatType::EVADE, 0);
|
|
} else if (which == "def") {
|
|
p->set_material_usage(MatType::DEF, 0);
|
|
} else if (which == "luck") {
|
|
p->set_material_usage(MatType::LUCK, 0);
|
|
} else if (which == "hp") {
|
|
p->set_material_usage(MatType::HP, 0);
|
|
} else if (which == "tp") {
|
|
p->set_material_usage(MatType::TP, 0);
|
|
} else if (which == "all") {
|
|
p->set_material_usage(MatType::POWER, 0);
|
|
p->set_material_usage(MatType::MIND, 0);
|
|
p->set_material_usage(MatType::EVADE, 0);
|
|
p->set_material_usage(MatType::DEF, 0);
|
|
p->set_material_usage(MatType::LUCK, 0);
|
|
} else if (which == "every") {
|
|
p->set_material_usage(MatType::POWER, 0);
|
|
p->set_material_usage(MatType::MIND, 0);
|
|
p->set_material_usage(MatType::EVADE, 0);
|
|
p->set_material_usage(MatType::DEF, 0);
|
|
p->set_material_usage(MatType::LUCK, 0);
|
|
p->set_material_usage(MatType::HP, 0);
|
|
p->set_material_usage(MatType::TP, 0);
|
|
} else {
|
|
throw precondition_failed("$C6Invalid subcommand");
|
|
}
|
|
} else {
|
|
throw precondition_failed("$C6Invalid subcommand");
|
|
}
|
|
p->recompute_stats(s->level_table(a.c->version()), false);
|
|
} else if (tokens.at(0) == "namecolor") {
|
|
p->disp.visual.name_color = std::stoul(tokens.at(1), nullptr, 16);
|
|
} else if (tokens.at(0) == "language" || tokens.at(0) == "lang") {
|
|
if (tokens.at(1).size() != 1) {
|
|
throw std::runtime_error("invalid language");
|
|
}
|
|
Language new_language = language_for_char(tokens.at(1).at(0));
|
|
a.c->channel->language = new_language;
|
|
p->inventory.language = new_language;
|
|
p->guild_card.language = new_language;
|
|
auto sys = a.c->system_file(false);
|
|
if (sys) {
|
|
sys->language = new_language;
|
|
}
|
|
} else if (tokens.at(0) == "secid") {
|
|
if (!cheats_allowed && (p->disp.stats.level > 0) && s->cheat_flags.edit_section_id) {
|
|
throw precondition_failed("$C6You cannot change\nyour Section ID\nafter level 1");
|
|
}
|
|
uint8_t secid = section_id_for_name(tokens.at(1));
|
|
if (secid == 0xFF) {
|
|
throw precondition_failed("$C6No such section ID");
|
|
} else {
|
|
p->disp.visual.section_id = secid;
|
|
}
|
|
} else if (tokens.at(0) == "name") {
|
|
std::vector<std::string> orig_tokens = phosg::split(a.text, ' ', 1);
|
|
p->disp.name.encode(orig_tokens.at(1), p->inventory.language);
|
|
} else if (tokens.at(0) == "npc") {
|
|
if (tokens.at(1) == "none") {
|
|
p->disp.visual.extra_model = 0;
|
|
p->disp.visual.validation_flags &= 0xFD;
|
|
p->disp.visual.restore_npc_saved_fields();
|
|
} else {
|
|
uint8_t npc = npc_for_name(tokens.at(1), a.c->version());
|
|
if (npc == 0xFF) {
|
|
throw precondition_failed("$C6No such NPC");
|
|
}
|
|
p->disp.visual.backup_npc_saved_fields();
|
|
p->disp.visual.extra_model = npc;
|
|
p->disp.visual.validation_flags |= 0x02;
|
|
}
|
|
} else if (tokens.at(0) == "tech" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
|
uint8_t level = std::stoul(tokens.at(2)) - 1;
|
|
if (tokens.at(1) == "all") {
|
|
for (size_t x = 0; x < 0x14; x++) {
|
|
p->set_technique_level(x, level);
|
|
}
|
|
} else {
|
|
uint8_t tech_id = technique_for_name(tokens.at(1));
|
|
if (tech_id == 0xFF) {
|
|
throw precondition_failed("$C6No such technique");
|
|
}
|
|
try {
|
|
p->set_technique_level(tech_id, level);
|
|
} catch (const std::out_of_range&) {
|
|
throw precondition_failed("$C6Invalid technique");
|
|
}
|
|
}
|
|
} else {
|
|
throw precondition_failed("$C6Unknown field");
|
|
}
|
|
} catch (const std::out_of_range&) {
|
|
throw precondition_failed("$C6Not enough arguments");
|
|
}
|
|
|
|
// Reload the client in the lobby
|
|
send_player_leave_notification(l, a.c->lobby_client_id);
|
|
if (a.c->version() == Version::BB_V4) {
|
|
send_complete_player_bb(a.c);
|
|
}
|
|
a.c->v1_v2_last_reported_disp.reset();
|
|
s->send_lobby_join_notifications(l, a.c);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_event(
|
|
{"$event"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.c->proxy_session) {
|
|
if (a.text.empty()) {
|
|
a.c->override_lobby_event = 0xFF;
|
|
} else {
|
|
uint8_t new_event = event_for_name(a.text);
|
|
if (new_event == 0xFF) {
|
|
throw precondition_failed("$C6No such lobby event");
|
|
} else {
|
|
a.c->override_lobby_event = new_event;
|
|
if (!is_v1_or_v2(a.c->version())) {
|
|
a.c->channel->send(0xDA, a.c->override_lobby_event);
|
|
}
|
|
}
|
|
}
|
|
|
|
} else { // Not proxy session
|
|
a.check_is_game(false);
|
|
a.check_account_flag(Account::Flag::CHANGE_EVENT);
|
|
|
|
auto l = a.c->require_lobby();
|
|
|
|
uint8_t new_event = event_for_name(a.text);
|
|
if (new_event == 0xFF) {
|
|
throw precondition_failed("$C6No such lobby event");
|
|
}
|
|
|
|
l->event = new_event;
|
|
send_change_event(l, l->event);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_exit(
|
|
{"$exit"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (!(a.c->proxy_session ? a.c->proxy_session->is_in_game : a.c->require_lobby()->is_game())) {
|
|
// Client is in the lobby; send them to the login server (main menu)
|
|
if (a.c->proxy_session) {
|
|
if (is_v4(a.c->version())) {
|
|
throw precondition_failed("$C6You cannot exit proxy\nsessions on BB");
|
|
}
|
|
a.c->proxy_session->server_channel->disconnect();
|
|
} else {
|
|
send_self_leave_notification(a.c);
|
|
if (!a.c->check_flag(Client::Flag::NO_D6)) {
|
|
send_message_box(a.c, "");
|
|
}
|
|
co_await start_login_server_procedure(a.c);
|
|
}
|
|
co_return;
|
|
}
|
|
|
|
if (is_ep3(a.c->version())) {
|
|
// Client is on Ep3; command ED triggers game exit
|
|
a.c->channel->send(0xED, 0x00);
|
|
co_return;
|
|
}
|
|
|
|
bool is_in_quest;
|
|
if (a.c->proxy_session) {
|
|
is_in_quest = a.c->proxy_session->is_in_quest;
|
|
} else {
|
|
auto l = a.c->require_lobby();
|
|
is_in_quest = (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
|
|
}
|
|
if (is_in_quest) {
|
|
// Client is in a quest; command 6x73 triggers game exit
|
|
G_UnusedHeader cmd = {0x73, 0x01, 0x0000};
|
|
a.c->channel->send(0x60, 0x00, cmd);
|
|
co_return;
|
|
}
|
|
|
|
if (a.c->check_flag(Client::Flag::HAS_SEND_FUNCTION_CALL) &&
|
|
a.c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE)) {
|
|
co_await prepare_client_for_patches(a.c);
|
|
auto s = a.c->require_server_state();
|
|
std::shared_ptr<const ClientFunctionIndex::Function> fn;
|
|
try {
|
|
fn = s->client_functions->get("ExitAnywhere", a.c->specific_version);
|
|
} catch (const std::out_of_range&) {
|
|
}
|
|
if (fn) {
|
|
co_await send_function_call(a.c, fn);
|
|
co_return;
|
|
}
|
|
}
|
|
throw precondition_failed("$C6You must return to\nthe lobby first");
|
|
});
|
|
|
|
ChatCommandDefinition cc_gc(
|
|
{"$gc"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.c->proxy_session) {
|
|
bool any_card_sent = false;
|
|
for (const auto& p : a.c->proxy_session->lobby_players) {
|
|
if (!p.name.empty() && a.text == p.name) {
|
|
send_guild_card(a.c->channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class);
|
|
any_card_sent = true;
|
|
}
|
|
}
|
|
|
|
if (!any_card_sent) {
|
|
size_t index = stoull(a.text, nullptr, 0);
|
|
const auto& p = a.c->proxy_session->lobby_players.at(index);
|
|
if (!p.name.empty()) {
|
|
send_guild_card(a.c->channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class);
|
|
}
|
|
}
|
|
} else {
|
|
send_guild_card(a.c, a.c);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_infhp(
|
|
{"$infhp"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (!a.c->proxy_session) {
|
|
a.check_is_game(true);
|
|
}
|
|
|
|
if (a.c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
|
a.c->clear_flag(Client::Flag::INFINITE_HP_ENABLED);
|
|
send_text_message(a.c, "$C6Infinite HP disabled");
|
|
} else {
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.infinite_hp_tp);
|
|
a.c->set_flag(Client::Flag::INFINITE_HP_ENABLED);
|
|
co_await send_remove_negative_conditions(a.c);
|
|
if (a.c->proxy_session) {
|
|
send_remove_negative_conditions(a.c->proxy_session->server_channel, a.c->lobby_client_id);
|
|
}
|
|
send_text_message(a.c, "$C6Infinite HP enabled");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_inftime(
|
|
{"$inftime"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_ep3(true);
|
|
a.check_is_leader();
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (l->episode != Episode::EP3) {
|
|
throw std::logic_error("non-Ep3 client in Ep3 game");
|
|
}
|
|
if (!l->ep3_server) {
|
|
throw precondition_failed("$C6Episode 3 server\nis not initialized");
|
|
}
|
|
if (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION) {
|
|
throw precondition_failed("$C6Battle is already\nin progress");
|
|
}
|
|
|
|
l->ep3_server->options.behavior_flags ^= Episode3::BehaviorFlag::DISABLE_TIME_LIMITS;
|
|
bool infinite_time_enabled = (l->ep3_server->options.behavior_flags & Episode3::BehaviorFlag::DISABLE_TIME_LIMITS);
|
|
send_text_message(l, infinite_time_enabled ? "$C6Infinite time enabled" : "$C6Infinite time disabled");
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_inftp(
|
|
{"$inftp"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (!a.c->proxy_session) {
|
|
a.check_is_game(true);
|
|
}
|
|
|
|
if (a.c->check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
|
|
a.c->clear_flag(Client::Flag::INFINITE_TP_ENABLED);
|
|
send_text_message(a.c, "$C6Infinite TP disabled");
|
|
} else {
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.infinite_hp_tp);
|
|
a.c->set_flag(Client::Flag::INFINITE_TP_ENABLED);
|
|
send_text_message(a.c, "$C6Infinite TP enabled");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_item(
|
|
{"$item", "$i"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_game(true);
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.create_items);
|
|
|
|
ItemData item;
|
|
bool was_enqueued = false;
|
|
if (a.c->proxy_session) {
|
|
if (a.c->version() == Version::BB_V4) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in proxy\nsessions in BB games");
|
|
}
|
|
a.check_is_leader();
|
|
|
|
if (a.text.starts_with("!")) {
|
|
item = s->parse_item_description(a.c->version(), a.text.substr(1));
|
|
a.c->proxy_session->next_drop_item = item;
|
|
was_enqueued = true;
|
|
|
|
} else {
|
|
item = s->parse_item_description(a.c->version(), a.text);
|
|
item.id = phosg::random_object<uint32_t>() | 0x80000000;
|
|
|
|
send_drop_stacked_item_to_channel(s, a.c->channel, item, a.c->floor, a.c->pos);
|
|
send_drop_stacked_item_to_channel(s, a.c->proxy_session->server_channel, item, a.c->floor, a.c->pos);
|
|
}
|
|
|
|
} else {
|
|
auto l = a.c->require_lobby();
|
|
item = s->parse_item_description(a.c->version(), a.text);
|
|
item.id = l->generate_item_id(a.c->lobby_client_id);
|
|
|
|
if ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE)) {
|
|
l->add_item(a.c->floor, item, a.c->pos, nullptr, nullptr, (1 << a.c->lobby_client_id));
|
|
send_drop_stacked_item_to_channel(s, a.c->channel, item, a.c->floor, a.c->pos);
|
|
} else {
|
|
l->add_item(a.c->floor, item, a.c->pos, nullptr, nullptr, 0x00F);
|
|
send_drop_stacked_item_to_lobby(l, item, a.c->floor, a.c->pos);
|
|
}
|
|
}
|
|
|
|
std::string name = s->describe_item(a.c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES);
|
|
if (was_enqueued) {
|
|
send_text_message(a.c, "$C7Next item:\n" + name);
|
|
} else {
|
|
send_text_message(a.c, "$C7Item created:\n" + name);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_itemnotifs(
|
|
{"$itemnotifs"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.text == "every" || a.text == "everything") {
|
|
a.c->set_drop_notification_mode(Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA);
|
|
send_text_message_fmt(a.c, "$C6Notifications enabled\nfor all items and\nMeseta");
|
|
} else if (a.text == "all" || a.text == "on") {
|
|
a.c->set_drop_notification_mode(Client::ItemDropNotificationMode::ALL_ITEMS);
|
|
send_text_message_fmt(a.c, "$C6Notifications enabled\nfor all items");
|
|
} else if (a.text == "rare" || a.text == "rares") {
|
|
a.c->set_drop_notification_mode(Client::ItemDropNotificationMode::RARES_ONLY);
|
|
send_text_message_fmt(a.c, "$C6Notifications enabled\nfor rare items only");
|
|
} else if (a.text == "none" || a.text == "off") {
|
|
a.c->set_drop_notification_mode(Client::ItemDropNotificationMode::NOTHING);
|
|
send_text_message_fmt(a.c, "$C6Notifications disabled\nfor all items");
|
|
} else {
|
|
throw precondition_failed("$C6You must specify\n$C6off$C7, $C6rare$C7, $C6on$C7, or\n$C6everything$C7");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_kick(
|
|
{"$kick"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_account_flag(Account::Flag::KICK_USER);
|
|
|
|
auto s = a.c->require_server_state();
|
|
auto target = s->find_client(&a.text);
|
|
if (!target->login) {
|
|
// This should be impossible, but I'll bet it's not actually
|
|
throw precondition_failed("$C6Client not logged in");
|
|
}
|
|
|
|
if (target->login->account->check_flag(Account::Flag::KICK_USER)) {
|
|
throw precondition_failed("$C6You do not have\nsufficient privileges.");
|
|
}
|
|
if (a.c == target) {
|
|
// This shouldn't be possible because you need KICK_USER to get here, but the target can't have KICK_USER if we
|
|
// get here, so if a.c and target are the same, one of the preceding conditions must be false.
|
|
throw std::logic_error("client attempts to kick themself off");
|
|
}
|
|
|
|
send_message_box(target, "$C6You have been kicked off the server.");
|
|
send_text_message_fmt(a.c, "$C6{} kicked off", name_for_client(target));
|
|
target->channel->disconnect();
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_killcount(
|
|
{"$killcount"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
|
|
auto p = a.c->character_file();
|
|
std::vector<size_t> item_indexes;
|
|
for (size_t z = 0; z < p->inventory.num_items; z++) {
|
|
const auto& item = p->inventory.items[z];
|
|
if (item.is_equipped() && item.data.has_kill_count()) {
|
|
item_indexes.emplace_back(z);
|
|
}
|
|
}
|
|
|
|
if (item_indexes.empty()) {
|
|
throw precondition_failed("No equipped items\nhave kill counts");
|
|
|
|
} else {
|
|
// Kill counts are only accurate on the server side at all times on BB. On other versions, we update the
|
|
// server's view of the client's inventory during games, but we can't track kills because the client doesn't
|
|
// inform the server whether it counted a kill for any individual enemy. So, on non-BB versions, the kill count
|
|
// is accurate at all times in the lobby (since kills can't occur there), or at the beginning of a game.
|
|
if ((a.c->version() == Version::BB_V4) || !a.c->require_lobby()->is_game()) {
|
|
send_text_message(a.c, "As of now:");
|
|
} else {
|
|
send_text_message(a.c, "As of game join:");
|
|
}
|
|
|
|
auto s = a.c->require_server_state();
|
|
for (size_t z : item_indexes) {
|
|
const auto& item = p->inventory.items[z];
|
|
std::string name = s->describe_item(
|
|
a.c->version(), item.data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES | ItemNameIndex::Flag::NAME_ONLY);
|
|
send_text_message_fmt(a.c, "{}$C7: {} kills", name, item.data.get_kill_count());
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_lobby_info(
|
|
{"$li"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.c->proxy_session) {
|
|
std::string msg;
|
|
// On non-masked-GC sessions (BB), there is no remote Guild Card number, so we don't show it. (The user can see
|
|
// it in the pause menu, unlike in masked-GC sessions like GC.)
|
|
if (a.c->proxy_session->remote_guild_card_number >= 0) {
|
|
msg = std::format("$C7GC: $C6{}$C7\n", a.c->proxy_session->remote_guild_card_number);
|
|
}
|
|
msg += "Slots: ";
|
|
|
|
for (size_t z = 0; z < a.c->proxy_session->lobby_players.size(); z++) {
|
|
bool is_self = (z == a.c->lobby_client_id);
|
|
bool is_leader = (z == a.c->proxy_session->leader_client_id);
|
|
if (a.c->proxy_session->lobby_players[z].guild_card_number == 0) {
|
|
msg += std::format("$C0{:X}$C7", z);
|
|
} else if (is_self && is_leader) {
|
|
msg += std::format("$C6{:X}$C7", z);
|
|
} else if (is_self) {
|
|
msg += std::format("$C2{:X}$C7", z);
|
|
} else if (is_leader) {
|
|
msg += std::format("$C4{:X}$C7", z);
|
|
} else {
|
|
msg += std::format("{:X}", z);
|
|
}
|
|
}
|
|
|
|
std::vector<const char*> cheats_tokens;
|
|
if (a.c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
|
cheats_tokens.emplace_back("HP");
|
|
}
|
|
if (a.c->check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
|
|
cheats_tokens.emplace_back("TP");
|
|
}
|
|
if (!cheats_tokens.empty()) {
|
|
msg += "\n$C7Cheats: $C6";
|
|
msg += phosg::join(cheats_tokens, ",");
|
|
}
|
|
|
|
std::vector<const char*> behaviors_tokens;
|
|
if (a.c->check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
|
|
behaviors_tokens.emplace_back("SWA");
|
|
}
|
|
if (a.c->check_flag(Client::Flag::PROXY_SAVE_FILES)) {
|
|
behaviors_tokens.emplace_back("SF");
|
|
}
|
|
if (a.c->check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS)) {
|
|
behaviors_tokens.emplace_back("BF");
|
|
}
|
|
if (!behaviors_tokens.empty()) {
|
|
msg += "\n$C7Flags: $C6";
|
|
msg += phosg::join(behaviors_tokens, ",");
|
|
}
|
|
|
|
if (a.c->override_section_id != 0xFF) {
|
|
msg += "\n$C7SecID*: $C6";
|
|
msg += name_for_section_id(a.c->override_section_id);
|
|
}
|
|
|
|
send_text_message(a.c->channel, msg);
|
|
|
|
} else { // Not proxy session
|
|
std::vector<std::string> lines;
|
|
|
|
auto l = a.c->lobby.lock();
|
|
if (!l) {
|
|
lines.emplace_back("$C4No lobby info");
|
|
|
|
} else {
|
|
if (l->is_game()) {
|
|
if (!l->is_ep3()) {
|
|
if (l->max_level == 0xFFFFFFFF) {
|
|
lines.emplace_back(std::format("$C6{:08X}$C7 L$C6{}+$C7", l->lobby_id, l->min_level + 1));
|
|
} else {
|
|
lines.emplace_back(std::format("$C6{:08X}$C7 L$C6{}-{}$C7", l->lobby_id, l->min_level + 1, l->max_level + 1));
|
|
}
|
|
uint8_t effective_section_id = l->effective_section_id();
|
|
if (effective_section_id < 10) {
|
|
lines.emplace_back(std::format("$C7Section ID: $C6{}$C7", name_for_section_id(effective_section_id)));
|
|
}
|
|
|
|
switch (l->drop_mode) {
|
|
case ServerDropMode::DISABLED:
|
|
lines.emplace_back("Drops disabled");
|
|
break;
|
|
case ServerDropMode::CLIENT:
|
|
lines.emplace_back("Client item table");
|
|
break;
|
|
case ServerDropMode::SERVER_SHARED:
|
|
lines.emplace_back("Server item table");
|
|
break;
|
|
case ServerDropMode::SERVER_PRIVATE:
|
|
lines.emplace_back("Server indiv items");
|
|
break;
|
|
case ServerDropMode::SERVER_DUPLICATE:
|
|
lines.emplace_back("Server dup items");
|
|
break;
|
|
default:
|
|
lines.emplace_back("$C4Unknown drop mode$C7");
|
|
}
|
|
if (l->check_flag(Lobby::Flag::CHEATS_ENABLED)) {
|
|
lines.emplace_back("Cheats enabled");
|
|
}
|
|
|
|
} else {
|
|
char type_ch = dynamic_pointer_cast<DisabledRandomGenerator>(l->rand_crypt).get()
|
|
? 'X'
|
|
: dynamic_pointer_cast<MT19937Generator>(l->rand_crypt).get()
|
|
? 'M'
|
|
: dynamic_pointer_cast<PSOV2Encryption>(l->rand_crypt).get()
|
|
? 'L'
|
|
: '?';
|
|
lines.emplace_back(std::format("$C7State seed: $C6{:08X}/{:c}$C7", l->random_seed, type_ch));
|
|
}
|
|
|
|
} else {
|
|
lines.emplace_back(std::format("$C7Lobby ID: $C6{:08X}$C7", l->lobby_id));
|
|
}
|
|
|
|
std::string slots_str = "Slots: ";
|
|
for (size_t z = 0; z < l->clients.size(); z++) {
|
|
if (!l->clients[z]) {
|
|
slots_str += std::format("$C0{:X}$C7", z);
|
|
} else {
|
|
bool is_self = (l->clients[z] == a.c);
|
|
bool is_leader = (z == l->leader_id);
|
|
if (is_self && is_leader) {
|
|
slots_str += std::format("$C6{:X}$C7", z);
|
|
} else if (is_self) {
|
|
slots_str += std::format("$C2{:X}$C7", z);
|
|
} else if (is_leader) {
|
|
slots_str += std::format("$C4{:X}$C7", z);
|
|
} else {
|
|
slots_str += std::format("{:X}", z);
|
|
}
|
|
}
|
|
}
|
|
lines.emplace_back(std::move(slots_str));
|
|
}
|
|
|
|
send_text_message(a.c, phosg::join(lines, "\n"));
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_ln(
|
|
{"$ln"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
uint8_t new_type;
|
|
if (a.text.empty()) {
|
|
new_type = 0x80;
|
|
} else {
|
|
new_type = lobby_type_for_name(a.text);
|
|
if (new_type == 0x80) {
|
|
throw precondition_failed("$C6No such lobby type");
|
|
}
|
|
}
|
|
|
|
if (a.c->proxy_session) {
|
|
a.c->override_lobby_number = new_type;
|
|
} else {
|
|
a.check_is_game(false);
|
|
a.c->override_lobby_number = new_type;
|
|
send_join_lobby(a.c, a.c->require_lobby());
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_loadchar(
|
|
{"$loadchar"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(false);
|
|
if (a.check_permissions && a.c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) {
|
|
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
|
|
}
|
|
|
|
auto s = a.c->require_server_state();
|
|
auto l = a.c->require_lobby();
|
|
|
|
size_t index = stoull(a.text, nullptr, 0) - 1;
|
|
if (index >= s->num_backup_character_slots) {
|
|
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
|
|
}
|
|
|
|
std::shared_ptr<PSOGCEp3CharacterFile::Character> ep3_char;
|
|
if (is_ep3(a.c->version())) {
|
|
ep3_char = a.c->load_ep3_backup_character(a.c->login->account->account_id, index);
|
|
} else {
|
|
a.c->load_backup_character(a.c->login->account->account_id, index);
|
|
}
|
|
|
|
if (a.c->version() == Version::BB_V4) {
|
|
// On BB, it suffices to simply send the character file again
|
|
send_complete_player_bb(a.c);
|
|
send_player_leave_notification(l, a.c->lobby_client_id);
|
|
s->send_lobby_join_notifications(l, a.c);
|
|
|
|
} else if ((a.c->version() == Version::DC_V2) ||
|
|
(a.c->version() == Version::GC_NTE) ||
|
|
(a.c->version() == Version::GC_V3) ||
|
|
(a.c->version() == Version::GC_EP3_NTE) ||
|
|
(a.c->version() == Version::GC_EP3) ||
|
|
(a.c->version() == Version::XB_V3)) {
|
|
// TODO: Support extended player info on other versions
|
|
auto s = a.c->require_server_state();
|
|
if (!a.c->check_flag(Client::Flag::HAS_SEND_FUNCTION_CALL) ||
|
|
!a.c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE)) {
|
|
throw precondition_failed("Can\'t load character\ndata on this game\nversion");
|
|
}
|
|
|
|
auto send_set_extended_player_info = [&a, &s]<typename CharT>(const CharT& char_file) -> asio::awaitable<void> {
|
|
co_await prepare_client_for_patches(a.c);
|
|
try {
|
|
auto fn = s->client_functions->get("SetExtendedPlayerInfo", a.c->specific_version);
|
|
co_await send_function_call(a.c, fn, {}, &char_file, sizeof(CharT));
|
|
auto l = a.c->lobby.lock();
|
|
if (l) {
|
|
send_player_leave_notification(l, a.c->lobby_client_id);
|
|
s->send_lobby_join_notifications(l, a.c);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
a.c->log.warning_f("Failed to set extended player info: {}", e.what());
|
|
throw precondition_failed("Failed to set\nplayer info:\n{}", e.what());
|
|
}
|
|
};
|
|
|
|
if (a.c->version() == Version::DC_V2) {
|
|
PSODCV2CharacterFile::Character dc_char = *a.c->character_file();
|
|
co_await send_set_extended_player_info(dc_char);
|
|
} else if (a.c->version() == Version::GC_NTE) {
|
|
PSOGCNTECharacterFileCharacter gc_char = *a.c->character_file();
|
|
co_await send_set_extended_player_info(gc_char);
|
|
} else if (a.c->version() == Version::GC_V3) {
|
|
PSOGCCharacterFile::Character gc_char = *a.c->character_file();
|
|
co_await send_set_extended_player_info(gc_char);
|
|
} else if (a.c->version() == Version::GC_EP3_NTE) {
|
|
PSOGCEp3NTECharacter nte_char = *ep3_char;
|
|
co_await send_set_extended_player_info(nte_char);
|
|
} else if (a.c->version() == Version::GC_EP3) {
|
|
co_await send_set_extended_player_info(*ep3_char);
|
|
} else if (a.c->version() == Version::XB_V3) {
|
|
if (!a.c->login || !a.c->login->xb_license) {
|
|
throw std::runtime_error("XB client is not logged in");
|
|
}
|
|
PSOXBCharacterFile::Character xb_char = *a.c->character_file();
|
|
xb_char.guild_card.xb_user_id_high = (a.c->login->xb_license->user_id >> 32) & 0xFFFFFFFF;
|
|
xb_char.guild_card.xb_user_id_low = a.c->login->xb_license->user_id & 0xFFFFFFFF;
|
|
co_await send_set_extended_player_info(xb_char);
|
|
} else {
|
|
throw std::logic_error("unimplemented extended player info version");
|
|
}
|
|
|
|
} else {
|
|
// On v1 and v2, the client will assign its character data from the lobby join command, so it suffices to just
|
|
// resend the join notification.
|
|
auto s = a.c->require_server_state();
|
|
send_player_leave_notification(l, a.c->lobby_client_id);
|
|
s->send_lobby_join_notifications(l, a.c);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_makeobj(
|
|
{"$makeobj"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_debug_enabled();
|
|
|
|
auto tokens = phosg::split(a.text, ' ');
|
|
if (tokens.size() < 1) {
|
|
throw std::runtime_error("not enough arguments");
|
|
}
|
|
|
|
uint32_t base_type_high = std::stoul(tokens[0], nullptr, 0) << 16;
|
|
VectorXYZF pos = a.c->pos;
|
|
VectorXYZI angle{0, 0, 0};
|
|
VectorXYZF param123{0, 0, 0};
|
|
VectorXYZI param456{0, 0, 0};
|
|
for (size_t z = 1; z < tokens.size(); z++) {
|
|
auto subtokens = phosg::split(tokens[z], ':');
|
|
if (subtokens.size() != 2 || subtokens[0].size() != 1) {
|
|
throw std::runtime_error("invalid argument: " + tokens[z]);
|
|
}
|
|
switch (tolower(subtokens[0].front())) {
|
|
case 'X':
|
|
case 'x':
|
|
pos.x = stof(subtokens[1]);
|
|
break;
|
|
case 'Y':
|
|
case 'y':
|
|
pos.y = stof(subtokens[1]);
|
|
break;
|
|
case 'Z':
|
|
case 'z':
|
|
pos.z = stof(subtokens[1]);
|
|
break;
|
|
case 'R':
|
|
case 'r':
|
|
angle.x = stol(subtokens[1], nullptr, 0);
|
|
break;
|
|
case 'P':
|
|
case 'p':
|
|
angle.y = stol(subtokens[1], nullptr, 0);
|
|
break;
|
|
case 'W':
|
|
case 'w':
|
|
angle.z = stol(subtokens[1], nullptr, 0);
|
|
break;
|
|
case '1':
|
|
param123.x = stof(subtokens[1]);
|
|
break;
|
|
case '2':
|
|
param123.y = stof(subtokens[1]);
|
|
break;
|
|
case '3':
|
|
param123.z = stof(subtokens[1]);
|
|
break;
|
|
case '4':
|
|
param456.x = stol(subtokens[1], nullptr, 0);
|
|
break;
|
|
case '5':
|
|
param456.y = stol(subtokens[1], nullptr, 0);
|
|
break;
|
|
case '6':
|
|
param456.z = stol(subtokens[1], nullptr, 0);
|
|
break;
|
|
default:
|
|
throw std::runtime_error("invalid argument: " + tokens[z]);
|
|
}
|
|
}
|
|
|
|
std::unordered_map<std::string, uint32_t> label_writes{
|
|
{"base_type_high", base_type_high},
|
|
{"floor_low", a.c->floor},
|
|
{"pos_x", std::bit_cast<uint32_t>(pos.x.load())},
|
|
{"pos_y", std::bit_cast<uint32_t>(pos.y.load())},
|
|
{"pos_z", std::bit_cast<uint32_t>(pos.z.load())},
|
|
{"angle_x", angle.x},
|
|
{"angle_y", angle.y},
|
|
{"angle_z", angle.z},
|
|
{"param1", std::bit_cast<uint32_t>(param123.x.load())},
|
|
{"param2", std::bit_cast<uint32_t>(param123.y.load())},
|
|
{"param3", std::bit_cast<uint32_t>(param123.z.load())},
|
|
{"param4", param456.x},
|
|
{"param5", param456.y},
|
|
{"param6", param456.z},
|
|
};
|
|
|
|
co_await prepare_client_for_patches(a.c);
|
|
auto s = a.c->require_server_state();
|
|
auto fn = s->client_functions->get("CreateObject", a.c->specific_version);
|
|
co_await send_function_call(a.c, fn, label_writes);
|
|
});
|
|
|
|
ChatCommandDefinition cc_matcount(
|
|
{"$matcount"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
|
|
auto p = a.c->character_file();
|
|
if (is_v1_or_v2(a.c->version())) {
|
|
send_text_message_fmt(a.c, "{} HP, {} TP",
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::HP),
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::TP));
|
|
} else {
|
|
send_text_message_fmt(a.c, "{} HP, {} TP, {} POW\n{} MIND, {} EVADE\n{} DEF, {} LUCK",
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::HP),
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::TP),
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER),
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND),
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE),
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF),
|
|
p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK));
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_maxlevel(
|
|
{"$maxlevel"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_leader();
|
|
|
|
auto l = a.c->require_lobby();
|
|
l->max_level = stoull(a.text) - 1;
|
|
if (l->max_level >= 200) {
|
|
l->max_level = 0xFFFFFFFF;
|
|
}
|
|
|
|
if (l->max_level == 0xFFFFFFFF) {
|
|
send_text_message(l, "$C6Maximum level set\nto unlimited");
|
|
} else {
|
|
send_text_message_fmt(l, "$C6Maximum level set\nto {}", l->max_level + 1);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_minlevel(
|
|
{"$minlevel"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_leader();
|
|
|
|
size_t new_min_level = stoull(a.text) - 1;
|
|
|
|
auto l = a.c->require_lobby();
|
|
auto s = a.c->require_server_state();
|
|
bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) ||
|
|
a.c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
|
if (!cheats_allowed && s->cheat_flags.insufficient_minimum_level) {
|
|
size_t default_min_level = s->default_min_level_for_game(a.c->version(), l->episode, l->difficulty);
|
|
if (new_min_level < default_min_level) {
|
|
throw precondition_failed("$C6Cannot set minimum\nlevel below {}", default_min_level + 1);
|
|
}
|
|
}
|
|
|
|
l->min_level = new_min_level;
|
|
send_text_message_fmt(l, "$C6Minimum level set\nto {}", l->min_level + 1);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_next(
|
|
{"$next"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_game(true);
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.warp);
|
|
|
|
auto episode = a.c->proxy_session ? a.c->proxy_session->lobby_episode : a.c->require_lobby()->episode;
|
|
size_t limit = FloorDefinition::limit_for_episode(episode);
|
|
if (limit > 0) {
|
|
send_warp(a.c, (a.c->floor + 1) % limit, true);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_password(
|
|
{"$password"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_leader();
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (a.text.empty()) {
|
|
l->password.clear();
|
|
send_text_message(l, "$C6Game unlocked");
|
|
|
|
} else {
|
|
l->password = a.text;
|
|
send_text_message_fmt(l, "$C6Game password:\n{}", remove_color(l->password));
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_patch(
|
|
{"$patch"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
auto tokens = phosg::split(a.text, ' ');
|
|
if (tokens.empty()) {
|
|
throw std::runtime_error("not enough arguments");
|
|
}
|
|
|
|
std::string patch_name = std::move(tokens[0]);
|
|
std::unordered_map<std::string, uint32_t> label_writes;
|
|
for (size_t z = 0; z < tokens.size() - 1; z++) {
|
|
const auto& token = tokens[z + 1];
|
|
size_t equals_pos = token.find('=');
|
|
std::string key, value;
|
|
if (equals_pos == std::string::npos) {
|
|
key = std::format("arg{}", z);
|
|
value = token;
|
|
} else {
|
|
key = token.substr(0, equals_pos);
|
|
value = token.substr(equals_pos + 1);
|
|
}
|
|
if (value.contains('.')) { // float
|
|
label_writes.emplace(std::move(key), std::bit_cast<uint32_t>(std::stof(value, nullptr)));
|
|
} else { // int
|
|
label_writes.emplace(std::move(key), std::stoul(value, nullptr, 0));
|
|
}
|
|
}
|
|
|
|
co_await prepare_client_for_patches(a.c);
|
|
try {
|
|
auto s = a.c->require_server_state();
|
|
// Note: We can't look this up before prepare_client_for_patches because specific_version may not be set
|
|
auto fn = s->client_functions->get(patch_name, a.c->specific_version);
|
|
|
|
switch (fn->visibility) {
|
|
case ClientFunctionIndex::Function::Visibility::DEBUG_ONLY:
|
|
case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_ONLY:
|
|
a.check_debug_enabled();
|
|
break;
|
|
case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY_WITH_CHEAT_MODE:
|
|
a.check_cheats_enabled_or_allowed(true);
|
|
break;
|
|
case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY:
|
|
case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_AND_CHAT_COMMAND:
|
|
break;
|
|
default:
|
|
throw std::logic_error("Invalid client function visibility");
|
|
}
|
|
|
|
auto ret = co_await send_function_call(a.c, fn, label_writes);
|
|
if (fn->show_return_value) {
|
|
send_text_message_fmt(a.c, "$C6Return value:$C7\nInt: {}\nHex: {:08X}\nFloat: {:g}",
|
|
ret.return_value.load(), ret.return_value.load(), std::bit_cast<float>(ret.return_value.load()));
|
|
}
|
|
|
|
} catch (const std::out_of_range&) {
|
|
send_text_message(a.c, "$C6Invalid function");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_persist(
|
|
{"$persist"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (l->check_flag(Lobby::Flag::DEFAULT)) {
|
|
throw precondition_failed("$C6Default lobbies\ncannot be marked\ntemporary");
|
|
} else if (!l->is_game()) {
|
|
throw precondition_failed("$C6Private lobbies\ncannot be marked\npersistent");
|
|
} else if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
|
throw precondition_failed("$C6Games cannot be\npersistent if a\nquest has already\nbegun");
|
|
} else if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) {
|
|
throw precondition_failed("$C6Spectator teams\ncannot be marked\npersistent");
|
|
} else {
|
|
l->toggle_flag(Lobby::Flag::PERSISTENT);
|
|
send_text_message_fmt(l, "Lobby persistence\n{}", l->check_flag(Lobby::Flag::PERSISTENT) ? "enabled" : "disabled");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_ping(
|
|
{"$ping"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.c->ping_start_time = phosg::now();
|
|
send_command(a.c, 0x1D, 0x00);
|
|
if (a.c->proxy_session) {
|
|
a.c->proxy_session->server_ping_start_time = a.c->ping_start_time;
|
|
C_GuildCardSearch_40 cmd = {
|
|
0x00010000, a.c->proxy_session->remote_guild_card_number, a.c->proxy_session->remote_guild_card_number};
|
|
a.c->proxy_session->server_channel->send(0x40, 0x00, &cmd, sizeof(cmd));
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
static std::string file_path_for_recording(const std::string& args, uint32_t account_id, bool compressed) {
|
|
for (char ch : args) {
|
|
if (ch <= 0x20 || ch > 0x7E || ch == '/') {
|
|
throw std::runtime_error("invalid recording name");
|
|
}
|
|
}
|
|
return std::format("system/ep3/battle-records/{:010}_{}.mzr{}", account_id, args, compressed ? "" : "d");
|
|
}
|
|
|
|
ChatCommandDefinition cc_playrec(
|
|
{"$playrec"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_ep3(true);
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (l->is_game() && l->battle_player) {
|
|
l->battle_player->start();
|
|
} else if (!l->is_game()) {
|
|
|
|
auto s = a.c->require_server_state();
|
|
std::string filename = a.text;
|
|
bool start_battle_player_immediately = (filename.at(0) != '!');
|
|
if (!start_battle_player_immediately) {
|
|
filename = filename.substr(1);
|
|
}
|
|
|
|
std::string data;
|
|
try {
|
|
data = phosg::load_file(file_path_for_recording(filename, a.c->login->account->account_id, false));
|
|
} catch (const phosg::cannot_open_file&) {
|
|
try {
|
|
data = prs_decompress(phosg::load_file(file_path_for_recording(
|
|
filename, a.c->login->account->account_id, true)));
|
|
} catch (const phosg::cannot_open_file&) {
|
|
throw precondition_failed("$C4The recording does\nnot exist");
|
|
}
|
|
}
|
|
auto record = std::make_shared<Episode3::BattleRecord>(data);
|
|
auto battle_player = std::make_shared<Episode3::BattleRecordPlayer>(s->io_context, record);
|
|
auto game = create_game_generic(
|
|
s, a.c, filename, "", Episode::EP3, GameMode::NORMAL, Difficulty::NORMAL, false, nullptr, battle_player);
|
|
if (game) {
|
|
if (start_battle_player_immediately) {
|
|
game->set_flag(Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY);
|
|
}
|
|
s->change_client_lobby(a.c, game);
|
|
a.c->set_flag(Client::Flag::LOADING);
|
|
a.c->log.info_f("LOADING flag set");
|
|
}
|
|
} else {
|
|
throw precondition_failed("$C4This command cannot\nbe used in a game");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_qcall(
|
|
{"$qcall"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_debug_enabled();
|
|
a.check_is_game(true);
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (l->is_game() && l->quest) {
|
|
send_quest_function_call(a.c, std::stoul(a.text, nullptr, 0));
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_qcheck(
|
|
{"$qcheck"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
|
|
auto l = a.c->require_lobby();
|
|
uint16_t flag_num = std::stoul(a.text, nullptr, 0);
|
|
|
|
if (l->is_game()) {
|
|
if (!l->quest_flags_known || l->quest_flags_known->get(l->difficulty, flag_num)) {
|
|
send_text_message_fmt(a.c, "$C7Game: flag 0x{:X} ({})\nis {} on {}",
|
|
flag_num, flag_num,
|
|
a.c->character_file()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
|
name_for_difficulty(l->difficulty));
|
|
} else {
|
|
send_text_message_fmt(a.c, "$C7Game: flag 0x{:X} ({})\nis unknown on {}",
|
|
flag_num, flag_num, name_for_difficulty(l->difficulty));
|
|
}
|
|
} else if (a.c->version() == Version::BB_V4) {
|
|
send_text_message_fmt(a.c, "$C7Player: flag 0x{:X} ({})\nis {} on {}",
|
|
flag_num, flag_num,
|
|
a.c->character_file()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
|
name_for_difficulty(l->difficulty));
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
static void command_qset_qclear(const Args& a, bool should_set) {
|
|
a.check_is_game(true);
|
|
uint16_t flag_num = std::stoul(a.text, nullptr, 0);
|
|
|
|
if (!a.c->proxy_session) {
|
|
a.check_debug_enabled();
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (l->is_game()) {
|
|
if (l->quest_flags_known) {
|
|
l->quest_flags_known->set(l->difficulty, flag_num);
|
|
}
|
|
if (should_set) {
|
|
l->quest_flag_values->set(l->difficulty, flag_num);
|
|
} else {
|
|
l->quest_flag_values->clear(l->difficulty, flag_num);
|
|
}
|
|
}
|
|
|
|
auto p = a.c->character_file(false);
|
|
if (p) {
|
|
if (should_set) {
|
|
p->quest_flags.set(l->difficulty, flag_num);
|
|
} else {
|
|
p->quest_flags.clear(l->difficulty, flag_num);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_v1_or_v2(a.c->version())) {
|
|
G_UpdateQuestFlag_DC_PC_6x75 cmd = {{0x75, 0x02, 0x0000}, flag_num, should_set ? 0 : 1};
|
|
a.c->channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
|
if (a.c->proxy_session) {
|
|
a.c->proxy_session->server_channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
|
}
|
|
} else {
|
|
Difficulty difficulty = a.c->proxy_session ? a.c->proxy_session->lobby_difficulty : a.c->require_lobby()->difficulty;
|
|
G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, static_cast<uint16_t>(difficulty), 0x0000};
|
|
a.c->channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
|
if (a.c->proxy_session) {
|
|
a.c->proxy_session->server_channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
ChatCommandDefinition cc_qclear(
|
|
{"$qclear"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_qset_qclear(a, false);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_qfread(
|
|
{"$qfread"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
auto s = a.c->require_server_state();
|
|
|
|
uint8_t counter_index;
|
|
uint32_t mask;
|
|
try {
|
|
const auto& def = s->quest_counter_fields.at(a.text);
|
|
counter_index = def.first;
|
|
mask = def.second;
|
|
} catch (const std::out_of_range&) {
|
|
throw precondition_failed("$C4Invalid field name");
|
|
}
|
|
if (mask == 0) {
|
|
throw std::runtime_error("invalid quest counter definition");
|
|
}
|
|
|
|
uint32_t counter_value = a.c->character_file()->quest_counters.at(counter_index) & mask;
|
|
|
|
while (!(mask & 1)) {
|
|
mask >>= 1;
|
|
counter_value >>= 1;
|
|
}
|
|
|
|
if (mask == 1) {
|
|
send_text_message_fmt(a.c, "$C7Field {}\nhas value {}", a.text, counter_value ? "TRUE" : "FALSE");
|
|
} else {
|
|
send_text_message_fmt(a.c, "$C7Field {}\nhas value {}", a.text, counter_value);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_qgread(
|
|
{"$qgread"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
uint8_t counter_num = std::stoul(a.text, nullptr, 0);
|
|
const auto& counters = a.c->character_file()->quest_counters;
|
|
if (counter_num >= counters.size()) {
|
|
throw precondition_failed("$C7Counter ID must be\nless than {}", counters.size());
|
|
} else {
|
|
send_text_message_fmt(a.c, "$C7Quest counter {}\nhas value {}", counter_num, counters[counter_num]);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_qgwrite(
|
|
{"$qgwrite"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
if (a.c->version() != Version::BB_V4) {
|
|
throw precondition_failed("$C6This command can\nonly be used on BB");
|
|
}
|
|
a.check_debug_enabled();
|
|
auto l = a.c->require_lobby();
|
|
if (!l->is_game()) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in the lobby");
|
|
}
|
|
|
|
auto tokens = phosg::split(a.text, ' ');
|
|
if (tokens.size() != 2) {
|
|
throw precondition_failed("$C6Incorrect number\nof arguments");
|
|
}
|
|
|
|
uint8_t counter_num = std::stoul(tokens[0], nullptr, 0);
|
|
uint32_t value = std::stoul(tokens[1], nullptr, 0);
|
|
auto& counters = a.c->character_file()->quest_counters;
|
|
if (counter_num >= counters.size()) {
|
|
throw precondition_failed("$C7Counter ID must be\nless than {}", counters.size());
|
|
} else {
|
|
a.c->character_file()->quest_counters[counter_num] = value;
|
|
G_SetQuestCounter_BB_6xD2 cmd = {{0xD2, sizeof(G_SetQuestCounter_BB_6xD2) / 4, a.c->lobby_client_id}, counter_num, value};
|
|
send_command_t(a.c, 0x60, 0x00, cmd);
|
|
send_text_message_fmt(a.c, "$C7Quest counter {}\nset to {}", counter_num, value);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_qset(
|
|
{"$qset"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_qset_qclear(a, true);
|
|
co_return;
|
|
});
|
|
|
|
static void command_qsync_qsyncall(const Args& a, bool send_to_lobby) {
|
|
a.check_is_game(true);
|
|
a.check_debug_enabled();
|
|
|
|
auto tokens = phosg::split(a.text, ' ');
|
|
if (tokens.size() != 2) {
|
|
throw precondition_failed("$C6Incorrect number of\narguments");
|
|
}
|
|
|
|
G_SyncQuestRegister_6x77 cmd;
|
|
cmd.header = {0x77, 0x03, 0x0000};
|
|
cmd.register_number = std::stoul(tokens[0].substr(1), nullptr, 0);
|
|
cmd.unused = 0;
|
|
if (tokens[0][0] == 'r') {
|
|
cmd.value.as_int = std::stoul(tokens[1], nullptr, 0);
|
|
} else if (tokens[0][0] == 'f') {
|
|
cmd.value.as_float = stof(tokens[1]);
|
|
} else {
|
|
throw precondition_failed("$C6First argument must\nbe a register");
|
|
}
|
|
send_command_t(a.c, 0x60, 0x00, cmd);
|
|
if (send_to_lobby) {
|
|
if (a.c->proxy_session) {
|
|
send_command_t(a.c->proxy_session->server_channel, 0x60, 0x00, cmd);
|
|
} else {
|
|
send_command_t(a.c->require_lobby(), 0x60, 0x00, cmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
ChatCommandDefinition cc_qsync(
|
|
{"$qsync"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_qsync_qsyncall(a, false);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_qsyncall(
|
|
{"$qsyncall"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_qsync_qsyncall(a, true);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_quest(
|
|
{"$quest"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
|
|
auto s = a.c->require_server_state();
|
|
auto q = s->quest_index->get(stoul(a.text));
|
|
if (!q) {
|
|
throw precondition_failed("$C6Quest not found");
|
|
}
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (a.check_permissions && !a.c->check_flag(Client::Flag::DEBUG_ENABLED)) {
|
|
if (l->count_clients() > 1) {
|
|
throw precondition_failed("$C6This command can only\nbe used with no\nother players present");
|
|
}
|
|
if (!q->meta.allow_start_from_chat_command) {
|
|
throw precondition_failed("$C6This quest cannot\nbe started with the\n%squest command");
|
|
}
|
|
}
|
|
|
|
for (size_t client_id = 0; client_id < l->max_clients; client_id++) {
|
|
auto lc = l->clients[client_id];
|
|
if (lc) {
|
|
if (!q->version(lc->version(), lc->language())) {
|
|
throw precondition_failed("$C6Quest does not exist\nfor all players\' game\nversions");
|
|
}
|
|
}
|
|
}
|
|
|
|
set_lobby_quest(a.c->require_lobby(), q, true);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_fastkill(
|
|
{"$fastkill"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (!a.c->proxy_session) {
|
|
a.check_is_game(true);
|
|
}
|
|
|
|
if (a.c->check_flag(Client::Flag::FAST_KILLS_ENABLED)) {
|
|
a.c->clear_flag(Client::Flag::FAST_KILLS_ENABLED);
|
|
send_text_message(a.c, "$C6Fast kills disabled");
|
|
} else {
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.fast_kills);
|
|
a.c->set_flag(Client::Flag::FAST_KILLS_ENABLED);
|
|
send_text_message(a.c, "$C6Fast kills enabled");
|
|
}
|
|
co_return;
|
|
});
|
|
ChatCommandDefinition cc_allrare(
|
|
{"$allrare"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_debug_enabled();
|
|
a.c->toggle_flag(Client::Flag::ALL_RARES_ENABLED);
|
|
send_text_message_fmt(
|
|
a.c, "$C6All-rares {}", a.c->check_flag(Client::Flag::ALL_RARES_ENABLED) ? "enabled" : "disabled");
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_rand(
|
|
{"$rand"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
auto s = a.c->require_server_state();
|
|
auto l = a.c->require_lobby();
|
|
a.check_is_game(false);
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.override_random_seed);
|
|
|
|
if (a.text.empty()) {
|
|
a.c->override_random_seed = -1;
|
|
send_text_message(a.c, "$C6Override seed\nremoved");
|
|
} else {
|
|
a.c->override_random_seed = stoll(a.text, 0, 16);
|
|
send_text_message(a.c, "$C6Override seed\nset");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
static bool console_address_in_range(Version version, uint32_t addr) {
|
|
if (is_dc(version)) {
|
|
return ((addr > 0x8C000000) && (addr <= 0x8CFFFFFC));
|
|
} else if (is_gc(version)) {
|
|
return ((addr > 0x80000000) && (addr <= 0x817FFFFC));
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
ChatCommandDefinition cc_readmem(
|
|
{"$readmem"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_debug_enabled();
|
|
|
|
uint32_t addr = std::stoul(a.text, nullptr, 16);
|
|
if (!console_address_in_range(a.c->version(), addr)) {
|
|
throw precondition_failed("$C4Address out of\nrange");
|
|
}
|
|
|
|
co_await prepare_client_for_patches(a.c);
|
|
|
|
std::shared_ptr<const ClientFunctionIndex::Function> fn;
|
|
try {
|
|
auto s = a.c->require_server_state();
|
|
fn = s->client_functions->get("ReadMemoryWord", a.c->specific_version);
|
|
} catch (const std::out_of_range&) {
|
|
throw precondition_failed("Invalid patch name");
|
|
}
|
|
|
|
std::unordered_map<std::string, uint32_t> label_writes{{"address", addr}};
|
|
auto res = co_await send_function_call(a.c, fn, label_writes);
|
|
std::string data_str;
|
|
if (is_big_endian(a.c->version())) {
|
|
be_uint32_t v = res.return_value.load();
|
|
data_str = phosg::format_data_string(&v, sizeof(v));
|
|
} else {
|
|
data_str = phosg::format_data_string(&res.return_value, sizeof(res.return_value));
|
|
}
|
|
send_text_message_fmt(a.c, "Bytes at {:08X}:\n$C6{}", addr, data_str);
|
|
});
|
|
|
|
ChatCommandDefinition cc_save(
|
|
{"$save"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_version(Version::BB_V4);
|
|
a.check_is_proxy(false);
|
|
a.c->save_all();
|
|
send_text_message(a.c, "All data saved");
|
|
a.c->reschedule_save_game_data_timer();
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_savechar(
|
|
{"$savechar"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
if (a.check_permissions && a.c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) {
|
|
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
|
|
}
|
|
co_await server_command_bbchar_savechar(a, false);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_savefiles(
|
|
{"$savefiles"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(true);
|
|
|
|
auto s = a.c->require_server_state();
|
|
if (!s->proxy_allow_save_files) {
|
|
send_text_message(a.c, "$C6Save files is not\nallowed");
|
|
} else if (a.c->check_flag(Client::Flag::PROXY_SAVE_FILES)) {
|
|
a.c->clear_flag(Client::Flag::PROXY_SAVE_FILES);
|
|
send_text_message(a.c, "$C6Save files disabled");
|
|
} else {
|
|
auto s = a.c->require_server_state();
|
|
a.c->set_flag(Client::Flag::PROXY_SAVE_FILES);
|
|
send_text_message(a.c, "$C6Save files enabled");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_saverec(
|
|
{"$saverec"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
// TODO: We can support this on the proxy server, but it would only include CA commands from the local player
|
|
a.check_is_proxy(false);
|
|
if (!a.c->ep3_prev_battle_record) {
|
|
throw precondition_failed("$C4No finished\nrecording is\npresent");
|
|
}
|
|
phosg::save_file(
|
|
file_path_for_recording(a.text, a.c->login->account->account_id, false),
|
|
a.c->ep3_prev_battle_record->serialize());
|
|
send_text_message(a.c, "$C7Recording saved");
|
|
a.c->ep3_prev_battle_record.reset();
|
|
co_return;
|
|
});
|
|
|
|
static asio::awaitable<void> command_send_command(const Args& a, bool to_client, bool to_server, bool send_protected) {
|
|
if (!a.c->proxy_session) {
|
|
a.check_debug_enabled();
|
|
}
|
|
std::string data = phosg::parse_data_string(a.text);
|
|
data.resize((data.size() + 3) & (~3));
|
|
if (to_client) {
|
|
if (send_protected) {
|
|
co_await send_protected_command(a.c, data.data(), data.size(), false);
|
|
} else {
|
|
a.c->channel->send(data);
|
|
}
|
|
}
|
|
if (to_server) {
|
|
if (a.c->proxy_session) {
|
|
a.c->proxy_session->server_channel->send(data);
|
|
} else {
|
|
co_await on_command_with_header(a.c, data);
|
|
}
|
|
}
|
|
co_return;
|
|
}
|
|
|
|
ChatCommandDefinition cc_sb(
|
|
{"$sb"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return command_send_command(a, true, true, false);
|
|
});
|
|
|
|
ChatCommandDefinition cc_sc(
|
|
{"$sc"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return command_send_command(a, true, false, false);
|
|
});
|
|
|
|
ChatCommandDefinition cc_scp(
|
|
{"$scp"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return command_send_command(a, true, false, true);
|
|
});
|
|
|
|
ChatCommandDefinition cc_secid(
|
|
{"$secid"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.override_section_id);
|
|
|
|
uint8_t new_override_section_id;
|
|
if (a.text.empty()) {
|
|
new_override_section_id = 0xFF;
|
|
send_text_message(a.c, "$C6Override section ID\nremoved");
|
|
} else {
|
|
new_override_section_id = section_id_for_name(a.text);
|
|
if (new_override_section_id == 0xFF) {
|
|
throw precondition_failed("$C6Invalid section ID");
|
|
} else {
|
|
send_text_message_fmt(a.c, "$C6Override section ID\nset to {}", name_for_section_id(new_override_section_id));
|
|
}
|
|
}
|
|
|
|
a.c->override_section_id = new_override_section_id;
|
|
if (!a.c->proxy_session) {
|
|
auto l = a.c->require_lobby();
|
|
if (l->is_game() && (l->leader_id == a.c->lobby_client_id)) {
|
|
l->override_section_id = new_override_section_id;
|
|
l->create_item_creator();
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_setassist(
|
|
{"$setassist"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_ep3(true);
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_in_game(s->cheat_flags.ep3_replace_assist);
|
|
|
|
auto l = a.c->require_lobby();
|
|
if (l->episode != Episode::EP3) {
|
|
throw std::logic_error("non-Ep3 client in Ep3 game");
|
|
}
|
|
if (!l->ep3_server) {
|
|
throw precondition_failed("$C6Episode 3 server\nis not initialized");
|
|
}
|
|
if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) {
|
|
throw precondition_failed("$C6Battle has not\nyet begun");
|
|
}
|
|
if (a.text.empty()) {
|
|
throw precondition_failed("$C6Missing arguments");
|
|
}
|
|
|
|
size_t client_id;
|
|
std::string card_name;
|
|
if (isdigit(a.text[0])) {
|
|
auto tokens = phosg::split(a.text, ' ', 1);
|
|
client_id = std::stoul(tokens.at(0), nullptr, 0) - 1;
|
|
card_name = tokens.at(1);
|
|
} else {
|
|
client_id = a.c->lobby_client_id;
|
|
card_name = a.text;
|
|
}
|
|
if (client_id >= 4) {
|
|
throw precondition_failed("$C6Invalid client ID");
|
|
}
|
|
|
|
std::shared_ptr<const Episode3::CardIndex::CardEntry> ce;
|
|
try {
|
|
ce = l->ep3_server->options.card_index->definition_for_name_normalized(card_name);
|
|
} catch (const std::out_of_range&) {
|
|
throw precondition_failed("$C6Card not found");
|
|
}
|
|
if (ce->def.type != Episode3::CardType::ASSIST) {
|
|
throw precondition_failed("$C6Card is not an\nAssist card");
|
|
}
|
|
l->ep3_server->force_replace_assist_card(client_id, ce->def.card_id);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_server_info(
|
|
{"$si"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
auto s = a.c->require_server_state();
|
|
std::string uptime_str = phosg::format_duration(phosg::now() - s->creation_time);
|
|
send_text_message_fmt(a.c,
|
|
"Uptime: $C6{}$C7\nLobbies: $C6{}$C7\nClients: $C6{}$C7(g) $C6{}$C7(p)",
|
|
uptime_str,
|
|
s->id_to_lobby.size(),
|
|
s->game_server->all_clients().size() - ProxySession::num_proxy_sessions,
|
|
ProxySession::num_proxy_sessions);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_silence(
|
|
{"$silence"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_account_flag(Account::Flag::SILENCE_USER);
|
|
|
|
auto s = a.c->require_server_state();
|
|
auto target = s->find_client(&a.text);
|
|
if (!target->login) {
|
|
// This should be impossible, but I'll bet it's not actually
|
|
throw precondition_failed("$C6Client not logged in");
|
|
}
|
|
|
|
if (target->login->account->check_flag(Account::Flag::SILENCE_USER)) {
|
|
throw precondition_failed("$C6You do not have\nsufficient privileges.");
|
|
}
|
|
|
|
target->can_chat = !target->can_chat;
|
|
std::string target_name = name_for_client(target);
|
|
send_text_message_fmt(a.c, "$C6{} {}silenced", target_name, target->can_chat ? "un" : "");
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_song(
|
|
{"$song"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_ep3(true);
|
|
|
|
int32_t song = stol(a.text, nullptr, 0);
|
|
if (song < 0 && a.c->proxy_session) {
|
|
song = -song;
|
|
send_ep3_change_music(a.c->proxy_session->server_channel, song);
|
|
}
|
|
send_ep3_change_music(a.c->channel, std::stoul(a.text, nullptr, 0));
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_sound(
|
|
{"$sound"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
bool echo_to_all = (!a.text.empty() && a.text[0] == '!');
|
|
uint32_t sound_id = std::stoul(echo_to_all ? a.text.substr(1) : a.text, nullptr, 16);
|
|
|
|
auto l = a.c->require_lobby();
|
|
uint8_t area = l->is_game() ? l->area_for_floor(a.c->version(), a.c->floor) : 0x0F;
|
|
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, area, 0x00, a.c->lobby_client_id, sound_id};
|
|
if (!echo_to_all) {
|
|
send_command_t(a.c, 0x60, 0x00, cmd);
|
|
} else if (a.c->proxy_session) {
|
|
send_command_t(a.c, 0x60, 0x00, cmd);
|
|
send_command_t(a.c->proxy_session->server_channel, 0x60, 0x00, cmd);
|
|
} else {
|
|
a.check_debug_enabled();
|
|
send_command_t(a.c->require_lobby(), 0x60, 0x00, cmd);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_spec(
|
|
{"$spec"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_ep3(true);
|
|
auto l = a.c->require_lobby();
|
|
if (!l->is_ep3()) {
|
|
throw std::logic_error("Episode 3 client in non-Episode 3 game");
|
|
}
|
|
|
|
// In non-tournament games, only the leader can do this; in a tournament match, the players don't have control
|
|
// over who the leader is, so we allow all players to use this command
|
|
if (!l->tournament_match) {
|
|
a.check_is_leader();
|
|
}
|
|
|
|
if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in a spectator\nteam");
|
|
}
|
|
|
|
if (l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)) {
|
|
l->clear_flag(Lobby::Flag::SPECTATORS_FORBIDDEN);
|
|
send_text_message(l, "$C6Spectators allowed");
|
|
|
|
} else {
|
|
l->set_flag(Lobby::Flag::SPECTATORS_FORBIDDEN);
|
|
for (auto watcher_l : l->watcher_lobbies) {
|
|
send_command(watcher_l, 0xED, 0x00);
|
|
}
|
|
l->watcher_lobbies.clear();
|
|
send_text_message(l, "$C6Spectators forbidden");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_ss(
|
|
{"$ss"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
return command_send_command(a, false, true, false);
|
|
});
|
|
|
|
ChatCommandDefinition cc_stat(
|
|
{"$stat"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_ep3(true);
|
|
auto l = a.c->require_lobby();
|
|
if (l->episode != Episode::EP3) {
|
|
throw std::logic_error("non-Ep3 client in Ep3 game");
|
|
}
|
|
if (!l->ep3_server) {
|
|
throw precondition_failed("$C6Episode 3 server\nis not initialized");
|
|
}
|
|
if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) {
|
|
throw precondition_failed("$C6Battle has not\nyet started");
|
|
}
|
|
auto ps = l->ep3_server->player_states.at(a.c->lobby_client_id);
|
|
if (!ps) {
|
|
throw precondition_failed("$C6Player is missing");
|
|
}
|
|
uint8_t team_id = ps->get_team_id();
|
|
if (team_id > 1) {
|
|
throw std::logic_error("team ID is incorrect");
|
|
}
|
|
|
|
if (a.text == "rank") {
|
|
float score = ps->stats.score(l->ep3_server->get_round_num());
|
|
uint8_t rank = ps->stats.rank_for_score(score);
|
|
const char* rank_name = ps->stats.name_for_rank(rank);
|
|
send_text_message_fmt(a.c, "$C7Score: {:g}\nRank: {} ({})", score, rank, rank_name);
|
|
} else if (a.text == "duration") {
|
|
send_text_message_fmt(a.c, "$C7Duration: {}",
|
|
phosg::format_duration(phosg::now() - l->ep3_server->battle_start_usecs));
|
|
} else if (a.text == "fcs-destroyed") {
|
|
send_text_message_fmt(a.c, "$C7Team FCs destroyed:\n{}", l->ep3_server->team_num_ally_fcs_destroyed[team_id]);
|
|
} else if (a.text == "cards-destroyed") {
|
|
send_text_message_fmt(a.c, "$C7Team cards destroyed:\n{}", l->ep3_server->team_num_cards_destroyed[team_id]);
|
|
} else if (a.text == "damage-given") {
|
|
send_text_message_fmt(a.c, "$C7Damage given: {}", ps->stats.damage_given);
|
|
} else if (a.text == "damage-taken") {
|
|
send_text_message_fmt(a.c, "$C7Damage taken: {}", ps->stats.damage_taken);
|
|
} else if (a.text == "opp-cards-destroyed") {
|
|
send_text_message_fmt(a.c, "$C7Opp. cards destroyed:\n{}", ps->stats.num_opponent_cards_destroyed);
|
|
} else if (a.text == "own-cards-destroyed") {
|
|
send_text_message_fmt(a.c, "$C7Own cards destroyed:\n{}", ps->stats.num_owned_cards_destroyed);
|
|
} else if (a.text == "move-distance") {
|
|
send_text_message_fmt(a.c, "$C7Move distance: {}", ps->stats.total_move_distance);
|
|
} else if (a.text == "cards-set") {
|
|
send_text_message_fmt(a.c, "$C7Cards set: {}", ps->stats.num_cards_set);
|
|
} else if (a.text == "fcs-set") {
|
|
send_text_message_fmt(a.c, "$C7FC cards set: {}", ps->stats.num_item_or_creature_cards_set);
|
|
} else if (a.text == "attack-actions-set") {
|
|
send_text_message_fmt(a.c, "$C7Attack actions set:\n{}", ps->stats.num_attack_actions_set);
|
|
} else if (a.text == "techs-set") {
|
|
send_text_message_fmt(a.c, "$C7Techs set: {}", ps->stats.num_tech_cards_set);
|
|
} else if (a.text == "assists-set") {
|
|
send_text_message_fmt(a.c, "$C7Assists set: {}", ps->stats.num_assist_cards_set);
|
|
} else if (a.text == "defenses-self") {
|
|
send_text_message_fmt(a.c, "$C7Defenses on self:\n{}", ps->stats.defense_actions_set_on_self);
|
|
} else if (a.text == "defenses-ally") {
|
|
send_text_message_fmt(a.c, "$C7Defenses on ally:\n{}", ps->stats.defense_actions_set_on_ally);
|
|
} else if (a.text == "cards-drawn") {
|
|
send_text_message_fmt(a.c, "$C7Cards drawn: {}", ps->stats.num_cards_drawn);
|
|
} else if (a.text == "max-attack-damage") {
|
|
send_text_message_fmt(a.c, "$C7Maximum attack damage:\n{}", ps->stats.max_attack_damage);
|
|
} else if (a.text == "max-combo") {
|
|
send_text_message_fmt(a.c, "$C7Longest combo: {}", ps->stats.max_attack_combo_size);
|
|
} else if (a.text == "attacks-given") {
|
|
send_text_message_fmt(a.c, "$C7Attacks given: {}", ps->stats.num_attacks_given);
|
|
} else if (a.text == "attacks-taken") {
|
|
send_text_message_fmt(a.c, "$C7Attacks taken: {}", ps->stats.num_attacks_taken);
|
|
} else if (a.text == "sc-damage") {
|
|
send_text_message_fmt(a.c, "$C7SC damage taken: {}", ps->stats.sc_damage_taken);
|
|
} else if (a.text == "damage-defended") {
|
|
send_text_message_fmt(a.c, "$C7Damage defended: {}", ps->stats.action_card_negated_damage);
|
|
} else {
|
|
throw precondition_failed("$C6Unknown statistic");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_surrender(
|
|
{"$surrender"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_ep3(true);
|
|
auto l = a.c->require_lobby();
|
|
if (l->episode != Episode::EP3) {
|
|
throw std::logic_error("non-Ep3 client in Ep3 game");
|
|
}
|
|
if (!l->ep3_server) {
|
|
throw precondition_failed("$C6Episode 3 server\nis not initialized");
|
|
}
|
|
if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) {
|
|
throw precondition_failed("$C6Battle has not\nyet started");
|
|
}
|
|
auto ps = l->ep3_server->get_player_state(a.c->lobby_client_id);
|
|
if (!ps || !ps->is_alive()) {
|
|
throw precondition_failed("$C6Defeated players\ncannot surrender");
|
|
}
|
|
std::string name = remove_color(a.c->character_file()->disp.name.decode(a.c->language()));
|
|
send_text_message_fmt(l, "$C6{} has\nsurrendered", name);
|
|
for (const auto& watcher_l : l->watcher_lobbies) {
|
|
send_text_message_fmt(watcher_l, "$C6{} has\nsurrendered", name);
|
|
}
|
|
l->ep3_server->force_battle_result(a.c->lobby_client_id, false);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_swa(
|
|
{"$swa"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
auto s = a.c->require_server_state();
|
|
auto l = a.c->require_lobby();
|
|
a.check_is_game(true);
|
|
|
|
a.c->toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
|
|
bool enabled = a.c->check_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
|
|
send_text_message_fmt(a.c, "$C6Switch assist {}", enabled ? "enabled" : "disabled");
|
|
co_return;
|
|
});
|
|
|
|
static void command_swset_swclear(const Args& a, bool should_set) {
|
|
a.check_debug_enabled();
|
|
a.check_is_game(true);
|
|
|
|
auto tokens = phosg::split(a.text, ' ');
|
|
uint8_t floor, flag_num;
|
|
if (tokens.size() == 1) {
|
|
floor = a.c->floor;
|
|
flag_num = std::stoul(tokens[0], nullptr, 0);
|
|
} else if (tokens.size() == 2) {
|
|
floor = std::stoul(tokens[0], nullptr, 0);
|
|
flag_num = std::stoul(tokens[1], nullptr, 0);
|
|
} else {
|
|
throw precondition_failed("$C4Incorrect parameters");
|
|
}
|
|
|
|
uint8_t cmd_flags = should_set ? 0x01 : 0x00;
|
|
G_WriteSwitchFlag_6x05 cmd = {{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, floor, cmd_flags};
|
|
|
|
if (!a.c->proxy_session) {
|
|
auto l = a.c->require_lobby();
|
|
if (l->switch_flags) {
|
|
if (should_set) {
|
|
l->switch_flags->set(floor, flag_num);
|
|
} else {
|
|
l->switch_flags->clear(floor, flag_num);
|
|
}
|
|
}
|
|
send_command_t(l, 0x60, 0x00, cmd);
|
|
} else {
|
|
send_command_t(a.c, 0x60, 0x00, cmd);
|
|
send_command_t(a.c->proxy_session->server_channel, 0x60, 0x00, cmd);
|
|
}
|
|
}
|
|
|
|
ChatCommandDefinition cc_swclear(
|
|
{"$swclear"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_swset_swclear(a, false);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_swset(
|
|
{"$swset"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_swset_swclear(a, true);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_swsetall(
|
|
{"$swsetall"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_debug_enabled();
|
|
a.check_is_game(true);
|
|
|
|
parray<G_WriteSwitchFlag_6x05, 0x100> cmds;
|
|
for (size_t z = 0; z < cmds.size(); z++) {
|
|
auto& cmd = cmds[z];
|
|
cmd.header.subcommand = 0x05;
|
|
cmd.header.size = 0x03;
|
|
cmd.header.entity_id = 0xFFFF;
|
|
cmd.switch_flag_floor = a.c->floor;
|
|
cmd.switch_flag_num = z;
|
|
cmd.flags = 0x01;
|
|
}
|
|
cmds[0].flags = 0x03; // Play room unlock sound
|
|
|
|
if (!a.c->proxy_session) {
|
|
auto l = a.c->require_lobby();
|
|
if (!l->is_game()) {
|
|
throw precondition_failed("$C6This command cannot\nbe used in the lobby");
|
|
}
|
|
if (l->switch_flags) {
|
|
l->switch_flags->data[a.c->floor].clear(0xFF);
|
|
}
|
|
send_command_t(l, 0x6C, 0x00, cmds);
|
|
} else {
|
|
send_command_t(a.c, 0x6C, 0x00, cmds);
|
|
send_command_t(a.c->proxy_session->server_channel, 0x6C, 0x00, cmds);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_switchchar(
|
|
{"$switchchar"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
auto l = a.c->require_lobby();
|
|
auto s = a.c->require_server_state();
|
|
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(false);
|
|
if (a.c->version() != Version::BB_V4) {
|
|
throw precondition_failed("This command can only\nbe used on BB");
|
|
}
|
|
|
|
int32_t index = stol(a.text, nullptr, 0) - 1;
|
|
if (index < 0) {
|
|
throw precondition_failed("Invalid slot number");
|
|
}
|
|
auto filename = Client::character_filename(a.c->login->bb_license->username, index);
|
|
if (!std::filesystem::is_regular_file(filename)) {
|
|
throw precondition_failed("No character exists\nin that slot");
|
|
}
|
|
|
|
a.c->unload_character(true);
|
|
a.c->bb_character_index = index;
|
|
a.c->bb_bank_character_index = index;
|
|
|
|
// TODO: This can trigger a client bug where the previous character's name label object isn't deleted if the
|
|
// leave and join notifications are received on the same frame. This results in the receiving player seeing both
|
|
// labels over the new character, with the latest one appearing on top. We could fix this by requiring each
|
|
// recipient to reply to a ping between the two commands, similar to how the 64 and 6x6D commands are split
|
|
// during game joining, but implementing that here seems not worth the effort given the low likelihood and impact
|
|
// of this bug.
|
|
send_complete_player_bb(a.c);
|
|
send_player_leave_notification(l, a.c->lobby_client_id);
|
|
s->send_lobby_join_notifications(l, a.c);
|
|
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_unset(
|
|
{"$unset"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
a.check_is_ep3(true);
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_in_game(s->cheat_flags.ep3_unset_field_character);
|
|
auto l = a.c->require_lobby();
|
|
if (l->episode != Episode::EP3) {
|
|
throw std::logic_error("non-Ep3 client in Ep3 game");
|
|
}
|
|
if (!l->ep3_server) {
|
|
throw precondition_failed("$C6Episode 3 server\nis not initialized");
|
|
}
|
|
if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) {
|
|
throw precondition_failed("$C6Battle has not\nyet begun");
|
|
}
|
|
|
|
size_t index = stoull(a.text);
|
|
if (index == 0) {
|
|
l->ep3_server->force_replace_assist_card(a.c->lobby_client_id, 0xFFFF);
|
|
} else {
|
|
l->ep3_server->force_destroy_field_character(a.c->lobby_client_id, index - 1);
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_variations(
|
|
{"$variations"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
// Note: This command is intentionally undocumented, since it's primarily used for testing
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(false);
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_in_game(s->cheat_flags.override_variations);
|
|
|
|
a.c->override_variations = std::make_unique<Variations>();
|
|
for (size_t z = 0; z < std::min<size_t>(a.c->override_variations->entries.size() * 2, a.text.size()); z++) {
|
|
auto& entry = a.c->override_variations->entries.at(z / 2);
|
|
if (z & 1) {
|
|
entry.entities = a.text[z] - '0';
|
|
} else {
|
|
entry.layout = a.text[z] - '0';
|
|
}
|
|
}
|
|
auto vars_str = a.c->override_variations->str();
|
|
a.c->log.info_f("Override variations set to {}", vars_str);
|
|
co_return;
|
|
});
|
|
|
|
static void command_warp(const Args& a, bool is_warpall) {
|
|
a.check_is_game(true);
|
|
auto s = a.c->require_server_state();
|
|
a.check_cheats_enabled_or_allowed(s->cheat_flags.warp);
|
|
|
|
uint32_t floor = std::stoul(a.text, nullptr, 0);
|
|
if (!is_warpall && (a.c->floor == floor)) {
|
|
return;
|
|
}
|
|
|
|
Episode episode = a.c->proxy_session ? a.c->proxy_session->lobby_episode : a.c->require_lobby()->episode;
|
|
size_t limit = FloorDefinition::limit_for_episode(episode);
|
|
if (limit == 0) {
|
|
return;
|
|
} else if (floor > limit) {
|
|
throw precondition_failed("$C6Area numbers must\nbe {} or less", limit);
|
|
}
|
|
|
|
if (a.c->proxy_session) {
|
|
send_warp(a.c, floor, true);
|
|
if (is_warpall) {
|
|
for (size_t z = 0; z < a.c->proxy_session->lobby_players.size(); z++) {
|
|
const auto& lp = a.c->proxy_session->lobby_players[z];
|
|
if (lp.guild_card_number) {
|
|
send_warp(a.c->proxy_session->server_channel, z, floor, true);
|
|
}
|
|
}
|
|
}
|
|
} else if (is_warpall) {
|
|
send_warp(a.c->require_lobby(), floor, false);
|
|
} else {
|
|
send_warp(a.c, floor, true);
|
|
}
|
|
}
|
|
|
|
ChatCommandDefinition cc_warp(
|
|
{"$warp", "$warpme"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_warp(a, false);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_warpall(
|
|
{"$warpall"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
command_warp(a, true);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_what(
|
|
{"$what"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
auto l = a.c->require_lobby();
|
|
if (!episode_has_arpg_semantics(l->episode)) {
|
|
co_return;
|
|
}
|
|
|
|
std::shared_ptr<const Lobby::FloorItem> nearest_fi;
|
|
float min_dist2 = 0.0f;
|
|
for (const auto& it : l->floor_item_managers.at(a.c->floor).items) {
|
|
if (!it.second->visible_to_client(a.c->lobby_client_id)) {
|
|
continue;
|
|
}
|
|
float dist2 = (it.second->pos - a.c->pos).norm2();
|
|
if (!nearest_fi || (dist2 < min_dist2)) {
|
|
nearest_fi = it.second;
|
|
min_dist2 = dist2;
|
|
}
|
|
}
|
|
|
|
if (!nearest_fi) {
|
|
throw precondition_failed("$C4No items are near you");
|
|
} else {
|
|
auto s = a.c->require_server_state();
|
|
send_text_message(
|
|
a.c, s->describe_item(a.c->version(), nearest_fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES));
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_enes) {
|
|
// TODO: This probably wouldn't be too hard to implement for proxy sessions. We already have the map and most of the
|
|
// lobby metadata (episode, etc.)
|
|
a.check_is_proxy(false);
|
|
a.check_is_game(true);
|
|
auto l = a.c->require_lobby();
|
|
if (!l->map_state) {
|
|
throw precondition_failed("$C4No map loaded");
|
|
}
|
|
|
|
uint8_t area, layout_var;
|
|
auto s = a.c->require_server_state();
|
|
if (l->episode != Episode::EP3) {
|
|
area = l->area_for_floor(a.c->version(), a.c->floor);
|
|
layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00;
|
|
} else {
|
|
area = a.c->floor;
|
|
layout_var = 0;
|
|
}
|
|
|
|
double min_dist2 = -1.0;
|
|
VectorXYZF nearest_worldspace_pos;
|
|
std::shared_ptr<const MapState::ObjectState> nearest_obj;
|
|
std::shared_ptr<const MapState::EnemyState> nearest_ene;
|
|
|
|
auto check_entity = [&](auto& nearest_entity, auto entity, const auto& def) -> void {
|
|
VectorXYZF worldspace_pos;
|
|
if (l->episode != Episode::EP3) {
|
|
try {
|
|
const auto& room = s->room_layout_index->get_room(area, layout_var, def.set_entry->room);
|
|
// This is the order in which the game does the rotations; not sure why
|
|
worldspace_pos = def.set_entry->pos.rotate_x(room.angle.x).rotate_z(room.angle.z).rotate_y(room.angle.y) + room.position;
|
|
} catch (const std::out_of_range&) {
|
|
a.c->log.warning_f("Can't find definition for room {:02X}:{:02X}:{:08X}", area, layout_var, def.set_entry->room);
|
|
worldspace_pos = def.set_entry->pos;
|
|
}
|
|
} else {
|
|
worldspace_pos = def.set_entry->pos;
|
|
}
|
|
|
|
float dist2 = (VectorXZF(worldspace_pos) - a.c->pos).norm2();
|
|
if ((min_dist2 < 0.0) || (dist2 < min_dist2)) {
|
|
nearest_entity = entity;
|
|
nearest_worldspace_pos = worldspace_pos;
|
|
min_dist2 = dist2;
|
|
}
|
|
};
|
|
|
|
if (include_objs) {
|
|
for (const auto& it : l->map_state->iter_object_states(a.c->version())) {
|
|
if (it->super_obj && (it->super_obj->floor == a.c->floor)) {
|
|
const auto& def = it->super_obj->version(a.c->version());
|
|
if (def.set_entry) {
|
|
check_entity(nearest_obj, it, def);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (include_enes) {
|
|
for (const auto& it : l->map_state->iter_enemy_states(a.c->version())) {
|
|
if (it->super_ene && (it->super_ene->floor == a.c->floor)) {
|
|
const auto& def = it->super_ene->version(a.c->version());
|
|
if (def.set_entry) {
|
|
check_entity(nearest_ene, it, def);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Since we check all objects first, nearest_ene will only be set if there is an enemy closer than all objects. So,
|
|
// we print that if it's set, and print the object if not.
|
|
if (nearest_ene) {
|
|
const auto* set_entry = nearest_ene->super_ene->version(a.c->version()).set_entry;
|
|
std::string type_name = MapFile::name_for_enemy_type(set_entry->base_type, a.c->version(), area);
|
|
uint8_t area = l->area_for_floor(a.c->version(), a.c->floor);
|
|
send_text_message_fmt(a.c, "$C5E-{:03X}\n$C6{}\n$C2{}\n$C7X:{:.2f} Z:{:.2f}",
|
|
nearest_ene->e_id, phosg::name_for_enum(nearest_ene->type(a.c->version(), area, l->difficulty, l->event)),
|
|
type_name, nearest_worldspace_pos.x, nearest_worldspace_pos.z);
|
|
auto set_str = set_entry->str(a.c->version(), area);
|
|
a.c->log.info_f("Enemy found via $whatobj: E-{:03X} {} at x={:g} y={:g} z={:g}",
|
|
nearest_ene->e_id, set_str,
|
|
nearest_worldspace_pos.x, nearest_worldspace_pos.y, nearest_worldspace_pos.z);
|
|
|
|
} else if (nearest_obj) {
|
|
const auto* set_entry = nearest_obj->super_obj->version(a.c->version()).set_entry;
|
|
auto type_name = nearest_obj->type_name(a.c->version());
|
|
send_text_message_fmt(a.c, "$C5K-{:03X}\n$C6{}\n$C7X:{:.2f} Z:{:.2f}",
|
|
nearest_obj->k_id, type_name, nearest_worldspace_pos.x, nearest_worldspace_pos.z);
|
|
auto set_str = set_entry->str(a.c->version(), area);
|
|
a.c->log.info_f("Object found via $whatobj: K-{:03X} {} at x={:g} y={:g} z={:g}",
|
|
nearest_obj->k_id, set_str,
|
|
nearest_worldspace_pos.x, nearest_worldspace_pos.y, nearest_worldspace_pos.z);
|
|
|
|
} else {
|
|
throw precondition_failed("$C4No objects or\nenemies are nearby");
|
|
}
|
|
}
|
|
|
|
ChatCommandDefinition cc_whatobj(
|
|
{"$whatobj"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
whatobj_whatene_fn(a, true, false);
|
|
co_return;
|
|
});
|
|
ChatCommandDefinition cc_whatene(
|
|
{"$whatene"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
whatobj_whatene_fn(a, false, true);
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_where(
|
|
{"$where"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
uint32_t floor;
|
|
Episode episode;
|
|
auto l = a.c->lobby.lock();
|
|
if (a.c->proxy_session) {
|
|
floor = a.c->floor;
|
|
episode = a.c->proxy_session->lobby_episode;
|
|
} else if (l && l->is_game()) {
|
|
floor = a.c->floor;
|
|
episode = l->episode;
|
|
} else {
|
|
floor = 0x0F;
|
|
episode = Episode::EP1;
|
|
}
|
|
|
|
send_text_message_fmt(a.c, "$C7{:X}:{} X:{} Z:{}",
|
|
floor,
|
|
FloorDefinition::get(episode, floor).short_name,
|
|
static_cast<int32_t>(a.c->pos.x),
|
|
static_cast<int32_t>(a.c->pos.z));
|
|
|
|
if (!a.c->proxy_session && l && l->is_game()) {
|
|
for (auto lc : l->clients) {
|
|
if (lc && (lc != a.c)) {
|
|
std::string name = lc->character_file()->disp.name.decode(lc->language());
|
|
send_text_message_fmt(a.c, "$C6{}$C7 {:X}:{}",
|
|
name, lc->floor, FloorDefinition::get(l->episode, lc->floor).short_name);
|
|
}
|
|
}
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_writemem(
|
|
{"$writemem"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_debug_enabled();
|
|
|
|
auto tokens = phosg::split(a.text, ' ');
|
|
if (tokens.size() < 2) {
|
|
throw precondition_failed("Incorrect arguments");
|
|
}
|
|
|
|
uint32_t addr = std::stoul(tokens[0], nullptr, 16);
|
|
if (!console_address_in_range(a.c->version(), addr)) {
|
|
throw precondition_failed("$C4Address out of\nrange");
|
|
}
|
|
|
|
tokens.erase(tokens.begin());
|
|
std::string data = phosg::parse_data_string(phosg::join(tokens, " "));
|
|
|
|
co_await prepare_client_for_patches(a.c);
|
|
|
|
try {
|
|
auto s = a.c->require_server_state();
|
|
auto fn = s->client_functions->get("WriteMemory", a.c->specific_version);
|
|
std::unordered_map<std::string, uint32_t> label_writes{{"dest_addr", addr}, {"size", data.size()}};
|
|
co_await send_function_call(a.c, fn, label_writes, data.data(), data.size());
|
|
} catch (const std::out_of_range&) {
|
|
throw precondition_failed("Invalid patch name");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
ChatCommandDefinition cc_nativecall(
|
|
{"$nativecall"},
|
|
+[](const Args& a) -> asio::awaitable<void> {
|
|
a.check_debug_enabled();
|
|
|
|
// TODO: $nativecall is not implemented on x86 (yet) because there are multiple calling conventions used within
|
|
// the executable (at least on Xbox and BB), so we would need a way to specify which calling convention to use,
|
|
// which would be annoying
|
|
if (is_x86(a.c->version())) {
|
|
throw precondition_failed("Command not supported\non x86 clients");
|
|
}
|
|
|
|
auto tokens = phosg::split(a.text, ' ');
|
|
if (tokens.size() < 1) {
|
|
throw precondition_failed("Incorrect arguments");
|
|
}
|
|
|
|
uint32_t addr = std::stoul(tokens[0], nullptr, 16);
|
|
if (!console_address_in_range(a.c->version(), addr)) {
|
|
throw precondition_failed("$C4Function address\nout of range");
|
|
}
|
|
|
|
std::unordered_map<std::string, uint32_t> label_writes{{"call_addr", addr}};
|
|
for (size_t z = 0; z < tokens.size() - 1; z++) {
|
|
label_writes.emplace(std::format("arg{}", z), stoull(tokens[z + 1], nullptr, 16));
|
|
}
|
|
|
|
co_await prepare_client_for_patches(a.c);
|
|
|
|
try {
|
|
auto s = a.c->require_server_state();
|
|
auto fn = s->client_functions->get("CallNativeFunction", a.c->specific_version);
|
|
co_await send_function_call(a.c, fn, label_writes);
|
|
} catch (const std::out_of_range&) {
|
|
throw precondition_failed("Invalid patch name");
|
|
}
|
|
co_return;
|
|
});
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Dispatch methods
|
|
|
|
struct SplitCommand {
|
|
std::string name;
|
|
std::string args;
|
|
|
|
SplitCommand(const std::string& text) {
|
|
size_t space_pos = text.find(' ');
|
|
if (space_pos != std::string::npos) {
|
|
this->name = text.substr(0, space_pos);
|
|
this->args = text.substr(space_pos + 1);
|
|
} else {
|
|
this->name = text;
|
|
}
|
|
}
|
|
};
|
|
|
|
// This function is called every time any player sends a chat message beginning with $. It is this function's
|
|
// responsibility to see if the chat is a command, and to execute the command and block the chat if it is.
|
|
asio::awaitable<void> on_chat_command(std::shared_ptr<Client> c, const std::string& text, bool check_permissions) {
|
|
SplitCommand cmd(text);
|
|
|
|
// This function is only called by on_06 if it looks like a chat command (starts with $, or @ on 11/2000, or
|
|
// s->chat_command_sentinel if overridden), so we just normalize all commands to $ here
|
|
if (!cmd.name.empty()) {
|
|
cmd.name[0] = '$';
|
|
}
|
|
|
|
const ChatCommandDefinition* def = nullptr;
|
|
try {
|
|
def = ChatCommandDefinition::all_defs.at(cmd.name);
|
|
} catch (const std::out_of_range&) {
|
|
}
|
|
if (!def) {
|
|
send_text_message(c, "$C6Unknown command");
|
|
co_return;
|
|
}
|
|
|
|
try {
|
|
co_await def->handler(Args{cmd.args, check_permissions, c});
|
|
} catch (const precondition_failed& e) {
|
|
send_text_message(c, e.what());
|
|
} catch (const std::exception& e) {
|
|
send_text_message(c, "$C6Failed:\n" + remove_color(e.what()));
|
|
}
|
|
}
|