add Ep3 USA patch function

This commit is contained in:
Martin Michelsen
2022-10-31 16:33:56 -07:00
parent ed81599cc9
commit 0a1eb5f0d7
16 changed files with 564 additions and 111 deletions
+1
View File
@@ -64,6 +64,7 @@ Client::Client(
can_chat(true),
pending_bb_save_player_index(0),
proxy_block_events(false),
proxy_block_function_calls(false),
proxy_save_files(false),
proxy_suppress_remote_login(false),
proxy_zero_remote_guild_card(false),
+19 -15
View File
@@ -33,40 +33,43 @@ struct Client {
// Note that this flag is NOT set for Episode 3 Trial Edition clients, since
// that version is similar enough to the release version of Episode 3 that
// newserv does not have to change its behavior at all.
IS_TRIAL_EDITION = 0x2000,
IS_TRIAL_EDITION = 0x2000,
// Client is DC v1
IS_DC_V1 = 0x0010,
IS_DC_V1 = 0x0010,
// For patch server clients, client is Blue Burst rather than PC
IS_BB_PATCH = 0x0001,
IS_BB_PATCH = 0x0001,
// After joining a lobby, client will no longer send D6 commands when they
// close message boxes
NO_D6_AFTER_LOBBY = 0x0002,
NO_D6_AFTER_LOBBY = 0x0002,
// Client has the above flag and has already joined a lobby, or is not GC
NO_D6 = 0x0004,
NO_D6 = 0x0004,
// Client is Episode 3, should be able to see CARD lobbies, and should only
// be able to see/join games with the EPISODE_3_ONLY flag
IS_EPISODE_3 = 0x0008,
IS_EPISODE_3 = 0x0008,
// Client disconnects if it receives B2 (send_function_call)
NO_SEND_FUNCTION_CALL = 0x0200,
NO_SEND_FUNCTION_CALL = 0x0200,
// Client requires doubly-encrypted code section in send_function_call
ENCRYPTED_SEND_FUNCTION_CALL = 0x0800,
ENCRYPTED_SEND_FUNCTION_CALL = 0x0800,
// Client supports send_function_call but does not actually run the code
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x1000,
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x1000,
// Client is vulnerable to a buffer overflow that we can use to enable
// send_function_call
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x8000,
// Client is loading into a game
LOADING = 0x0020,
LOADING = 0x0020,
// Client is loading a quest
LOADING_QUEST = 0x0040,
LOADING_QUEST = 0x0040,
// Client is in the information menu (login server only)
IN_INFORMATION_MENU = 0x0080,
IN_INFORMATION_MENU = 0x0080,
// Client is at the welcome message (login server only)
AT_WELCOME_MESSAGE = 0x0100,
AT_WELCOME_MESSAGE = 0x0100,
// Client has already received a 97 (enable saves) command, so don't show
// the programs menu anymore
SAVE_ENABLED = 0x0400,
SAVE_ENABLED = 0x0400,
// Client has received newserv's Episode 3 card definitions, so don't send
// them again
HAS_EP3_CARD_DEFS = 0x4000,
HAS_EP3_CARD_DEFS = 0x4000,
};
uint64_t id;
@@ -121,6 +124,7 @@ struct Client {
uint8_t pending_bb_save_player_index;
bool proxy_block_events;
bool proxy_block_function_calls;
bool proxy_save_files;
bool proxy_suppress_remote_login;
bool proxy_zero_remote_guild_card;
+2 -1
View File
@@ -1513,7 +1513,8 @@ struct C_Register_BB_9C {
struct C_Login_DC_PC_GC_9D {
le_uint32_t player_tag; // 0x00010000 if guild card is set (via 04)
le_uint32_t guild_card_number; // 0xFFFFFFFF if not set
le_uint64_t unused;
le_uint32_t unused1;
le_uint32_t unused2;
le_uint32_t sub_version;
uint8_t is_extended; // If 1, structure has extended format
uint8_t language; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES
+6
View File
@@ -141,6 +141,12 @@ void populate_state_from_config(shared_ptr<ServerState> s,
s->item_tracking_enabled = true;
}
try {
s->episode_3_send_function_call_enabled = d.at("EnableEpisode3SendFunctionCall")->as_bool();
} catch (const out_of_range&) {
s->episode_3_send_function_call_enabled = false;
}
shared_ptr<JSONObject> log_levels_json;
try {
log_levels_json = d.at("LogLevels");
+4 -3
View File
@@ -59,9 +59,10 @@ namespace ProxyOptionsMenuItemID {
constexpr uint32_t INFINITE_TP = 0xAA2222AA;
constexpr uint32_t SWITCH_ASSIST = 0xAA3333AA;
constexpr uint32_t BLOCK_EVENTS = 0xAA4444AA;
constexpr uint32_t SAVE_FILES = 0xAA5555AA;
constexpr uint32_t SUPPRESS_LOGIN = 0xAA6666AA;
constexpr uint32_t SKIP_CARD = 0xAA7777AA;
constexpr uint32_t BLOCK_PATCHES = 0xAA5555AA;
constexpr uint32_t SAVE_FILES = 0xAA6666AA;
constexpr uint32_t SUPPRESS_LOGIN = 0xAA7777AA;
constexpr uint32_t SKIP_CARD = 0xAA8888AA;
}
+55 -45
View File
@@ -172,7 +172,8 @@ static HandlerResult S_G_9A(shared_ptr<ServerState>,
cmd.player_tag = 0x00010000;
cmd.guild_card_number = session.remote_guild_card_number;
}
cmd.unused = 0;
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.sub_version = session.sub_version;
cmd.is_extended = (session.remote_guild_card_number < 0) ? 0 : 1;
cmd.language = session.language;
@@ -322,7 +323,8 @@ static HandlerResult S_V123P_02_17(
cmd.player_tag = 0x00010000;
cmd.guild_card_number = session.remote_guild_card_number;
}
cmd.unused = 0;
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.sub_version = session.sub_version;
cmd.is_extended = 0;
cmd.language = session.language;
@@ -371,7 +373,8 @@ static HandlerResult S_V123P_02_17(
C_LoginExtended_GC_9E cmd;
cmd.player_tag = 0x00010000;
cmd.guild_card_number = guild_card_number;
cmd.unused = 0;
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.sub_version = session.sub_version;
cmd.is_extended = 0;
cmd.language = session.language;
@@ -936,14 +939,14 @@ static HandlerResult S_6x(shared_ptr<ServerState>,
template <typename T>
static HandlerResult S_44_A6(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) {
if (session.save_files) {
const auto& cmd = check_size_t<S_OpenFile_PC_V3_44_A6>(data);
bool is_download_quest = (command == 0xA6);
const auto& cmd = check_size_t<S_OpenFile_PC_V3_44_A6>(data);
string filename = cmd.filename;
string output_filename = string_printf("%s.%s.%" PRIu64,
string filename = cmd.filename;
string output_filename;
if (session.save_files) {
output_filename = string_printf("%s.%s.%" PRIu64,
filename.c_str(),
is_download_quest ? "download" : "online", now());
(command == 0xA6) ? "download" : "online", now());
for (size_t x = 0; x < output_filename.size(); x++) {
if (output_filename[x] < 0x20 || output_filename[x] > 0x7E || output_filename[x] == '/') {
output_filename[x] = '_';
@@ -952,12 +955,17 @@ static HandlerResult S_44_A6(shared_ptr<ServerState>,
if (output_filename[0] == '.') {
output_filename[0] = '_';
}
ProxyServer::LinkedSession::SavingFile sf(
cmd.filename, output_filename, cmd.file_size);
session.saving_files.emplace(cmd.filename, move(sf));
session.log.info("Opened file %s", output_filename.c_str());
}
ProxyServer::LinkedSession::SavingFile sf(
cmd.filename, output_filename, cmd.file_size);
session.saving_files.emplace(cmd.filename, move(sf));
if (session.save_files) {
session.log.info("Opened file %s", output_filename.c_str());
} else {
session.log.info("Tracking file %s", filename.c_str());
}
return HandlerResult::Type::FORWARD;
}
@@ -967,39 +975,41 @@ constexpr on_command_t S_B_44_A6 = &S_44_A6<S_OpenFile_BB_44_A6>;
static HandlerResult S_13_A7(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
if (session.save_files) {
const auto& cmd = check_size_t<S_WriteFile_13_A7>(data);
auto& cmd = check_size_t<S_WriteFile_13_A7>(data);
bool modified = false;
ProxyServer::LinkedSession::SavingFile* sf = nullptr;
try {
sf = &session.saving_files.at(cmd.filename);
} catch (const out_of_range&) {
string filename = cmd.filename;
session.log.warning("Received data for non-open file %s", filename.c_str());
return HandlerResult::Type::FORWARD;
}
size_t bytes_to_write = cmd.data_size;
if (bytes_to_write > 0x400) {
session.log.warning("Chunk data size is invalid; truncating to 0x400");
bytes_to_write = 0x400;
}
session.log.info("Writing %zu bytes to %s", bytes_to_write, sf->output_filename.c_str());
fwritex(sf->f.get(), cmd.data, bytes_to_write);
if (bytes_to_write > sf->remaining_bytes) {
session.log.warning("Chunk size extends beyond original file size; file may be truncated");
sf->remaining_bytes = 0;
} else {
sf->remaining_bytes -= bytes_to_write;
}
if (sf->remaining_bytes == 0) {
session.log.info("File %s is complete", sf->output_filename.c_str());
session.saving_files.erase(cmd.filename);
}
ProxyServer::LinkedSession::SavingFile* sf = nullptr;
try {
sf = &session.saving_files.at(cmd.filename);
} catch (const out_of_range&) {
string filename = cmd.filename;
session.log.warning("Received data for non-open file %s", filename.c_str());
return HandlerResult::Type::FORWARD;
}
return HandlerResult::Type::FORWARD;
if (cmd.data_size > sf->remaining_bytes) {
session.log.warning("Chunk size extends beyond original file size; truncating file");
cmd.data_size = sf->remaining_bytes;
modified = true;
} else if (cmd.data_size > 0x400) {
session.log.warning("Chunk data size is invalid; truncating to 0x400");
cmd.data_size = 0x400;
modified = true;
}
if (sf->f.get()) {
session.log.info("Writing %" PRIu32 " bytes to %s",
cmd.data_size.load(), sf->output_filename.c_str());
fwritex(sf->f.get(), cmd.data, cmd.data_size);
}
sf->remaining_bytes -= cmd.data_size;
if (sf->remaining_bytes == 0) {
session.log.info("Closing file %s", sf->output_filename.c_str());
session.saving_files.erase(cmd.filename);
}
return modified ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD;
}
static HandlerResult S_G_B7(shared_ptr<ServerState>,
+5 -2
View File
@@ -626,8 +626,11 @@ ProxyServer::LinkedSession::SavingFile::SavingFile(
uint32_t remaining_bytes)
: basename(basename),
output_filename(output_filename),
remaining_bytes(remaining_bytes),
f(fopen_unique(this->output_filename, "wb")) { }
remaining_bytes(remaining_bytes) {
if (!this->output_filename.empty()) {
this->f = fopen_unique(this->output_filename, "wb");
}
}
+3 -3
View File
@@ -659,9 +659,9 @@ string Quest::decode_gci(
} else if (header.game_id[2] == 'S') { // Episode 3
// The first 0x10 bytes in the data segment appear to be unused. In most
// files I've seen, the last half of it (8 bytes) are duplicates of the
// first 8 bytes of the unscrambled, compressed data, though this is likely
// the result of an uninitialized memory bug when the client encodes the
// file and not an actual constraint on what should be in these 8 bytes.
// first 8 bytes of the unscrambled, compressed data, though this is the
// result of an uninitialized memory bug when the client encodes the file
// and not an actual constraint on what should be in these 8 bytes.
r.skip(16);
// The game treats this field as a 16-byte string (including the \0). The 8
// bytes after it appear to be completely unused.
+36 -3
View File
@@ -67,7 +67,8 @@ static const unordered_map<uint32_t, const char16_t*> proxy_options_menu_descrip
{ProxyOptionsMenuItemID::INFINITE_HP, u"If enabled, the proxy\nwill restore your HP\nwhen you are hit by\nan enemy or trap,\nbut cannot revive\nyou from one-hit\nkills"},
{ProxyOptionsMenuItemID::INFINITE_TP, u"If enabled, the proxy\nwill restore your TP\nwhen you cast any\ntechnique"},
{ProxyOptionsMenuItemID::SWITCH_ASSIST, u"If enabled, the proxy\nwill attempt to\nunlock 2-player\ndoors when you step\non both switches\nsequentially"},
{ProxyOptionsMenuItemID::BLOCK_EVENTS, u"If enabled, season\nevents in the lobby\nand in games are\ndisabled."},
{ProxyOptionsMenuItemID::BLOCK_EVENTS, u"If enabled, seasonal\nevents in the lobby\nand in games are\ndisabled."},
{ProxyOptionsMenuItemID::BLOCK_PATCHES, u"If enabled, patches\nsent by the remote\nserver are blocked."},
{ProxyOptionsMenuItemID::SAVE_FILES, u"If enabled, the proxy\nwill save local\ncopies of files from\nthe remote server\n(quests, etc.)"},
{ProxyOptionsMenuItemID::SUPPRESS_LOGIN, u"If enabled, the proxy\nwill use an alternate\nlogin sequence"},
{ProxyOptionsMenuItemID::SKIP_CARD, u"If enabled, the proxy\nwill use an alternate\nvalue for your initial\nGuild Card"},
@@ -91,6 +92,8 @@ static vector<MenuItem> proxy_options_menu_for_client(
}
ret.emplace_back(ProxyOptionsMenuItemID::BLOCK_EVENTS,
c->proxy_block_events ? u"Block events ON" : u"Block events OFF", u"", 0);
ret.emplace_back(ProxyOptionsMenuItemID::BLOCK_PATCHES,
c->proxy_block_function_calls ? u"Block patches ON" : u"Block patches OFF", u"", 0);
ret.emplace_back(ProxyOptionsMenuItemID::SAVE_FILES,
c->proxy_save_files ? u"Save files ON" : u"Save files OFF", u"", 0);
ret.emplace_back(ProxyOptionsMenuItemID::SUPPRESS_LOGIN,
@@ -127,6 +130,9 @@ static void send_client_to_proxy_server(shared_ptr<ServerState> s, shared_ptr<Cl
if (c->proxy_block_events) {
session->override_lobby_event = 0;
}
if (c->proxy_block_function_calls) {
session->function_call_return_value = 0xFFFFFFFF;
}
if (c->proxy_zero_remote_guild_card) {
session->remote_guild_card_number = 0;
} else {
@@ -152,6 +158,20 @@ static void send_proxy_destinations_menu(shared_ptr<ServerState> s, shared_ptr<C
} catch (const out_of_range&) { }
}
static bool send_enable_send_function_call_if_applicable(
shared_ptr<ServerState> s, shared_ptr<Client> c) {
if (c->flags & Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL) {
if (s->episode_3_send_function_call_enabled) {
send_quest_buffer_overflow(s, c);
} else {
c->flags |= Client::Flag::NO_SEND_FUNCTION_CALL;
}
c->flags &= ~Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL;
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
@@ -210,6 +230,9 @@ void on_login_complete(shared_ptr<ServerState> s, shared_ptr<Client> c) {
(c->flags & Client::Flag::NO_D6) ||
!(c->flags & Client::Flag::AT_WELCOME_MESSAGE)) {
c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE;
if (send_enable_send_function_call_if_applicable(s, c)) {
send_update_client_config(c);
}
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
} else {
send_message_box(c, s->welcome_message.c_str());
@@ -637,6 +660,13 @@ static void on_login_d_e_dc_pc_v3(shared_ptr<ServerState> s, shared_ptr<Client>
}
set_console_client_flags(c, base_cmd->sub_version);
// See system/ppc/Episode3USAQuestBufferOverflow.s for where this value gets
// set. We use this to determine if the client has already run the code or
// not; sending it again when the client has already run it will likely cause
// the client to crash.
if (base_cmd->unused1 == 0x5F5CA297) {
c->flags &= ~(Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL | Client::Flag::NO_SEND_FUNCTION_CALL);
}
uint32_t serial_number = stoul(base_cmd->serial_number, nullptr, 16);
try {
@@ -687,7 +717,6 @@ static void on_login_d_e_dc_pc_v3(shared_ptr<ServerState> s, shared_ptr<Client>
}
send_update_client_config(c);
on_login_complete(s, c);
}
@@ -985,9 +1014,10 @@ static void on_message_box_closed(shared_ptr<ServerState> s, shared_ptr<Client>
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);
send_enable_send_function_call_if_applicable(s, c);
c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE;
send_update_client_config(c);
send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu);
}
}
@@ -1301,6 +1331,9 @@ static void on_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
case ProxyOptionsMenuItemID::BLOCK_EVENTS:
c->proxy_block_events = !c->proxy_block_events;
goto resend_proxy_options_menu;
case ProxyOptionsMenuItemID::BLOCK_PATCHES:
c->proxy_block_function_calls = !c->proxy_block_function_calls;
goto resend_proxy_options_menu;
case ProxyOptionsMenuItemID::SAVE_FILES:
c->proxy_save_files = !c->proxy_save_files;
goto resend_proxy_options_menu;
+68 -39
View File
@@ -243,6 +243,74 @@ void send_update_client_config(shared_ptr<Client> c) {
template <typename CommandT>
void send_quest_open_file_t(
shared_ptr<Client> c,
const string& quest_name,
const string& filename,
uint32_t file_size,
QuestFileType type) {
CommandT cmd;
uint8_t command_num;
switch (type) {
case QuestFileType::ONLINE:
command_num = 0x44;
cmd.name = "PSO/" + quest_name;
cmd.flags = 2;
break;
case QuestFileType::GBA_DEMO:
command_num = 0xA6;
cmd.name = "GBA Demo";
cmd.flags = 2;
break;
case QuestFileType::DOWNLOAD:
command_num = 0xA6;
cmd.name = "PSO/" + quest_name;
cmd.flags = 0;
break;
case QuestFileType::EPISODE_3:
command_num = 0xA6;
cmd.name = "PSO/" + quest_name;
cmd.flags = 3;
break;
default:
throw logic_error("invalid quest file type");
}
cmd.unused.clear(0);
cmd.file_size = file_size;
cmd.filename = filename.c_str();
send_command_t(c, command_num, 0x00, cmd);
}
void send_quest_buffer_overflow(
shared_ptr<ServerState> s, shared_ptr<Client> c) {
// TODO: Figure out a way to share this state across sessions. Maybe we could
// e.g. modify send_1D to send a nonzero flag value, which we could use to
// know that the client already has this patch? Or just add another command in
// the login sequence?
// PSO Episode 3 USA doesn't natively support the B2 command, but we can add
// it back to the game with some tricky commands. For details on how this
// works, see system/ppc/Episode3USAQuestBufferOverflow.s.
auto fn = s->function_code_index->name_to_function.at("Episode3USAQuestBufferOverflow");
if (fn->code.size() > 0x400) {
throw runtime_error("Episode 3 buffer overflow code must be a single segment");
}
static const string filename = "m999999p_e.bin";
send_quest_open_file_t<S_OpenFile_PC_V3_44_A6>(
c, "BufferOverflow", filename, 0x18, QuestFileType::EPISODE_3);
S_WriteFile_13_A7 cmd;
cmd.filename = filename;
memcpy(cmd.data, fn->code.data(), fn->code.size());
if (fn->code.size() < 0x400) {
memset(&cmd.data[fn->code.size()], 0, 0x400 - fn->code.size());
}
cmd.data_size = fn->code.size();
send_command_t(c, 0xA7, 0x00, cmd);
}
void send_function_call(
shared_ptr<Client> c,
shared_ptr<CompiledFunctionCode> code,
@@ -1727,45 +1795,6 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) {
template <typename CommandT>
void send_quest_open_file_t(
shared_ptr<Client> c,
const string& quest_name,
const string& filename,
uint32_t file_size,
QuestFileType type) {
CommandT cmd;
uint8_t command_num;
switch (type) {
case QuestFileType::ONLINE:
command_num = 0x44;
cmd.name = "PSO/" + quest_name;
cmd.flags = 2;
break;
case QuestFileType::GBA_DEMO:
command_num = 0xA6;
cmd.name = "GBA Demo";
cmd.flags = 2;
break;
case QuestFileType::DOWNLOAD:
command_num = 0xA6;
cmd.name = "PSO/" + quest_name;
cmd.flags = 0;
break;
case QuestFileType::EPISODE_3:
command_num = 0xA6;
cmd.name = "PSO/" + quest_name;
cmd.flags = 3;
break;
default:
throw logic_error("invalid quest file type");
}
cmd.unused.clear(0);
cmd.file_size = file_size;
cmd.filename = filename.c_str();
send_command_t(c, command_num, 0x00, cmd);
}
void send_quest_file_chunk(
shared_ptr<Client> c,
const string& filename,
+2
View File
@@ -122,6 +122,8 @@ void send_server_init(
uint8_t flags);
void send_update_client_config(std::shared_ptr<Client> c);
void send_quest_buffer_overflow(
std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void send_function_call(
std::shared_ptr<Client> c,
std::shared_ptr<CompiledFunctionCode> code,
+1
View File
@@ -22,6 +22,7 @@ ServerState::ServerState()
allow_unregistered_users(false),
allow_saving(true),
item_tracking_enabled(true),
episode_3_send_function_call_enabled(false),
run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1),
pre_lobby_event(0),
ep3_menu_song(-1) {
+1
View File
@@ -49,6 +49,7 @@ struct ServerState {
bool allow_unregistered_users;
bool allow_saving;
bool item_tracking_enabled;
bool episode_3_send_function_call_enabled;
RunShellBehavior run_shell_behavior;
std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
std::shared_ptr<const FunctionCodeIndex> function_code_index;
+3
View File
@@ -70,6 +70,9 @@ uint16_t flags_for_version(GameVersion version, int64_t sub_version) {
Client::Flag::IS_EPISODE_3 |
Client::Flag::ENCRYPTED_SEND_FUNCTION_CALL;
case 0x41: // GC Ep3 US
return Client::Flag::NO_D6_AFTER_LOBBY |
Client::Flag::IS_EPISODE_3 |
Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL;
case 0x43: // GC Ep3 EU
return Client::Flag::NO_D6_AFTER_LOBBY |
Client::Flag::IS_EPISODE_3 |