Files
psopeeps-newserv/src/ReceiveCommands.cc
T
2022-08-08 23:22:55 -07:00

2992 lines
112 KiB
C++

#include "ReceiveCommands.hh"
#include <inttypes.h>
#include <string.h>
#include <memory>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <phosg/Network.hh>
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
#include "Loggers.hh"
#include "ChatCommands.hh"
#include "FileContentsCache.hh"
#include "ProxyServer.hh"
#include "PSOProtocol.hh"
#include "ReceiveSubcommands.hh"
#include "SendCommands.hh"
#include "StaticGameData.hh"
#include "Text.hh"
using namespace std;
vector<MenuItem> quest_categories_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
});
vector<MenuItem> quest_battle_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::BATTLE), u"Battle", u"$E$C6Battle mode rule\nsets", 0),
});
vector<MenuItem> quest_challenge_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::CHALLENGE), u"Challenge", u"$E$C6Challenge mode\nquests", 0),
});
vector<MenuItem> quest_solo_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::SOLO), u"Solo Quests", u"$E$C6Quests that require\na single player", 0),
});
vector<MenuItem> quest_government_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_1), u"Hero in Red",u"$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_2), u"The Military's Hero",u"$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_4), u"The Meteor Impact Incident", u"$E$C6Quests that follow\nthe Episode 4\nstoryline", 0),
});
vector<MenuItem> quest_download_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::DOWNLOAD), u"Download", u"$E$C6Quests to download\nto your Memory Card", 0),
});
////////////////////////////////////////////////////////////////////////////////
void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c) {
switch (c->server_behavior) {
case ServerBehavior::SPLIT_RECONNECT: {
uint16_t pc_port = s->name_to_port_config.at("pc-login")->port;
uint16_t gc_port = s->name_to_port_config.at("gc-jp10")->port;
send_pc_gc_split_reconnect(c, s->connect_address_for_client(c), pc_port, gc_port);
c->should_disconnect = true;
break;
}
case ServerBehavior::LOGIN_SERVER:
send_server_init(s, c, true, false);
if (s->pre_lobby_event) {
send_change_event(c, s->pre_lobby_event);
}
break;
case ServerBehavior::PATCH_SERVER_BB:
c->flags |= Client::Flag::BB_PATCH;
[[fallthrough]];
case ServerBehavior::PATCH_SERVER_PC:
case ServerBehavior::DATA_SERVER_BB:
case ServerBehavior::LOBBY_SERVER:
send_server_init(s, c, false, false);
break;
default:
c->log.error("Unimplemented behavior: %" PRId64,
static_cast<int64_t>(c->server_behavior));
}
}
void process_login_complete(shared_ptr<ServerState> s, shared_ptr<Client> c) {
// On the BB data server, this function is called only on the last connection
// (when we should send ths ship select menu).
if ((c->server_behavior == ServerBehavior::LOGIN_SERVER) ||
(c->server_behavior == ServerBehavior::DATA_SERVER_BB)) {
// On the login server, send the ep3 updates and the main menu or welcome
// message
if (c->flags & Client::Flag::EPISODE_3) {
send_ep3_card_list_update(s, c);
send_ep3_rank_update(c);
}
// On BB, send the pre-lobby event, if set. This normally happens on the
// login server immediately after the encryption init command, but on BB we
// don't know the client's state until after we receive the login command,
// so we do it here instead.
if ((c->version == GameVersion::BB) && s->pre_lobby_event) {
send_change_event(c, s->pre_lobby_event);
}
if (s->welcome_message.empty() ||
(c->flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION) ||
!(c->flags & Client::Flag::AT_WELCOME_MESSAGE)) {
c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE;
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
} else {
send_message_box(c, s->welcome_message.c_str());
}
} else if (c->server_behavior == ServerBehavior::LOBBY_SERVER) {
if (c->version == GameVersion::BB) {
// This implicitly loads the client's account and player data
send_complete_player_bb(c);
}
send_lobby_list(c, s);
send_get_player_info(c);
}
}
void process_disconnect(shared_ptr<ServerState> s, shared_ptr<Client> c) {
// if the client was in a lobby, remove them and notify the other clients
if (c->lobby_id) {
s->remove_client_from_lobby(c);
}
// TODO: Make a timer event for each connected player that saves their data
// periodically, not only when they disconnect
// TODO: Track play time somewhere for BB players
// c->game_data.player()->disp.play_time += ((now() - c->play_time_begin) / 1000000);
// Note: The client's GameData destructor should save their player data
// shortly after this point
}
////////////////////////////////////////////////////////////////////////////////
void process_verify_license_v3(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // DB
const auto& cmd = check_size_t<C_VerifyLicense_V3_DB>(data);
c->flags |= flags_for_version(c->version, cmd.sub_version);
uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16);
try {
auto l = s->license_manager->verify_gc(serial_number, cmd.access_key,
cmd.password);
c->set_license(l);
send_command(c, 0x9A, 0x02);
} catch (const incorrect_access_key& e) {
send_command(c, 0x9A, 0x03);
c->should_disconnect = true;
return;
} catch (const incorrect_password& e) {
send_command(c, 0x9A, 0x07);
c->should_disconnect = true;
return;
} catch (const missing_license& e) {
if (!s->allow_unregistered_users) {
send_command(c, 0x9A, 0x04);
c->should_disconnect = true;
return;
} else {
auto l = LicenseManager::create_license_gc(serial_number, cmd.access_key,
cmd.password, true);
s->license_manager->add(l);
c->set_license(l);
send_command(c, 0x9A, 0x02);
}
}
}
void process_login_a_dc_pc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 9A
const auto& cmd = check_size_t<C_Login_DC_PC_V3_9A>(data);
c->flags |= flags_for_version(c->version, cmd.sub_version);
uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16);
try {
shared_ptr<const License> l;
switch (c->version) {
case GameVersion::PC:
l = s->license_manager->verify_pc(serial_number, cmd.access_key);
break;
case GameVersion::GC:
l = s->license_manager->verify_gc(serial_number, cmd.access_key);
break;
case GameVersion::XB:
throw runtime_error("xbox licenses are not implemented");
break;
default:
throw logic_error("unsupported versioned command");
}
c->set_license(l);
send_command(c, 0x9A, 0x02);
} catch (const incorrect_access_key& e) {
send_command(c, 0x9A, 0x03);
c->should_disconnect = true;
return;
} catch (const incorrect_password& e) {
send_command(c, 0x9A, 0x07);
c->should_disconnect = true;
return;
} catch (const missing_license& e) {
// On V3, the client should have sent a different command containing the
// password already, which should have created and added a temporary
// license. So, if no license exists at this point, disconnect the client
// even if unregistered clients are allowed.
shared_ptr<License> l;
if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) {
send_command(c, 0x9A, 0x04);
c->should_disconnect = true;
return;
} else if (c->version == GameVersion::PC) {
l = LicenseManager::create_license_pc(serial_number, cmd.access_key, true);
s->license_manager->add(l);
c->set_license(l);
send_command(c, 0x9A, 0x02);
} else {
throw runtime_error("unsupported game version");
}
}
}
void process_login_c_dc_pc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 9C
const auto& cmd = check_size_t<C_Register_DC_PC_V3_9C>(data);
c->flags |= flags_for_version(c->version, cmd.sub_version);
uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16);
try {
shared_ptr<const License> l;
switch (c->version) {
case GameVersion::PC:
l = s->license_manager->verify_pc(serial_number, cmd.access_key);
break;
case GameVersion::GC:
l = s->license_manager->verify_gc(serial_number, cmd.access_key,
cmd.password);
break;
case GameVersion::XB:
throw runtime_error("xbox licenses are not implemented");
break;
default:
throw logic_error("unsupported versioned command");
}
c->set_license(l);
send_command(c, 0x9C, 0x01);
} catch (const incorrect_password& e) {
send_command(c, 0x9C, 0x00);
c->should_disconnect = true;
return;
} catch (const missing_license& e) {
if (!s->allow_unregistered_users) {
send_command(c, 0x9C, 0x00);
c->should_disconnect = true;
return;
} else {
shared_ptr<License> l;
switch (c->version) {
case GameVersion::PC:
l = LicenseManager::create_license_pc(serial_number, cmd.access_key,
true);
break;
case GameVersion::GC:
l = LicenseManager::create_license_gc(serial_number, cmd.access_key,
cmd.password, true);
break;
case GameVersion::XB:
throw runtime_error("xbox licenses are not implemented");
break;
default:
throw logic_error("unsupported versioned command");
}
s->license_manager->add(l);
c->set_license(l);
send_command(c, 0x9C, 0x01);
}
}
}
void process_login_d_e_pc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t, const string& data) { // 9D 9E
// The client sends extra unused data the first time it sends these commands,
// hence the odd check_size calls here
const C_Login_PC_9D* base_cmd;
if (command == 0x9D) {
base_cmd = &check_size_t<C_Login_PC_9D>(data,
sizeof(C_Login_PC_9D), sizeof(C_LoginExtended_PC_9D));
if (base_cmd->is_extended) {
const auto& cmd = check_size_t<C_LoginExtended_PC_9D>(data);
if (cmd.extension.menu_id == MenuID::LOBBY) {
c->preferred_lobby_id = cmd.extension.preferred_lobby_id;
}
}
} else if (command == 0x9E) {
const auto& cmd = check_size_t<C_Login_GC_9E>(data,
sizeof(C_Login_GC_9E), sizeof(C_LoginExtended_GC_9E));
base_cmd = &cmd;
if (cmd.is_extended) {
const auto& cmd = check_size_t<C_LoginExtended_GC_9E>(data);
if (cmd.extension.menu_id == MenuID::LOBBY) {
c->preferred_lobby_id = cmd.extension.preferred_lobby_id;
}
}
try {
c->import_config(cmd.client_config.cfg);
} catch (const invalid_argument&) {
// If we can't import the config, assume that the client was not connected
// to newserv before, so we should show the welcome message.
c->flags |= Client::Flag::AT_WELCOME_MESSAGE;
c->bb_game_state = ClientStateBB::INITIAL_LOGIN;
c->game_data.bb_player_index = 0;
}
} else {
throw logic_error("9D/9E handler called for incorrect command");
}
c->flags |= flags_for_version(c->version, base_cmd->sub_version);
uint32_t serial_number = stoul(base_cmd->serial_number, nullptr, 16);
try {
shared_ptr<const License> l;
switch (c->version) {
case GameVersion::PC:
l = s->license_manager->verify_pc(serial_number, base_cmd->access_key);
break;
case GameVersion::GC:
l = s->license_manager->verify_gc(serial_number, base_cmd->access_key);
break;
case GameVersion::XB:
throw runtime_error("xbox licenses are not implemented");
break;
default:
throw logic_error("unsupported versioned command");
}
c->set_license(l);
} catch (const incorrect_access_key& e) {
send_command(c, 0x04, 0x03);
c->should_disconnect = true;
return;
} catch (const incorrect_password& e) {
send_command(c, 0x04, 0x06);
c->should_disconnect = true;
return;
} catch (const missing_license& e) {
// On V3, the client should have sent a different command containing the
// password already, which should have created and added a temporary
// license. So, if no license exists at this point, disconnect the client
// even if unregistered clients are allowed.
shared_ptr<License> l;
if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) {
send_command(c, 0x04, 0x04);
c->should_disconnect = true;
return;
} else if (c->version == GameVersion::PC) {
l = LicenseManager::create_license_pc(serial_number, base_cmd->access_key, true);
s->license_manager->add(l);
c->set_license(l);
} else {
throw runtime_error("unsupported game version");
}
}
if ((c->flags & Client::Flag::EPISODE_3) && (s->ep3_menu_song >= 0)) {
send_ep3_change_music(c, s->ep3_menu_song);
}
send_update_client_config(c);
process_login_complete(s, c);
}
void process_login_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 93
const auto& cmd = check_size_t<C_Login_BB_93>(data,
sizeof(C_Login_BB_93) - 8, sizeof(C_Login_BB_93));
bool is_old_format;
if (data.size() == sizeof(C_Login_BB_93) - 8) {
is_old_format = true;
} else if (data.size() == sizeof(C_Login_BB_93)) {
is_old_format = false;
} else {
throw runtime_error("invalid size for 93 command");
}
c->flags |= flags_for_version(c->version, -1);
try {
auto l = s->license_manager->verify_bb(cmd.username, cmd.password);
c->set_license(l);
} catch (const incorrect_password& e) {
u16string message = u"Login failed: " + decode_sjis(e.what());
send_message_box(c, message.c_str());
c->should_disconnect = true;
return;
} catch (const missing_license& e) {
if (!s->allow_unregistered_users) {
u16string message = u"Login failed: " + decode_sjis(e.what());
send_message_box(c, message.c_str());
c->should_disconnect = true;
return;
} else {
shared_ptr<License> l = LicenseManager::create_license_bb(
fnv1a32(cmd.username) & 0x7FFFFFFF, cmd.username, cmd.password, true);
s->license_manager->add(l);
c->set_license(l);
}
}
try {
if (is_old_format) {
c->import_config(cmd.var.old_clients_cfg.cfg);
} else {
c->import_config(cmd.var.new_clients.cfg.cfg);
}
if (c->bb_game_state < ClientStateBB::IN_GAME) {
c->bb_game_state++;
}
} catch (const invalid_argument&) {
c->bb_game_state = ClientStateBB::INITIAL_LOGIN;
c->game_data.bb_player_index = 0;
}
if (cmd.menu_id == MenuID::LOBBY) {
c->preferred_lobby_id = cmd.preferred_lobby_id;
}
send_client_init_bb(c, 0);
switch (c->bb_game_state) {
case ClientStateBB::INITIAL_LOGIN:
// On first login, send the client to the data server port
send_reconnect(c, s->connect_address_for_client(c),
s->name_to_port_config.at("bb-data1")->port);
break;
case ClientStateBB::DOWNLOAD_DATA:
case ClientStateBB::CHOOSE_PLAYER:
case ClientStateBB::SAVE_PLAYER:
// Just wait in these cases; the client will request something from us and
// the command handlers will take care of it
break;
case ClientStateBB::SHIP_SELECT:
process_login_complete(s, c);
break;
case ClientStateBB::IN_GAME:
break;
default:
throw runtime_error("invalid bb game state");
}
}
void process_return_client_config(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 9F
if (c->version == GameVersion::BB) {
const auto& cfg = check_size_t<ClientConfigBB>(data);
c->import_config(cfg);
} else {
const auto& cfg = check_size_t<ClientConfig>(data);
c->import_config(cfg);
}
}
void process_client_checksum(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 96
check_size_t<C_CharSaveInfo_V3_BB_96>(data);
send_server_time(c);
}
void process_server_time_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // B1
check_size_v(data.size(), 0);
send_server_time(c);
// The B1 command is sent in response to a 97 command, which is normally part
// of the pre-ship-select login sequence. However, newserv delays this until
// after the ship select menu so that loading a GameCube program doesn't cause
// the player's items to be deleted when they next play PSO. It's also not a
// good idea to send a 97 and 19 at the same time, because the memory card and
// BBA are on the same EXI bus on the GameCube and this seems to cause the SYN
// packet after a 19 to get dropped pretty often, which causes a delay in
// joining the lobby. This is why we delay the 19 command until the client
// responds after saving.
if (c->should_send_to_lobby_server) {
static const vector<string> version_to_port_name({
"dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "xb-lobby", "bb-lobby"});
const auto& port_name = version_to_port_name.at(static_cast<size_t>(c->version));
send_reconnect(c, s->connect_address_for_client(c),
s->name_to_port_config.at(port_name)->port);
}
}
////////////////////////////////////////////////////////////////////////////////
// Ep3 commands. Note that these commands are not at all functional. The command
// handlers that partially worked were lost in a dead hard drive, unfortunately.
void process_ep3_meseta_transaction(shared_ptr<ServerState>,
shared_ptr<Client> c, uint16_t command, uint32_t, const string& data) {
const auto& in_cmd = check_size_t<C_Meseta_GC_Ep3_BA>(data);
S_Meseta_GC_Ep3_BA out_cmd = {1000000, 1000000, in_cmd.request_token};
send_command(c, command, 0x03, &out_cmd, sizeof(out_cmd));
}
void process_ep3_menu_challenge(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t flag, const string& data) { // DC
check_size_v(data.size(), 0);
if (flag != 0) {
send_command(c, 0xDC, 0x00);
}
}
void process_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // CA
check_size_v(data.size(), 8, 0xFFFF);
const PSOSubcommand* cmds = reinterpret_cast<const PSOSubcommand*>(data.data());
auto l = s->find_lobby(c->lobby_id);
if (!l || !(l->flags & Lobby::Flag::EPISODE_3_ONLY) || !l->is_game()) {
c->should_disconnect = true;
return;
}
if (cmds[0].byte[0] != 0xB3) {
c->should_disconnect = true;
return;
}
switch (cmds[1].byte[0]) {
// phase 1: map select
case 0x40:
send_ep3_map_list(s, l);
break;
case 0x41:
send_ep3_map_data(s, l, cmds[4].dword);
break;
/*// phase 2: deck/name entry
case 0x13:
ti = FindTeam(s, c->teamID);
memcpy(&ti->ep3game, ((DWORD)c->bufferin + 0x14), 0x2AC);
CommandEp3InitChangeState(s, c, 1);
break;
case 0x1B:
ti = FindTeam(s, c->teamID);
memcpy(&ti->ep3names[*(BYTE*)((DWORD)c->bufferin + 0x24)], ((DWORD)c->bufferin + 0x14), 0x14); // NOTICE: may be 0x26 instead of 0x24
CommandEp3InitSendNames(s, c);
break;
case 0x14:
ti = FindTeam(s, c->teamID);
memcpy(&ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], ((DWORD)c->bufferin + 0x18), 0x58); // NOTICE: may be 0x16 instead of 0x14
Ep3FillHand(&ti->ep3game, &ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]);
//Ep3RollDice(&ti->ep3game, &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]);
CommandEp3InitSendDecks(s, c);
CommandEp3InitSendMapLayout(s, c);
for (x = 0, param = 0; x < 4; x++) if ((ti->ep3decks[x].clientID != 0xFFFFFFFF) && (ti->ep3names[x].clientID != 0xFF)) param++;
if (param >= ti->ep3game.numPlayers) CommandEp3InitChangeState(s, c, 3);
break;
// phase 3: hands & game states
case 0x1D:
ti = FindTeam(s, c->teamID);
Ep3ReprocessMap(&ti->ep3game);
CommandEp3SendMapData(s, c, ti->ep3game.mapID);
for (y = 0, x = 0; x < 4; x++)
{
if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue;
Ep3EquipCard(&ti->ep3game, &ti->ep3decks[x], &ti->ep3pcs[x], 0); // equip SC card
CommandEp3InitHandUpdate(s, c, x);
CommandEp3InitStatUpdate(s, c, x);
y++;
}
CommandEp3Init_B4_06(s, c, (y == 4) ? true : false);
CommandEp3InitSendMapLayout(s, c);
for (x = 0; x < 4; x++)
{
if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue;
CommandEp3Init_B4_4E(s, c, x);
CommandEp3Init_B4_4C(s, c, x);
CommandEp3Init_B4_4D(s, c, x);
CommandEp3Init_B4_4F(s, c, x);
}
CommandEp3InitSendDecks(s, c);
CommandEp3InitSendMapLayout(s, c);
for (x = 0; x < 4; x++)
{
if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue;
CommandEp3InitHandUpdate(s, c, x);
}
CommandEp3InitSendNames(s, c);
CommandEp3InitChangeState(s, c, 4);
CommandEp3Init_B4_50(s, c);
CommandEp3InitSendMapLayout(s, c);
CommandEp3Init_B4_39(s, c); // MISSING: 60 00 AC 00 B4 2A 00 00 39 56 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
CommandEp3InitBegin(s, c);
break; */
default:
c->log.error("Unknown Episode III server data request: %02X", cmds[1].byte[0]);
}
}
void process_ep3_tournament_control(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string&) { // E2
// The client will get stuck here unless we send something. An 01 (lobby
// message box) seems to get them unstuck.
send_lobby_message_box(c, u"$C6Tournaments are\nnot supported.");
// In case we ever implement this (doubtful), the flag values are:
// 00 - list tournaments
// 01 - check tournament entry status
// 02 - cancel tournament entry
// 03 - create tournament spectator team (presumably get battle list, like get team list)
// 04 - join tournament spectator team (presumably also get battle list)
}
////////////////////////////////////////////////////////////////////////////////
// menu commands
void process_message_box_closed(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D6
check_size_v(data.size(), 0);
if (c->flags & Client::Flag::IN_INFORMATION_MENU) {
send_menu(c, u"Information", MenuID::INFORMATION,
*s->information_menu_for_version(c->version));
} else if (c->flags & Client::Flag::AT_WELCOME_MESSAGE) {
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE;
send_update_client_config(c);
}
}
void process_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 09
const auto& cmd = check_size_t<C_MenuItemInfoRequest_09>(data);
switch (cmd.menu_id) {
case MenuID::MAIN:
for (const auto& item : s->main_menu) {
if (item.item_id == cmd.item_id) {
send_ship_info(c, item.description);
}
}
break;
case MenuID::INFORMATION:
if (cmd.item_id == InformationMenuItemID::GO_BACK) {
send_ship_info(c, u"Return to the\nmain menu.");
} else {
try {
// we use item_id + 1 here because "go back" is the first item
send_ship_info(c, s->information_menu_for_version(c->version)->at(cmd.item_id + 1).description.c_str());
} catch (const out_of_range&) {
send_ship_info(c, u"$C4Missing information\nmenu item");
}
}
break;
case MenuID::PROXY_DESTINATIONS:
if (cmd.item_id == ProxyDestinationsMenuItemID::GO_BACK) {
send_ship_info(c, u"Return to the\nmain menu.");
} else {
try {
const auto& menu = s->proxy_destinations_menu_for_version(c->version);
// we use item_id + 1 here because "go back" is the first item
send_ship_info(c, menu.at(cmd.item_id + 1).description.c_str());
} catch (const out_of_range&) {
send_ship_info(c, u"$C4Missing proxy\ndestination");
}
}
break;
case MenuID::QUEST_FILTER:
// Don't send anything here. The quest filter menu already has short
// descriptions included with the entries, which the client shows in the
// usual location on the screen.
break;
case MenuID::QUEST: {
if (!s->quest_index) {
send_quest_info(c, u"$C6Quests are not available.", !c->lobby_id);
break;
}
auto q = s->quest_index->get(c->version, cmd.item_id);
if (!q) {
send_quest_info(c, u"$C4Quest does not\nexist.", !c->lobby_id);
break;
}
send_quest_info(c, q->long_description.c_str(), !c->lobby_id);
break;
}
case MenuID::GAME: {
shared_ptr<Lobby> game;
try {
game = s->find_lobby(cmd.item_id);
} catch (const out_of_range& e) {
send_ship_info(c, u"$C4Game no longer\nexists.");
break;
}
if (!game->is_game()) {
send_ship_info(c, u"$C4Incorrect game ID");
} else {
string info;
for (size_t x = 0; x < game->max_clients; x++) {
const auto& game_c = game->clients[x];
if (game_c.get()) {
auto player = game_c->game_data.player();
auto name = encode_sjis(player->disp.name);
if (game->flags & Lobby::Flag::EPISODE_3_ONLY) {
info += string_printf("%zu: $C6%s$C7 L%" PRIu32 "\n",
x + 1, name.c_str(), player->disp.level + 1);
} else {
info += string_printf("%zu: $C6%s$C7 %s L%" PRIu32 "\n",
x + 1, name.c_str(),
abbreviation_for_char_class(player->disp.char_class),
player->disp.level + 1);
}
}
}
int episode = game->episode;
if (episode == 3) {
episode = 4;
} else if (episode == 0xFF) {
episode = 3;
}
string secid_str = name_for_section_id(game->section_id);
info += string_printf("Ep%d %c %s %s\n",
episode,
abbreviation_for_difficulty(game->difficulty),
abbreviation_for_game_mode(game->mode),
secid_str.c_str());
bool cheats_enabled = game->flags & Lobby::Flag::CHEATS_ENABLED;
bool locked = !game->password.empty();
if (cheats_enabled && locked) {
info += "$C4Locked$C7, $C6cheats enabled$C7\n";
} else if (cheats_enabled) {
info += "$C6Cheats enabled$C7\n";
} else if (locked) {
info += "$C4Locked$C7\n";
}
if (game->loading_quest) {
if (game->flags & Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) {
info += "$C6Quest: " + encode_sjis(game->loading_quest->name);
} else {
info += "$C4Quest: " + encode_sjis(game->loading_quest->name);
}
} else if (game->flags & Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) {
info += "$C6Quest in progress";
} else if (game->flags & Lobby::Flag::QUEST_IN_PROGRESS) {
info += "$C4Quest in progress";
}
send_ship_info(c, decode_sjis(info));
}
break;
}
case MenuID::PATCHES:
// TODO: Find a way to provide desccriptions for patches.
break;
case MenuID::PROGRAMS: {
if (cmd.item_id == ProgramsMenuItemID::GO_BACK) {
send_ship_info(c, u"Return to the\nmain menu.");
} else {
try {
auto dol = s->dol_file_index->item_id_to_file.at(cmd.item_id);
string size_str = format_size(dol->data.size());
string info = string_printf("$C6%s$C7\n%s", dol->name.c_str(), size_str.c_str());
send_ship_info(c, decode_sjis(info));
} catch (const out_of_range&) {
send_ship_info(c, u"Incorrect program ID.");
}
}
break;
}
default:
send_ship_info(c, u"Incorrect menu ID.");
break;
}
}
void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 10
bool uses_unicode = ((c->version == GameVersion::PC) || (c->version == GameVersion::BB));
uint32_t menu_id;
uint32_t item_id;
u16string password;
if (data.size() > sizeof(C_MenuSelection_10_Flag00)) {
if (uses_unicode) {
const auto& cmd = check_size_t<C_MenuSelection_PC_BB_10_Flag02>(data);
password = cmd.password;
menu_id = cmd.menu_id;
item_id = cmd.item_id;
} else {
const auto& cmd = check_size_t<C_MenuSelection_DC_V3_10_Flag02>(data);
password = decode_sjis(cmd.password);
menu_id = cmd.menu_id;
item_id = cmd.item_id;
}
} else {
const auto& cmd = check_size_t<C_MenuSelection_10_Flag00>(data);
menu_id = cmd.menu_id;
item_id = cmd.item_id;
}
switch (menu_id) {
case MenuID::MAIN: {
switch (item_id) {
case MainMenuItemID::GO_TO_LOBBY: {
c->should_send_to_lobby_server = true;
if (!(c->flags & Client::Flag::SAVE_ENABLED)) {
c->flags |= Client::Flag::SAVE_ENABLED;
send_command(c, 0x97, 0x01);
send_update_client_config(c);
} else {
static const vector<string> version_to_port_name({
"dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "xb-lobby", "bb-lobby"});
const auto& port_name = version_to_port_name.at(static_cast<size_t>(c->version));
send_reconnect(c, s->connect_address_for_client(c),
s->name_to_port_config.at(port_name)->port);
}
break;
}
case MainMenuItemID::INFORMATION:
send_menu(c, u"Information", MenuID::INFORMATION,
*s->information_menu_for_version(c->version));
c->flags |= Client::Flag::IN_INFORMATION_MENU;
break;
case MainMenuItemID::PROXY_DESTINATIONS:
send_menu(c, u"Proxy server", MenuID::PROXY_DESTINATIONS,
s->proxy_destinations_menu_for_version(c->version));
break;
case MainMenuItemID::DOWNLOAD_QUESTS:
if (c->flags & Client::Flag::EPISODE_3) {
shared_ptr<Lobby> l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr;
auto quests = s->quest_index->filter(
c->version, false, QuestCategory::EPISODE_3);
if (quests.empty()) {
send_lobby_message_box(c, u"$C6There are no quests\navailable.");
} else {
// Episode 3 has only download quests, not online quests, so this
// is always the download quest menu. (Episode 3 does actually
// have online quests, but they don't use the file download
// paradigm that all other versions use.)
send_quest_menu(c, MenuID::QUEST, quests, true);
}
} else {
send_quest_menu(c, MenuID::QUEST_FILTER, quest_download_menu, true);
}
break;
case MainMenuItemID::PATCHES:
send_menu(c, u"Patches", MenuID::PATCHES, s->function_code_index->patch_menu());
break;
case MainMenuItemID::PROGRAMS:
send_menu(c, u"Programs", MenuID::PROGRAMS, s->dol_file_index->menu());
break;
case MainMenuItemID::DISCONNECT:
c->should_disconnect = true;
break;
case MainMenuItemID::CLEAR_LICENSE:
send_command(c, 0x9A, 0x04);
c->should_disconnect = true;
break;
default:
send_message_box(c, u"Incorrect menu item ID.");
break;
}
break;
}
case MenuID::INFORMATION: {
if (item_id == InformationMenuItemID::GO_BACK) {
c->flags &= ~Client::Flag::IN_INFORMATION_MENU;
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
} else {
try {
send_message_box(c, s->information_contents->at(item_id).c_str());
} catch (const out_of_range&) {
send_message_box(c, u"$C6No such information exists.");
}
}
break;
}
case MenuID::PROXY_DESTINATIONS: {
if (item_id == ProxyDestinationsMenuItemID::GO_BACK) {
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
} else {
const pair<string, uint16_t>* dest = nullptr;
try {
dest = &s->proxy_destinations_for_version(c->version).at(item_id);
} catch (const out_of_range&) { }
if (!dest) {
send_message_box(c, u"$C6No such destination exists.");
c->should_disconnect = true;
} else {
// TODO: We can probably avoid using client config and reconnecting the
// client here; it's likely we could build a way to just directly link
// the client to the proxy server instead (would have to provide
// license/char name/etc. for remote auth)
static const vector<string> version_to_port_name({
"dc-proxy", "pc-proxy", "", "gc-proxy", "xb-proxy", "bb-proxy"});
const auto& port_name = version_to_port_name.at(static_cast<size_t>(c->version));
uint16_t local_port = s->name_to_port_config.at(port_name)->port;
c->proxy_destination_address = resolve_ipv4(dest->first);
c->proxy_destination_port = dest->second;
send_update_client_config(c);
s->proxy_server->delete_session(c->license->serial_number);
s->proxy_server->create_licensed_session(
c->license, local_port, c->version, c->export_config_bb());
send_reconnect(c, s->connect_address_for_client(c), local_port);
}
}
break;
}
case MenuID::GAME: {
auto game = s->find_lobby(item_id);
if (!game) {
send_lobby_message_box(c, u"$C6You cannot join this\ngame because it no\nlonger exists.");
break;
}
if (!game->is_game()) {
send_lobby_message_box(c, u"$C6You cannot join this\ngame because it is\nnot a game.");
break;
}
if (game->count_clients() >= game->max_clients) {
send_lobby_message_box(c, u"$C6You cannot join this\ngame because it is\nfull.");
break;
}
if ((game->version != c->version) ||
(!(game->flags & Lobby::Flag::EPISODE_3_ONLY) != !(c->flags & Client::Flag::EPISODE_3))) {
send_lobby_message_box(c, u"$C6You cannot join this\ngame because it is\nfor a different\nversion of PSO.");
break;
}
if (game->flags & Lobby::Flag::QUEST_IN_PROGRESS) {
send_lobby_message_box(c, u"$C6You cannot join this\ngame because a\nquest is already\nin progress.");
break;
}
if (game->any_client_loading()) {
send_lobby_message_box(c, u"$C6You cannot join this\ngame because\nanother player is\ncurrently loading.\nTry again soon.");
break;
}
if (game->mode == 3) {
send_lobby_message_box(c, u"$C6You cannot join this\n game because it is\na Solo Mode game.");
break;
}
if (!(c->license->privileges & Privilege::FREE_JOIN_GAMES)) {
if (!game->password.empty() && (password != game->password)) {
send_message_box(c, u"$C6Incorrect password.");
break;
}
if (c->game_data.player()->disp.level < game->min_level) {
send_message_box(c, u"$C6Your level is too\nlow to join this\ngame.");
break;
}
if (c->game_data.player()->disp.level > game->max_level) {
send_message_box(c, u"$C6Your level is too\nhigh to join this\ngame.");
break;
}
}
if (!s->change_client_lobby(c, game)) {
throw logic_error("client cannot join game after all preconditions satisfied");
}
c->flags |= Client::Flag::LOADING;
break;
}
case MenuID::QUEST_FILTER: {
if (!s->quest_index) {
send_lobby_message_box(c, u"$C6Quests are not available.");
break;
}
shared_ptr<Lobby> l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr;
auto quests = s->quest_index->filter(c->version,
c->flags & Client::Flag::DCV1,
static_cast<QuestCategory>(item_id & 0xFF));
if (quests.empty()) {
send_lobby_message_box(c, u"$C6There are no quests\navailable in that\ncategory.");
break;
}
// Hack: assume the menu to be sent is the download quest menu if the
// client is not in any lobby
send_quest_menu(c, MenuID::QUEST, quests, !c->lobby_id);
break;
}
case MenuID::QUEST: {
if (!s->quest_index) {
send_lobby_message_box(c, u"$C6Quests are not available.");
break;
}
auto q = s->quest_index->get(c->version, item_id);
if (!q) {
send_lobby_message_box(c, u"$C6Quest does not exist.");
break;
}
// If the client is not in a lobby, send the quest as a download quest.
// Otherwise, they must be in a game to load a quest.
shared_ptr<Lobby> l;
if (c->lobby_id) {
l = s->find_lobby(c->lobby_id);
if (!l->is_game()) {
send_lobby_message_box(c, u"$C6Quests cannot be loaded\nin lobbies.");
break;
}
}
bool is_ep3 = (q->episode == 0xFF);
string bin_basename = q->bin_filename();
shared_ptr<const string> bin_contents = q->bin_contents();
string dat_basename;
shared_ptr<const string> dat_contents;
if (!is_ep3) {
dat_basename = q->dat_filename();
dat_contents = q->dat_contents();
}
if (l) {
if (is_ep3 || !dat_contents) {
throw runtime_error("episode 3 quests cannot be loaded during games");
}
if (q->joinable) {
l->flags |= Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS;
} else {
l->flags |= Lobby::Flag::QUEST_IN_PROGRESS;
}
l->loading_quest = q;
for (size_t x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
// TODO: It looks like blasting all the chunks to the client at once
// can cause GC clients to crash in rare cases. Find a way to slow
// this down (perhaps by only sending each new chunk when they
// acknowledge the previous chunk with a 44 [first chunk] or 13 [later
// chunks] command).
send_quest_file(l->clients[x], bin_basename + ".bin", bin_basename,
*bin_contents, QuestFileType::ONLINE);
send_quest_file(l->clients[x], dat_basename + ".dat", dat_basename,
*dat_contents, QuestFileType::ONLINE);
// There is no such thing as command AC on PSO PC - quests just start
// immediately when they're done downloading. There are also no chunk
// acknowledgements (C->S 13 commands) like there are on GC. So, for
// PC clients, we can just not set the loading flag, since we never
// need to check/clear it later.
if (l->clients[x]->version != GameVersion::PC) {
l->clients[x]->flags |= Client::Flag::LOADING_QUEST;
}
}
} else {
string quest_name = encode_sjis(q->name);
// Episode 3 uses the download quest commands (A6/A7) but does not
// expect the server to have already encrypted the quest files, unlike
// other versions.
if (!is_ep3) {
q = q->create_download_quest();
}
send_quest_file(c, quest_name, bin_basename, *q->bin_contents(),
is_ep3 ? QuestFileType::EPISODE_3 : QuestFileType::DOWNLOAD);
if (dat_contents) {
send_quest_file(c, quest_name, dat_basename, *q->dat_contents(),
is_ep3 ? QuestFileType::EPISODE_3 : QuestFileType::DOWNLOAD);
}
}
break;
}
case MenuID::PATCHES:
if (item_id == PatchesMenuItemID::GO_BACK) {
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
} else {
if (c->flags & Client::Flag::DOES_NOT_SUPPORT_SEND_FUNCTION_CALL) {
throw runtime_error("client does not support send_function_call");
}
send_function_call(
c, s->function_code_index->menu_item_id_to_patch_function.at(item_id));
send_menu(c, u"Patches", MenuID::PATCHES, s->function_code_index->patch_menu());
}
break;
case MenuID::PROGRAMS:
if (item_id == ProgramsMenuItemID::GO_BACK) {
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
} else {
if (c->flags & Client::Flag::DOES_NOT_SUPPORT_SEND_FUNCTION_CALL) {
throw runtime_error("client does not support send_function_call");
}
c->loading_dol_file = s->dol_file_index->item_id_to_file.at(item_id);
// Send the first function call, which triggers the process of loading a
// DOL file. This function call determines the necessary base address
// for loading the file.
send_function_call(
c,
s->function_code_index->name_to_function.at("ReadMemoryWord"),
{{"address", 0x80000034}}); // ArenaHigh from GC globals
}
break;
default:
send_message_box(c, u"Incorrect menu ID.");
break;
}
}
void process_change_lobby(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 84
const auto& cmd = check_size_t<C_LobbySelection_84>(data);
if (cmd.menu_id != MenuID::LOBBY) {
send_message_box(c, u"Incorrect menu ID.");
return;
}
// If the client isn't in any lobby, then they just left a game. Ignore their
// selection and add them to any lobby with room. If they're already in a
// lobby, then they used the lobby teleporter - add them to a specific lobby.
if (c->lobby_id == 0) {
c->preferred_lobby_id = cmd.item_id;
s->add_client_to_available_lobby(c);
// If the client already is in a lobby, then they're using the lobby
// teleporter; add them to the lobby they requested or send a failure message.
} else {
shared_ptr<Lobby> new_lobby;
try {
new_lobby = s->find_lobby(cmd.item_id);
} catch (const out_of_range&) {
send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby does not\nexist.");
return;
}
if ((new_lobby->flags & Lobby::Flag::EPISODE_3_ONLY) && !(c->flags & Client::Flag::EPISODE_3)) {
send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby is for\nEpisode 3 only.");
return;
}
if (!s->change_client_lobby(c, new_lobby)) {
send_lobby_message_box(c, u"$C6Can\'t change lobby\n\n$C7The lobby is full.");
}
}
}
void process_game_list_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 08
check_size_v(data.size(), 0);
send_game_menu(c, s);
}
void process_information_menu_request_pc(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 1F
check_size_v(data.size(), 0);
send_menu(c, u"Information", MenuID::INFORMATION,
*s->information_menu_for_version(c->version), true);
}
void process_change_ship(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string&) { // A0
// The client actually sends data in this command... looks like nothing
// important (player_tag and guild_card_number are the only discernable
// things, which we already know). We intentionally don't call check_size
// here, but instead just ignore the data.
// Delete the player from the lobby they're in (but only visible to themself).
// This makes it safe to allow the player to choose download quests from the
// main menu again - if we didn't do this, they could move in the lobby after
// canceling the download quests menu, which looks really bad.
send_self_leave_notification(c);
// Sending a blank message box here works around the bug where the log window
// contents appear prepended to the next large message box.
send_message_box(c, u"");
static const vector<string> version_to_port_name({
"dc-login", "pc-login", "bb-patch", "gc-us3", "xb-login", "bb-init"});
const auto& port_name = version_to_port_name.at(static_cast<size_t>(c->version));
send_reconnect(c, s->connect_address_for_client(c),
s->name_to_port_config.at(port_name)->port);
}
void process_change_block(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t flag, const string& data) { // A1
// newserv doesn't have blocks; treat block change the same as ship change
process_change_ship(s, c, command, flag, data);
}
////////////////////////////////////////////////////////////////////////////////
// DOL loading commands
static void send_dol_file_chunk(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint32_t start_addr) {
size_t offset = start_addr - c->dol_base_addr;
if (offset >= c->loading_dol_file->data.size()) {
throw logic_error("DOL file offset beyond end of data");
}
size_t bytes_to_send = min<size_t>(0x7800, c->loading_dol_file->data.size() - offset);
string data_to_send = c->loading_dol_file->data.substr(offset, bytes_to_send);
auto fn = s->function_code_index->name_to_function.at("WriteMemory");
unordered_map<string, uint32_t> label_writes(
{{"dest_addr", start_addr}, {"size", bytes_to_send}});
send_function_call(c, fn, label_writes, data_to_send);
size_t progress_percent = ((offset + bytes_to_send) * 100) / c->loading_dol_file->data.size();
string info = string_printf("Loading $C6%s$C7\n%zu%%%% complete",
c->loading_dol_file->name.c_str(), progress_percent);
send_ship_info(c, decode_sjis(info));
}
void process_function_call_result(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t flag, const string& data) { // B3
const auto& cmd = check_size_t<C_ExecuteCodeResult_B3>(data);
if (flag == 0) {
return;
}
auto called_fn = s->function_code_index->index_to_function.at(flag);
if (c->loading_dol_file.get()) {
if (called_fn->name == "ReadMemoryWord") {
c->dol_base_addr = (cmd.return_value - c->loading_dol_file->data.size()) & (~3);
send_dol_file_chunk(s, c, c->dol_base_addr);
} else if (called_fn->name == "WriteMemory") {
if (cmd.return_value >= c->dol_base_addr + c->loading_dol_file->data.size()) {
auto fn = s->function_code_index->name_to_function.at("RunDOL");
unordered_map<string, uint32_t> label_writes(
{{"dol_base_ptr", c->dol_base_addr}});
send_function_call(c, fn, label_writes);
// The client will stop running PSO after this, so disconnect them
c->should_disconnect = true;
} else {
send_dol_file_chunk(s, c, cmd.return_value);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// Quest commands
void process_quest_list_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t flag, const string& data) { // A2
check_size_v(data.size(), 0);
if (!s->quest_index) {
send_lobby_message_box(c, u"$C6Quests are not available.");
return;
}
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game()) {
send_lobby_message_box(c, u"$C6Quests are not available\nin lobbies.");
return;
}
// In Episode 3, there are no quest categories, so skip directly to the quest
// filter menu.
if (c->flags & Client::Flag::EPISODE_3) {
send_lobby_message_box(c, u"$C6Episode 3 does not\nprovide online quests\nvia this interface.");
} else {
vector<MenuItem>* menu = nullptr;
if ((c->version == GameVersion::BB) && flag) {
menu = &quest_government_menu;
} else {
if (l->mode == 0) {
menu = &quest_categories_menu;
} else if (l->mode == 1) {
menu = &quest_battle_menu;
} else if (l->mode == 2) {
menu = &quest_challenge_menu;
} else if (l->mode == 3) {
menu = &quest_solo_menu;
} else {
throw logic_error("no quest menu available for mode");
}
}
send_quest_menu(c, MenuID::QUEST_FILTER, *menu, false);
}
}
void process_quest_barrier(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // AC
check_size_v(data.size(), 0);
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game()) {
return;
}
// If this client is NOT loading, they should not send an AC. Sending an AC to
// a client that isn't waiting to start a quest will crash the client, so we
// have to be careful not to do so.
if (!(c->flags & Client::Flag::LOADING_QUEST)) {
return;
}
c->flags &= ~Client::Flag::LOADING_QUEST;
// check if any client is still loading
// TODO: we need to handle clients disconnecting while loading. probably
// process_client_disconnect needs to check for this case or something
size_t x;
for (x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
if (l->clients[x]->flags & Client::Flag::LOADING_QUEST) {
break;
}
}
// if they're all done, start the quest
if (x == l->max_clients) {
send_command(l, 0xAC, 0x00);
}
}
void process_update_quest_statistics(shared_ptr<ServerState> s,
shared_ptr<Client> c, uint16_t, uint32_t, const string& data) { // AA
const auto& cmd = check_size_t<C_UpdateQuestStatistics_AA>(data);
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game() || !l->loading_quest.get() ||
(l->loading_quest->internal_id != cmd.quest_internal_id)) {
return;
}
S_ConfirmUpdateQuestStatistics_AB response;
response.unknown_a1 = 0x0000;
response.unknown_a2 = 0x0000;
response.request_token = cmd.request_token;
response.unknown_a3 = 0xBFFF;
send_command_t(c, 0xAB, 0x00, response);
}
void process_gba_file_request(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D7
string filename(data);
strip_trailing_zeroes(filename);
static FileContentsCache gba_file_cache(300 * 1000 * 1000);
auto f = gba_file_cache.get_or_load("system/gba/" + filename).file;
send_quest_file(c, "", filename, f->data, QuestFileType::GBA_DEMO);
}
////////////////////////////////////////////////////////////////////////////////
// player data commands
void process_player_data(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t flag, const string& data) { // 61 98
// Note: we add extra buffer on the end when checking sizes because the
// autoreply text is a variable length
switch (c->version) {
case GameVersion::PC: {
const auto& pd = check_size_t<PSOPlayerDataPC>(data,
sizeof(PSOPlayerDataPC), 0xFFFF);
c->game_data.import_player(pd);
break;
}
case GameVersion::GC:
case GameVersion::XB: {
const PSOPlayerDataV3* pd;
if (flag == 4) { // Episode 3
if (!(c->flags & Client::Flag::EPISODE_3)) {
throw runtime_error("non-Episode 3 client sent Episode 3 player data");
}
const auto* pd3 = &check_size_t<PSOPlayerDataGCEp3>(data);
c->game_data.ep3_config.reset(new Ep3Config(pd3->ep3_config));
pd = reinterpret_cast<const PSOPlayerDataV3*>(pd3);
} else {
pd = &check_size_t<PSOPlayerDataV3>(data, sizeof(PSOPlayerDataV3),
sizeof(PSOPlayerDataV3) + c->game_data.player()->auto_reply.bytes());
}
c->game_data.import_player(*pd);
break;
}
case GameVersion::BB: {
const auto& pd = check_size_t<PSOPlayerDataBB>(data, sizeof(PSOPlayerDataBB),
sizeof(PSOPlayerDataBB) + c->game_data.player()->auto_reply.bytes());
c->game_data.import_player(pd);
break;
}
default:
throw logic_error("player data command not implemented for version");
}
auto player = c->game_data.player(false);
if (player) {
string name_str = remove_language_marker(encode_sjis(player->disp.name));
c->channel.name = string_printf("C-%" PRIX64 " (%s)",
c->id, name_str.c_str());
}
// 98 should only be sent when leaving a game, and we should leave the client
// in no lobby (they will send an 84 soon afterward to choose a lobby).
if (command == 0x98) {
s->remove_client_from_lobby(c);
} else if (command == 0x61) {
if (!c->pending_bb_save_username.empty()) {
string prev_bb_username = c->game_data.bb_username;
size_t prev_bb_player_index = c->game_data.bb_player_index;
c->game_data.bb_username = c->pending_bb_save_username;
c->game_data.bb_player_index = c->pending_bb_save_player_index;
bool failure = false;
try {
c->game_data.save_player_data();
} catch (const exception& e) {
u16string buffer = u"$C6PSOBB player data could\nnot be saved:\n" + decode_sjis(e.what());
send_text_message(c, buffer.c_str());
failure = true;
}
if (!failure) {
send_text_message_printf(c,
"$C6BB player data saved\nas player %hhu for user\n%s",
static_cast<uint8_t>(c->pending_bb_save_player_index + 1),
c->pending_bb_save_username.c_str());
}
c->game_data.bb_username = prev_bb_username;
c->game_data.bb_player_index = prev_bb_player_index;
c->pending_bb_save_username.clear();
}
// We use 61 during the lobby server init sequence to trigger joining an
// available lobby
if (!c->lobby_id && (c->server_behavior == ServerBehavior::LOBBY_SERVER)) {
s->add_client_to_available_lobby(c);
}
}
}
////////////////////////////////////////////////////////////////////////////////
// subcommands
void process_game_command(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t flag, const string& data) { // 60 62 6C 6D C9 CB (C9 CB are ep3 only)
check_size_v(data.size(), 4, 0xFFFF);
auto l = s->find_lobby(c->lobby_id);
if (!l) {
return;
}
process_subcommand(s, l, c, command, flag, data);
}
////////////////////////////////////////////////////////////////////////////////
// chat commands
void process_chat_generic(shared_ptr<ServerState> s, shared_ptr<Client> c,
const u16string& text) { // 06
u16string processed_text = remove_language_marker(text);
if (processed_text.empty()) {
return;
}
auto l = s->find_lobby(c->lobby_id);
if (!l) {
return;
}
if (processed_text[0] == L'$') {
if (processed_text[1] == L'$') {
processed_text = processed_text.substr(1);
} else {
process_chat_command(s, l, c, processed_text);
return;
}
}
if (!c->can_chat) {
return;
}
for (size_t x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
send_chat_message(l->clients[x], c->license->serial_number,
c->game_data.player()->disp.name.data(), processed_text.c_str());
}
}
void process_chat_pc_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 06
const auto& cmd = check_size_t<C_Chat_06>(data, sizeof(C_Chat_06), 0xFFFF);
u16string text(cmd.text.pcbb, (data.size() - sizeof(C_Chat_06)) / sizeof(char16_t));
strip_trailing_zeroes(text);
process_chat_generic(s, c, text);
}
void process_chat_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
const auto& cmd = check_size_t<C_Chat_06>(data, sizeof(C_Chat_06), 0xFFFF);
u16string decoded_s = decode_sjis(cmd.text.dcv3, data.size() - sizeof(C_Chat_06));
process_chat_generic(s, c, decoded_s);
}
////////////////////////////////////////////////////////////////////////////////
// BB commands
void process_key_config_request_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
check_size_v(data.size(), 0);
send_team_and_key_config_bb(c);
}
void process_player_preview_request_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
const auto& cmd = check_size_t<C_PlayerPreviewRequest_BB_E3>(data);
if (c->bb_game_state == ClientStateBB::CHOOSE_PLAYER) {
c->game_data.bb_player_index = cmd.player_index;
c->bb_game_state++;
send_client_init_bb(c, 0);
send_approve_player_choice_bb(c);
} else {
if (!c->license) {
c->should_disconnect = true;
return;
}
ClientGameData temp_gd;
temp_gd.serial_number = c->license->serial_number;
temp_gd.bb_username = c->license->username;
temp_gd.bb_player_index = cmd.player_index;
try {
auto preview = temp_gd.player()->disp.to_preview();
send_player_preview_bb(c, cmd.player_index, &preview);
} catch (const exception& e) {
// Player doesn't exist
send_player_preview_bb(c, cmd.player_index, nullptr);
}
}
}
void process_client_checksum_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t command, uint32_t, const string& data) {
constexpr size_t max_count = sizeof(GuildCardFileBB::entries) / sizeof(GuildCardEntryBB);
constexpr size_t max_blocked = sizeof(GuildCardFileBB::blocked) / sizeof(GuildCardBB);
switch (command) {
case 0x01E8: { // Check guild card file checksum
const auto& cmd = check_size_t<C_GuildCardChecksum_01E8>(data);
uint32_t checksum = c->game_data.account()->guild_cards.checksum();
c->log.info("(Guild card file) Server checksum = %08" PRIX32 ", client checksum = %08" PRIX32,
checksum, cmd.checksum.load());
S_GuildCardChecksumResponse_BB_02E8 response = {
(cmd.checksum != checksum), 0};
send_command_t(c, 0x02E8, 0x00000000, response);
break;
}
case 0x03E8: // Download guild card file
check_size_v(data.size(), 0);
send_guild_card_header_bb(c);
break;
case 0x04E8: { // Add guild card
auto& new_gc = check_size_t<GuildCardBB>(data);
auto& gcf = c->game_data.account()->guild_cards;
for (size_t z = 0; z < max_count; z++) {
if (!gcf.entries[z].data.present) {
gcf.entries[z].data = new_gc;
gcf.entries[z].unknown_a1.clear(0);
c->log.info("Added guild card %" PRIu32 " at position %zu",
new_gc.guild_card_number.load(), z);
break;
}
}
break;
}
case 0x05E8: { // Delete guild card
auto& cmd = check_size_t<C_DeleteGuildCard_BB_05E8_08E8>(data);
auto& gcf = c->game_data.account()->guild_cards;
for (size_t z = 0; z < max_count; z++) {
if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) {
c->log.info("Deleted guild card %" PRIu32 " at position %zu",
cmd.guild_card_number.load(), z);
for (z = 0; z < max_count - 1; z++) {
gcf.entries[z] = gcf.entries[z + 1];
}
gcf.entries[max_count - 1].clear();
break;
}
}
break;
}
case 0x06E8: { // Update guild card
auto& new_gc = check_size_t<GuildCardBB>(data);
auto& gcf = c->game_data.account()->guild_cards;
for (size_t z = 0; z < max_count; z++) {
if (gcf.entries[z].data.guild_card_number == new_gc.guild_card_number) {
gcf.entries[z].data = new_gc;
c->log.info("Updated guild card %" PRIu32 " at position %zu",
new_gc.guild_card_number.load(), z);
}
}
break;
}
case 0x07E8: { // Add blocked user
auto& new_gc = check_size_t<GuildCardBB>(data);
auto& gcf = c->game_data.account()->guild_cards;
for (size_t z = 0; z < max_blocked; z++) {
if (!gcf.blocked[z].present) {
gcf.blocked[z] = new_gc;
c->log.info("Added blocked guild card %" PRIu32 " at position %zu",
new_gc.guild_card_number.load(), z);
// Note: The client also sends a C6 command, so we don't have to
// manually sync the actual blocked senders list here
break;
}
}
break;
}
case 0x08E8: { // Delete blocked user
auto& cmd = check_size_t<C_DeleteGuildCard_BB_05E8_08E8>(data);
auto& gcf = c->game_data.account()->guild_cards;
for (size_t z = 0; z < max_blocked; z++) {
if (gcf.blocked[z].guild_card_number == cmd.guild_card_number) {
c->log.info("Deleted blocked guild card %" PRIu32 " at position %zu",
cmd.guild_card_number.load(), z);
for (z = 0; z < max_blocked - 1; z++) {
gcf.blocked[z] = gcf.blocked[z + 1];
}
gcf.blocked[max_blocked - 1].clear();
// Note: The client also sends a C6 command, so we don't have to
// manually sync the actual blocked senders list here
break;
}
}
break;
}
case 0x09E8: { // Write comment
auto& cmd = check_size_t<C_WriteGuildCardComment_BB_09E8>(data);
auto& gcf = c->game_data.account()->guild_cards;
for (size_t z = 0; z < max_count; z++) {
if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) {
gcf.entries[z].comment = cmd.comment;
c->log.info("Updated comment on guild card %" PRIu32 " at position %zu",
cmd.guild_card_number.load(), z);
break;
}
}
break;
}
case 0x0AE8: { // Move guild card in list
auto& cmd = check_size_t<C_MoveGuildCard_BB_0AE8>(data);
auto& gcf = c->game_data.account()->guild_cards;
if (cmd.position >= max_count) {
throw invalid_argument("invalid new position");
}
size_t index;
for (index = 0; index < max_count; index++) {
if (gcf.entries[index].data.guild_card_number == cmd.guild_card_number) {
break;
}
}
if (index >= max_count) {
throw invalid_argument("player does not have requested guild card");
}
auto moved_gc = gcf.entries[index];
for (; index < cmd.position; index++) {
gcf.entries[index] = gcf.entries[index + 1];
}
for (; index > cmd.position; index--) {
gcf.entries[index] = gcf.entries[index - 1];
}
gcf.entries[index] = moved_gc;
c->log.info("Moved guild card %" PRIu32 " to position %zu",
cmd.guild_card_number.load(), index);
break;
}
default:
throw invalid_argument("invalid command");
}
}
void process_guild_card_data_request_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
const auto& cmd = check_size_t<C_GuildCardDataRequest_BB_03DC>(data);
if (cmd.cont) {
send_guild_card_chunk_bb(c, cmd.chunk_index);
}
}
void process_stream_file_request_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t command, uint32_t flag, const string& data) {
check_size_v(data.size(), 0);
if (command == 0x04EB) {
send_stream_file_index_bb(c);
} else if (command == 0x03EB) {
send_stream_file_chunk_bb(c, flag);
} else {
throw invalid_argument("unimplemented command");
}
}
void process_create_character_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
const auto& cmd = check_size_t<SC_PlayerPreview_CreateCharacter_BB_00E5>(data);
if (!c->license) {
send_message_box(c, u"$C6You are not logged in.");
return;
}
// Hack: We use the security data to indicate to the server which phase the
// client is in (download data, character select, lobby, etc.). This presents
// a problem: the client expects to get an E4 (approve player choice) command
// immediately after the E5 (create character) command, but the client also
// will disconnect immediately after it receives that command. If we send an
// E6 before the E5 to update the security data (setting the correct next
// state), then the client sends ANOTHER E5, but this time it's blank! So, to
// be able to both create characters correctly and set security data
// correctly, we need to process only the first E5, and ignore the second. We
// do this by only creating a player if the current connection has no loaded
// player data.
if (c->game_data.player(false).get()) {
return;
}
c->game_data.bb_player_index = cmd.player_index;
try {
c->game_data.create_player(cmd.preview, s->level_table);
} catch (const exception& e) {
string message = string_printf("$C6New character could not be created:\n%s", e.what());
send_message_box(c, decode_sjis(message));
return;
}
send_client_init_bb(c, 0);
send_approve_player_choice_bb(c);
}
void process_change_account_data_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t command, uint32_t, const string& data) {
const auto* cmd = reinterpret_cast<const C_UpdateAccountData_BB_ED*>(data.data());
switch (command) {
case 0x01ED:
check_size_v(data.size(), sizeof(cmd->option));
c->game_data.account()->option_flags = cmd->option;
break;
case 0x02ED:
check_size_v(data.size(), sizeof(cmd->symbol_chats));
c->game_data.account()->symbol_chats = cmd->symbol_chats;
break;
case 0x03ED:
check_size_v(data.size(), sizeof(cmd->chat_shortcuts));
c->game_data.account()->shortcuts = cmd->chat_shortcuts;
break;
case 0x04ED:
check_size_v(data.size(), sizeof(cmd->key_config));
c->game_data.account()->key_config.key_config = cmd->key_config;
break;
case 0x05ED:
check_size_v(data.size(), sizeof(cmd->pad_config));
c->game_data.account()->key_config.joystick_config = cmd->pad_config;
break;
case 0x06ED:
check_size_v(data.size(), sizeof(cmd->tech_menu));
c->game_data.player()->tech_menu_config = cmd->tech_menu;
break;
case 0x07ED:
check_size_v(data.size(), sizeof(cmd->customize));
c->game_data.player()->disp.config = cmd->customize;
break;
default:
throw invalid_argument("unknown account command");
}
}
void process_return_player_data_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 00E7
const auto& cmd = check_size_t<PlayerBB>(data);
// We only trust the player's quest data and challenge data.
c->game_data.player()->challenge_data = cmd.challenge_data;
c->game_data.player()->quest_data1 = cmd.quest_data1;
c->game_data.player()->quest_data2 = cmd.quest_data2;
}
void process_update_key_config_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
// Some clients have only a uint32_t at the end for team rewards
auto& cmd = check_size_t<KeyAndTeamConfigBB>(data,
sizeof(KeyAndTeamConfigBB) - 4, sizeof(KeyAndTeamConfigBB));
c->game_data.account()->key_config = cmd;
// TODO: We should send a response here, but I don't know which one!
}
////////////////////////////////////////////////////////////////////////////////
// Lobby commands
void process_change_arrow_color(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t flag, const string& data) { // 89
check_size_v(data.size(), 0);
c->lobby_arrow_color = flag;
auto l = s->find_lobby(c->lobby_id);
if (l) {
send_arrow_update(l);
}
}
void process_card_search(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 40
const auto& cmd = check_size_t<C_GuildCardSearch_40>(data);
try {
auto result = s->find_client(nullptr, cmd.target_guild_card_number);
auto result_lobby = s->find_lobby(result->lobby_id);
send_card_search_result(s, c, result, result_lobby);
} catch (const out_of_range&) { }
}
void process_choice_search(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string&) { // C0
// TODO: Implement choice search.
send_text_message(c, u"$C6Choice Search is\nnot supported");
}
void process_simple_mail(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 81
u16string message;
uint32_t to_guild_card_number;
if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) {
const auto& cmd = check_size_t<SC_SimpleMail_V3_81>(data);
to_guild_card_number = cmd.to_guild_card_number;
message = decode_sjis(cmd.text);
} else if (c->version == GameVersion::PC) {
const auto& cmd = check_size_t<SC_SimpleMail_PC_81>(data);
to_guild_card_number = cmd.to_guild_card_number;
message = cmd.text;
} else if (c->version == GameVersion::BB) {
const auto& cmd = check_size_t<SC_SimpleMail_BB_81>(data);
to_guild_card_number = cmd.to_guild_card_number;
message = cmd.text;
} else {
// TODO
send_text_message(c, u"$C6Simple Mail is not\nsupported yet on\nthis platform.");
return;
}
auto target = s->find_client(nullptr, to_guild_card_number);
// If the sender is blocked, don't forward the mail
for (size_t y = 0; y < 30; y++) {
if (target->game_data.account()->blocked_senders.data()[y] == c->license->serial_number) {
return;
}
}
// If the target has auto-reply enabled, send the autoreply
if (!target->game_data.player()->auto_reply.empty()) {
send_simple_mail(c, target->license->serial_number,
target->game_data.player()->disp.name,
target->game_data.player()->auto_reply);
}
// Forward the message
send_simple_mail(
target,
c->license->serial_number,
c->game_data.player()->disp.name,
message);
}
////////////////////////////////////////////////////////////////////////////////
// Info board commands
void process_info_board_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D8
check_size_v(data.size(), 0);
send_info_board(c, s->find_lobby(c->lobby_id));
}
template <typename CharT>
void process_write_info_board_t(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D9
check_size_v(data.size(), 0, c->game_data.player()->info_board.size() * sizeof(CharT));
c->game_data.player()->info_board.assign(
reinterpret_cast<const CharT*>(data.data()),
data.size() / sizeof(CharT));
}
template <typename CharT>
void process_set_auto_reply_t(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // C7
check_size_v(data.size(), 0, c->game_data.player()->auto_reply.size() * sizeof(CharT));
c->game_data.player()->auto_reply.assign(
reinterpret_cast<const CharT*>(data.data()),
data.size() / sizeof(CharT));
}
void process_disable_auto_reply(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // C8
check_size_v(data.size(), 0);
c->game_data.player()->auto_reply.clear(0);
}
void process_set_blocked_senders_list(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // C6
if (c->version == GameVersion::BB) {
const auto& cmd = check_size_t<C_SetBlockedSenders_BB_C6>(data);
c->game_data.account()->blocked_senders = cmd.blocked_senders;
} else {
const auto& cmd = check_size_t<C_SetBlockedSenders_V3_C6>(data);
c->game_data.account()->blocked_senders = cmd.blocked_senders;
}
}
////////////////////////////////////////////////////////////////////////////////
// Game commands
shared_ptr<Lobby> create_game_generic(shared_ptr<ServerState> s,
shared_ptr<Client> c, const std::u16string& name,
const std::u16string& password, uint8_t episode, uint8_t difficulty,
uint8_t battle, uint8_t challenge, uint8_t solo) {
// A player's actual level is their displayed level - 1, so the minimums for
// Episode 1 (for example) are actually 1, 20, 40, 80.
static const uint32_t default_minimum_levels[3][4] = {
{0, 19, 39, 79}, // episode 1
{0, 29, 49, 89}, // episode 2
{0, 39, 79, 109}}; // episode 4
if (episode == 0) {
episode = 0xFF;
}
if (((episode != 0xFF) && (episode > 3)) || (episode == 0)) {
throw invalid_argument("incorrect episode number");
}
bool is_ep3 = (episode == 0xFF);
if (difficulty > 3) {
throw invalid_argument("incorrect difficulty level");
}
auto current_lobby = s->find_lobby(c->lobby_id);
if (!current_lobby) {
throw invalid_argument("cannot make a game from outside any lobby");
}
uint8_t min_level = ((episode == 0xFF) ? 0 : default_minimum_levels[episode - 1][difficulty]);
if (!(c->license->privileges & Privilege::FREE_JOIN_GAMES) &&
(min_level > c->game_data.player()->disp.level)) {
throw invalid_argument("level too low for difficulty");
}
bool item_tracking_enabled = (c->version == GameVersion::BB) | s->item_tracking_enabled;
shared_ptr<Lobby> game = s->create_lobby();
game->name = name;
game->password = password;
game->version = c->version;
game->section_id = c->override_section_id >= 0
? c->override_section_id : c->game_data.player()->disp.section_id;
game->episode = episode;
game->difficulty = difficulty;
if (battle) {
game->mode = 1;
}
if (challenge) {
game->mode = 2;
}
if (solo) {
game->mode = 3;
}
if (c->override_random_seed >= 0) {
game->random_seed = c->override_random_seed;
game->random->seed(game->random_seed);
}
game->common_item_creator.reset(new CommonItemCreator(
s->common_item_data, game->random));
game->event = Lobby::game_event_for_lobby_event(current_lobby->event);
game->block = 0xFF;
game->max_clients = 4;
game->flags =
(is_ep3 ? Lobby::Flag::EPISODE_3_ONLY : 0) |
(item_tracking_enabled ? Lobby::Flag::ITEM_TRACKING_ENABLED : 0) |
Lobby::Flag::GAME;
game->min_level = min_level;
game->max_level = 0xFFFFFFFF;
if (game->version == GameVersion::BB) {
// TODO: cache these somewhere so we don't read the file every time, lolz
game->rare_item_set.reset(new RareItemSet("system/blueburst/ItemRT.rel",
game->episode - 1, game->difficulty, game->section_id));
for (size_t x = 0; x < 4; x++) {
game->next_item_id[x] = (0x00200000 * x) + 0x00010000;
}
game->next_game_item_id = 0x00810000;
auto bp_subtable = s->battle_params->get_subtable(game->mode == 3,
game->episode - 1, game->difficulty);
generate_variations(
game->variations, game->random, game->episode, game->mode == 3);
for (size_t x = 0; x < 0x10; x++) {
try {
auto file = map_data_for_variation(
game->episode,
game->mode == 3,
x,
game->variations[x * 2 + 0],
game->variations[x * 2 + 1]);
auto area_enemies = parse_map(
game->episode,
game->difficulty,
bp_subtable,
file->data.data(),
file->data.size(),
false);
game->enemies.insert(
game->enemies.end(),
area_enemies.begin(),
area_enemies.end());
c->log.info("Loaded map for area %zu (%zu entries)", x, area_enemies.size());
for (size_t z = 0; z < area_enemies.size(); z++) {
string e_str = area_enemies[z].str();
static_game_data_log.info("(Entry %zu) %s", z, e_str.c_str());
}
} catch (const exception& e) {
c->log.warning("Failed to load map for area %zu: %s", x, e.what());
}
}
if (game->enemies.empty()) {
throw runtime_error("failed to load any map data");
}
c->log.info("Loaded maps contain %zu entries overall", game->enemies.size());
} else if (is_ep3) {
game->variations.clear(0);
} else {
// In non-BB non-Ep3 games, just set the variations (we don't track enemies)
generate_variations(game->variations, game->random, game->episode, false);
}
s->change_client_lobby(c, game);
c->flags |= Client::Flag::LOADING;
return game;
}
void process_create_game_pc(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // C1
const auto& cmd = check_size_t<C_CreateGame_PC_C1>(data);
create_game_generic(s, c, cmd.name, cmd.password, 1,
cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, 0);
}
void process_create_game_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t, const string& data) { // C1 EC (EC Ep3 only)
const auto& cmd = check_size_t<C_CreateGame_DC_V3_C1_Ep3_EC>(data);
// only allow EC from Ep3 clients
bool client_is_ep3 = c->flags & Client::Flag::EPISODE_3;
if ((command == 0xEC) && !client_is_ep3) {
return;
}
uint8_t episode = cmd.episode;
if ((c->version == GameVersion::DC) || (c->version == GameVersion::PC)) {
episode = 1;
}
if (client_is_ep3) {
episode = 0xFF;
}
u16string name = decode_sjis(cmd.name);
u16string password = decode_sjis(cmd.password);
create_game_generic(s, c, name.c_str(), password.c_str(),
episode, cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, 0);
}
void process_create_game_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // C1
const auto& cmd = check_size_t<C_CreateGame_BB_C1>(data);
create_game_generic(s, c, cmd.name, cmd.password, cmd.episode,
cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, cmd.solo_mode);
}
void process_lobby_name_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 8A
check_size_v(data.size(), 0);
auto l = s->find_lobby(c->lobby_id);
if (!l) {
throw invalid_argument("client not in any lobby");
}
send_lobby_name(c, l->name.c_str());
}
void process_client_ready(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 6F
check_size_v(data.size(), 0);
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game()) {
// go home client; you're drunk
throw invalid_argument("ready command cannot be sent outside game");
}
c->flags &= (~Client::Flag::LOADING);
send_resume_game(l, c);
send_server_time(c);
// Only get player info again on BB, since on other versions the returned info
// only includes items that would be saved if the client disconnects
// unexpectedly (that is, only equipped items are included).
if (c->version == GameVersion::BB) {
send_get_player_info(c);
}
}
////////////////////////////////////////////////////////////////////////////////
// Trade window commands
void process_trade_start(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D0
auto& cmd = check_size_t<SC_TradeItems_D0_D3>(data);
if (c->game_data.pending_item_trade) {
throw runtime_error("player started a trade when one is already pending");
}
if (cmd.item_count > 0x20) {
throw runtime_error("invalid item count in trade items command");
}
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game()) {
throw runtime_error("trade command received in non-game lobby");
}
auto target_c = l->clients.at(cmd.target_client_id);
if (!target_c) {
throw runtime_error("trade command sent to missing player");
}
c->game_data.pending_item_trade.reset(new PendingItemTrade());
c->game_data.pending_item_trade->other_client_id = cmd.target_client_id;
for (size_t x = 0; x < cmd.item_count; x++) {
c->game_data.pending_item_trade->items.emplace_back(cmd.items[x]);
}
// If the other player has a pending trade as well, assume this is the second
// half of the trade sequence, and send a D1 to both clients (which should
// cause them to delete the appropriate inventory items and send D2s). If the
// other player does not have a pending trade, assume this is the first half
// of the trade sequence, and send a D1 only to the target player (to request
// its D0 command).
send_command(target_c, 0xD1, 0x00);
if (target_c->game_data.pending_item_trade) {
send_command(c, 0xD1, 0x00);
}
}
void process_trade_execute(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D2
check_size_v(data.size(), 0);
if (!c->game_data.pending_item_trade) {
throw runtime_error("player executed a trade with none pending");
}
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game()) {
throw runtime_error("trade command received in non-game lobby");
}
auto target_c = l->clients.at(c->game_data.pending_item_trade->other_client_id);
if (!target_c) {
throw runtime_error("target player is missing");
}
if (!target_c->game_data.pending_item_trade) {
throw runtime_error("player executed a trade with no other side pending");
}
c->game_data.pending_item_trade->confirmed = true;
if (target_c->game_data.pending_item_trade->confirmed) {
send_execute_item_trade(c, target_c->game_data.pending_item_trade->items);
send_execute_item_trade(target_c, c->game_data.pending_item_trade->items);
send_command(c, 0xD4, 0x01);
send_command(target_c, 0xD4, 0x01);
c->game_data.pending_item_trade.reset();
target_c->game_data.pending_item_trade.reset();
}
}
void process_trade_error(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D4
check_size_v(data.size(), 0);
// Annoyingly, if the other client disconnects at a certain point during the
// trade sequence, the client can get into a state where it sends this command
// many times in a row. To deal with this, we just do nothing if the client
// has no trade pending.
if (!c->game_data.pending_item_trade) {
return;
}
uint8_t other_client_id = c->game_data.pending_item_trade->other_client_id;
c->game_data.pending_item_trade.reset();
send_command(c, 0xD4, 0x00);
// Cancel the other side of the trade too, if it's open
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game()) {
throw runtime_error("trade command received in non-game lobby");
}
auto target_c = l->clients.at(other_client_id);
if (!target_c) {
return;
}
if (!target_c->game_data.pending_item_trade) {
return;
}
target_c->game_data.pending_item_trade.reset();
send_command(target_c, 0xD4, 0x00);
}
////////////////////////////////////////////////////////////////////////////////
// Team commands
void process_team_command_bb(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t command, uint32_t, const string&) { // EA
if (command == 0x01EA) {
send_lobby_message_box(c, u"$C6Teams are not supported.");
} else if (command == 0x14EA) {
// Do nothing (for now)
} else {
throw invalid_argument("unimplemented team command");
}
}
////////////////////////////////////////////////////////////////////////////////
// Patch server commands
void process_encryption_ok_patch(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
check_size_v(data.size(), 0);
send_command(c, 0x04, 0x00); // This requests the user's login information
}
static void change_to_directory_patch(
shared_ptr<Client> c,
vector<string>& client_path_directories,
const vector<string>& file_path_directories) {
// First, exit all leaf directories that don't match the desired path
while (!client_path_directories.empty() &&
((client_path_directories.size() > file_path_directories.size()) ||
(client_path_directories.back() != file_path_directories[client_path_directories.size() - 1]))) {
send_command(c, 0x0A, 0x00);
client_path_directories.pop_back();
}
// At this point, client_path_directories should be a prefix of
// file_path_directories (or should match exactly)
if (client_path_directories.size() > file_path_directories.size()) {
throw logic_error("did not exit all necessary directories");
}
for (size_t x = 0; x < client_path_directories.size(); x++) {
if (client_path_directories[x] != file_path_directories[x]) {
throw logic_error("intermediate path is not a prefix of final path");
}
}
// Second, enter all necessary leaf directories
while (client_path_directories.size() < file_path_directories.size()) {
const string& dir = file_path_directories[client_path_directories.size()];
send_enter_directory_patch(c, dir);
client_path_directories.emplace_back(dir);
}
}
void process_login_patch(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) {
check_size_v(data.size(), sizeof(C_Login_Patch_04));
// On BB we can use colors and newlines should be \n; on PC we can't use
// colors, the text is auto-word-wrapped, and newlines should be \r\n.
const u16string& message = (c->flags & Client::Flag::BB_PATCH)
? s->bb_patch_server_message : s->pc_patch_server_message;
if (!message.empty()) {
send_message_box(c, message.c_str());
}
auto index = (c->flags & Client::Flag::BB_PATCH) ?
s->bb_patch_file_index : s->pc_patch_file_index;
if (index.get()) {
send_command(c, 0x0B, 0x00); // Start patch session; go to root directory
vector<string> path_directories;
for (const auto& file : index->files) {
change_to_directory_patch(c, path_directories, file->path_directories);
S_FileChecksumRequest_Patch_0C req = {
c->patch_file_checksum_requests.size(), file->name};
send_command_t(c, 0x0C, 0x00, req);
c->patch_file_checksum_requests.emplace_back(file);
}
change_to_directory_patch(c, path_directories, {});
send_command(c, 0x0D, 0x00); // End of checksum requests
} else {
// No patch index present: just do something that will satisfy the client
// without actually checking or downloading any files
send_enter_directory_patch(c, ".");
send_enter_directory_patch(c, "data");
send_enter_directory_patch(c, "scene");
send_command(c, 0x0A, 0x00);
send_command(c, 0x0A, 0x00);
send_command(c, 0x0A, 0x00);
send_command(c, 0x12, 0x00);
}
}
void process_file_checksum_result_patch(shared_ptr<ServerState>,
shared_ptr<Client> c, uint16_t, uint32_t, const string& data) { // 0F
auto& cmd = check_size_t<C_FileInformation_Patch_0F>(data);
auto& req = c->patch_file_checksum_requests.at(cmd.request_id);
req.crc32 = cmd.checksum;
req.size = cmd.size;
req.response_received = true;
}
void process_file_checksum_results_done_patch(shared_ptr<ServerState>,
shared_ptr<Client> c, uint16_t, uint32_t, const string&) { // 10
S_StartFileDownloads_Patch_11 start_cmd = {0, 0};
for (const auto& req : c->patch_file_checksum_requests) {
if (!req.response_received) {
throw runtime_error("client did not respond to checksum request");
}
if (req.needs_update()) {
start_cmd.total_bytes += req.file->size;
start_cmd.num_files++;
}
}
if (start_cmd.num_files) {
send_command_t(c, 0x11, 0x00, start_cmd);
vector<string> path_directories;
for (const auto& req : c->patch_file_checksum_requests) {
if (req.needs_update()) {
change_to_directory_patch(c, path_directories, req.file->path_directories);
send_patch_file(c, req.file);
}
}
change_to_directory_patch(c, path_directories, {});
}
send_command(c, 0x12, 0x00);
}
////////////////////////////////////////////////////////////////////////////////
// Command pointer arrays
void process_ignored_command(shared_ptr<ServerState>, shared_ptr<Client>,
uint16_t, uint32_t, const string&) { }
void process_unimplemented_command(shared_ptr<ServerState>,
shared_ptr<Client> c, uint16_t command, uint32_t flag, const string& data) {
c->log.warning("Unknown command: size=%04zX command=%04hX flag=%08" PRIX32 "\n",
data.size(), command, flag);
throw invalid_argument("unimplemented command");
}
typedef void (*process_command_t)(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t flag, const string& data);
// The entries in these arrays correspond to the ID of the command received. For
// instance, if a command 6C is received, the function at position 0x6C in the
// array corresponding to the client's version is called.
static process_command_t dc_handlers[0x100] = {
// 00
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_chat_dc_v3, nullptr,
process_game_list_request, process_menu_item_info_request, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 10
process_menu_selection, nullptr, nullptr, process_ignored_command,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, nullptr, nullptr,
// 20
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 30
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 40
process_card_search, nullptr, nullptr, nullptr,
process_ignored_command, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 50
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 60
process_game_command, nullptr, process_game_command, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
process_game_command, process_game_command, nullptr, process_client_ready,
// 70
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 80
nullptr, process_simple_mail, nullptr, nullptr,
process_change_lobby, nullptr, nullptr, nullptr,
nullptr, process_change_arrow_color, process_lobby_name_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 90
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, process_client_checksum, nullptr,
process_player_data, process_ignored_command, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// A0
process_change_ship, process_change_block, process_quest_list_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_update_quest_statistics, nullptr,
nullptr, nullptr, nullptr, nullptr,
// B0
nullptr, process_server_time_request, nullptr, process_function_call_result,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// C0
nullptr, process_create_game_dc_v3, nullptr, nullptr,
nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t<char>,
process_disable_auto_reply, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// D0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
process_info_board_request, process_write_info_board_t<char>, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// E0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// F0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
};
static process_command_t pc_handlers[0x100] = {
// 00
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_chat_pc_bb, nullptr,
process_game_list_request, process_menu_item_info_request, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 10
process_menu_selection, nullptr, nullptr, process_ignored_command,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, nullptr, process_information_menu_request_pc,
// 20
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 30
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 40
process_card_search, nullptr, nullptr, nullptr,
process_ignored_command, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 50
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 60
process_game_command, process_player_data, process_game_command, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
process_game_command, process_game_command, nullptr, process_client_ready,
// 70
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 80
nullptr, process_simple_mail, nullptr, nullptr,
process_change_lobby, nullptr, nullptr, nullptr,
nullptr, process_change_arrow_color, process_lobby_name_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 90
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, process_client_checksum, nullptr,
process_player_data, process_ignored_command, process_login_a_dc_pc_v3, nullptr,
process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, nullptr,
// A0
process_change_ship, process_change_block, process_quest_list_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_update_quest_statistics, nullptr,
nullptr, nullptr, nullptr, nullptr,
// B0
nullptr, process_server_time_request, nullptr, process_function_call_result,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// C0
nullptr, process_create_game_pc, nullptr, nullptr,
nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t<char16_t>,
process_disable_auto_reply, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// D0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
process_info_board_request, process_write_info_board_t<char16_t>, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// E0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// F0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
};
static process_command_t gc_handlers[0x100] = {
// 00
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_chat_dc_v3, nullptr,
process_game_list_request, process_menu_item_info_request, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 10
process_menu_selection, nullptr, nullptr, process_ignored_command,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, nullptr, nullptr,
// 20
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 30
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 40
process_card_search, nullptr, nullptr, nullptr,
process_ignored_command, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 50
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 60
process_game_command, process_player_data, process_game_command, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
process_game_command, process_game_command, nullptr, process_client_ready,
// 70
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 80
nullptr, process_simple_mail, nullptr, nullptr,
process_change_lobby, nullptr, nullptr, nullptr,
nullptr, process_change_arrow_color, process_lobby_name_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 90
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, process_client_checksum, nullptr,
process_player_data, process_ignored_command, nullptr, nullptr,
process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, process_return_client_config,
// A0
process_change_ship, process_change_block, process_quest_list_request, nullptr,
nullptr, nullptr, process_ignored_command, process_ignored_command,
nullptr, process_ignored_command, process_update_quest_statistics, nullptr,
process_quest_barrier, nullptr, nullptr, nullptr,
// B0
nullptr, process_server_time_request, nullptr, process_function_call_result,
nullptr, nullptr, nullptr, process_ignored_command,
process_ignored_command, nullptr, process_ep3_meseta_transaction, nullptr,
nullptr, nullptr, nullptr, nullptr,
// C0
process_choice_search, process_create_game_dc_v3, nullptr, nullptr,
nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t<char>,
process_disable_auto_reply, process_game_command, process_ep3_server_data_request, process_game_command,
nullptr, nullptr, nullptr, nullptr,
// D0
process_trade_start, nullptr, process_trade_execute, nullptr,
process_trade_error, nullptr, process_message_box_closed, process_gba_file_request,
process_info_board_request, process_write_info_board_t<char>, nullptr, process_verify_license_v3,
process_ep3_menu_challenge, nullptr, nullptr, nullptr,
// E0
nullptr, nullptr, process_ep3_tournament_control, nullptr,
process_ignored_command, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
process_create_game_dc_v3, nullptr, nullptr, nullptr,
// F0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
};
static process_command_t xb_handlers[0x100] = {
// 00
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_chat_dc_v3, nullptr,
process_game_list_request, process_menu_item_info_request, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 10
process_menu_selection, nullptr, nullptr, process_ignored_command,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, nullptr, nullptr,
// 20
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 30
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 40
process_card_search, nullptr, nullptr, nullptr,
process_ignored_command, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 50
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 60
process_game_command, process_player_data, process_game_command, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
process_game_command, process_game_command, nullptr, process_client_ready,
// 70
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 80
nullptr, process_simple_mail, nullptr, nullptr,
process_change_lobby, nullptr, nullptr, nullptr,
nullptr, process_change_arrow_color, process_lobby_name_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 90
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, process_client_checksum, nullptr,
process_player_data, process_ignored_command, nullptr, nullptr,
process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, process_return_client_config,
// A0
process_change_ship, process_change_block, process_quest_list_request, nullptr,
nullptr, nullptr, process_ignored_command, process_ignored_command,
nullptr, process_ignored_command, process_update_quest_statistics, nullptr,
process_quest_barrier, nullptr, nullptr, nullptr,
// B0
nullptr, process_server_time_request, nullptr, process_function_call_result,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// C0
process_choice_search, process_create_game_dc_v3, nullptr, nullptr,
nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t<char>,
process_disable_auto_reply, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// D0
process_trade_start, nullptr, process_trade_execute, nullptr,
process_trade_error, nullptr, process_message_box_closed, process_gba_file_request,
process_info_board_request, process_write_info_board_t<char>, nullptr, process_verify_license_v3,
nullptr, nullptr, nullptr, nullptr,
// E0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// F0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
};
static process_command_t bb_handlers[0x100] = {
// 00
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_chat_pc_bb, nullptr,
process_game_list_request, process_menu_item_info_request, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 10
process_menu_selection, nullptr, nullptr, process_ignored_command,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, nullptr, nullptr,
// 20
nullptr, nullptr, process_ignored_command, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 30
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 40
process_card_search, nullptr, nullptr, nullptr,
process_ignored_command, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 50
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 60
process_game_command, process_player_data, process_game_command, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
process_game_command, process_game_command, nullptr, process_client_ready,
// 70
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 80
nullptr, process_simple_mail, nullptr, nullptr,
process_change_lobby, nullptr, nullptr, nullptr,
nullptr, process_change_arrow_color, process_lobby_name_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 90
nullptr, nullptr, nullptr, process_login_bb,
nullptr, nullptr, nullptr, nullptr,
process_player_data, process_ignored_command, nullptr, nullptr,
nullptr, nullptr, nullptr, process_return_client_config,
// A0
process_change_ship, process_change_block, process_quest_list_request, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, process_ignored_command, process_update_quest_statistics, nullptr,
process_quest_barrier, nullptr, nullptr, nullptr,
// B0
nullptr, nullptr, nullptr, process_function_call_result,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// C0
nullptr, process_create_game_bb, nullptr, nullptr,
nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t<char16_t>,
process_disable_auto_reply, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// D0
process_trade_start, nullptr, process_trade_execute, nullptr,
process_trade_error, nullptr, nullptr, nullptr,
process_info_board_request, process_write_info_board_t<char16_t>, nullptr, nullptr,
process_guild_card_data_request_bb, nullptr, nullptr, nullptr,
// E0
process_key_config_request_bb, nullptr, process_update_key_config_bb, process_player_preview_request_bb,
nullptr, process_create_character_bb, nullptr, process_return_player_data_bb,
process_client_checksum_bb, nullptr, process_team_command_bb, process_stream_file_request_bb,
process_ignored_command, process_change_account_data_bb, nullptr, nullptr,
// F0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
};
static process_command_t patch_handlers[0x100] = {
// 00
nullptr, nullptr, process_encryption_ok_patch, nullptr,
process_login_patch, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, process_file_checksum_result_patch,
// 10
process_file_checksum_results_done_patch, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
// 20
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 30
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 40
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 50
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 60
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 70
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 80
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// 90
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// A0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// B0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// C0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// D0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// E0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
// F0
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
};
static process_command_t* handlers[6] = {
dc_handlers, pc_handlers, patch_handlers, gc_handlers, xb_handlers, bb_handlers};
void process_command(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t flag, const string& data) {
string encoded_name;
auto player = c->game_data.player(false);
if (player) {
encoded_name = remove_language_marker(encode_sjis(player->disp.name));
}
auto fn = handlers[static_cast<size_t>(c->version)][command & 0xFF];
if (fn) {
fn(s, c, command, flag, data);
} else {
process_unimplemented_command(s, c, command, flag, data);
}
}