diff --git a/README.md b/README.md index 2e641b82..b3ddfac2 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Current known issues / missing features: - Test all the communication features (info board, simple mail, card search, etc.) - The trade window isn't implemented yet. - PSO PC and PSOBB are not well-tested and likely will disconnect when clients try to use unimplemented features. Only GC is known to be stable and mostly complete. +- Patches currently are platform-specific but not version-specific. This makes them quite a bit harder to use properly. +- Find a way to silence audio in RunDOL.s. Some old DOLs don't reset audio systems at load time and it's annoying to hear the crash buzz when the GC hasn't actually crashed. ## Usage @@ -26,7 +28,8 @@ There is a probably-not-too-old macOS release on the newserv GitHub repository ( If you're running Linux or want to build newserv yourself, here's what you do: 1. Make sure you have CMake and libevent installed. (`brew install cmake libevent` on macOS, `sudo apt-get install cmake libevent-dev` on most Linuxes) 2. Build and install phosg (https://github.com/fuzziqersoftware/phosg). -3. Run `cmake . && make` on the newserv directory. +3. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this. +4. Run `cmake . && make` on the newserv directory. After building newserv or downloading a release, do this to set it up and use it: 1. In the system/ directory, make a copy of config.example.json named config.json, and edit it appropriately. @@ -46,7 +49,7 @@ Standard quest file names should be like `q###-CATEGORY-VERSION.EXT`; battle que There are multiple PSO quest formats out there; newserv supports most of them. Specifically, newserv can use quests in any of the following formats: - bin/dat format: These quests consist of two files with the same base name, a .bin file and a .dat file. - Unencrypted GCI format: These quests also consist of a .bin and .dat file, but an encoding is applied on top of them. The filenames should end in .bin.gci and .dat.gci. (Note that there also exists an encrypted GCI format, which newserv does not support.) -- Encrypted DLQ format: These quests also consist of a .bin and .dat file, but downlaod quest encryption is applied on top of them. The filenames should end in .bin.dlq and .dat.dlq. +- Encrypted DLQ format: These quests also consist of a .bin and .dat file, but download quest encryption is applied on top of them. The filenames should end in .bin.dlq and .dat.dlq. - QST format: These quests consist of only a .qst file, which contains both the .bin and .dat files within it. When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats. @@ -55,6 +58,16 @@ If you've changed the contents of the quests directory, you can re-index the que All quests, including those originally in GCI or DLQ format, are treated as online quests unless their filenames specify the dl category. newserv allows players to download all quests, even those in non-download categories. +### Patches and DOL files + +Everything in this section requires resource_dasm to be installed, so newserv can use the PowerPC assembler and disassembler from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available. + +You can put patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Patches are written in PowerPC assembly and are compiled when newserv is started. See system/ppc/WriteMemory.s for a commented example of such a function. + +You can also put DOL files in the system/dol directory, and they will appear in the Programs menu. Selecting a DOL file there will load the file into their GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.s, WriteMemory.s, and RunDOL.s must be present in the system/ppc directory. This has been tested on Dolphin but not on a real GameCube, so results may vary. + +I mainly built the DOL loading functionality for documentation purposes. By now, there are many better ways to load homebrew code on an unmodified GameCube, but to my knowledge there isn't another open-source implementation of this method in existence. + ### Chat commands The server's shell supports a variety of administration commands. If the interactive shell is enabled, you can enter these commands at any time, even if the prompt isn't visible. Run `help` in the server's shell to see all of the commands and how to use them. diff --git a/src/Client.cc b/src/Client.cc index fd5c6648..8ff4e93a 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -45,7 +45,9 @@ Client::Client( infinite_hp(false), infinite_tp(false), switch_assist(false), - can_chat(true) { + can_chat(true), + pending_bb_save_player_index(0), + dol_base_addr(0) { this->last_switch_enabled_command.subcommand = 0; int fd = bufferevent_getfd(this->bev); if (fd < 0) { diff --git a/src/Client.hh b/src/Client.hh index e144189f..aad3d662 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -4,13 +4,13 @@ #include +#include "CommandFormats.hh" +#include "FunctionCompiler.hh" #include "License.hh" #include "Player.hh" #include "PSOEncryption.hh" - -#include "Text.hh" #include "PSOProtocol.hh" -#include "CommandFormats.hh" +#include "Text.hh" @@ -41,15 +41,16 @@ struct Client { IN_INFORMATION_MENU = 0x0080, // Client is at the welcome message (login server only) AT_WELCOME_MESSAGE = 0x0100, + // Client disconnect if it receives B2 (send_function_call) + DOES_NOT_SUPPORT_SEND_FUNCTION_CALL = 0x0200, - // Note: There isn't a good way to detect Episode 3 until the player data is - // sent (via a 61 command), so the IS_EPISODE_3 flag is set in that handler - DEFAULT_V1 = DCV1, + // TODO: Do DCv1 and PC support send_function_call? Here we assume they don't + DEFAULT_V1 = DCV1 | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, DEFAULT_V2_DC = 0x0000, - DEFAULT_V2_PC = NO_MESSAGE_BOX_CLOSE_CONFIRMATION, + DEFAULT_V2_PC = NO_MESSAGE_BOX_CLOSE_CONFIRMATION | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, DEFAULT_V3_GC = 0x0000, - DEFAULT_V3_GC_PLUS = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN, - DEFAULT_V3_GC_EP3 = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | EPISODE_3, + DEFAULT_V3_GC_PLUS = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, + DEFAULT_V3_GC_EP3 = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | EPISODE_3 | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, DEFAULT_V4_BB = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | NO_MESSAGE_BOX_CLOSE_CONFIRMATION, }; @@ -104,6 +105,10 @@ struct Client { std::string pending_bb_save_username; uint8_t pending_bb_save_player_index; + // DOL file loading state + uint32_t dol_base_addr; + std::shared_ptr loading_dol_file; + Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior); diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 5eb3cfb4..cdf4f010 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -520,7 +520,10 @@ struct S_ReconnectSplit_19 { // 1E: Invalid command // 1F (S->C): Information menu -// Same format and usage as 07 command +// Same format and usage as 07 command, except: +// - The menu title will say "Information" instead of "Ship Select". +// - There is no way to request information before selecting a menu item (the +// client will not send 09 commands). // 20: Invalid command @@ -1306,7 +1309,7 @@ struct S_ExecuteCode_Footer_BB_B2 : S_ExecuteCode_Footer_B2 { }; // B3 (C->S): Execute code and/or checksum memory result // GC v1.0/v1.1 and BB only. -struct C_ExecuteCodeResult_GC_BB_B3 { +struct C_ExecuteCodeResult_B3 { le_uint32_t return_value; // 0 if no code was run le_uint32_t checksum; // 0 if no checksum was computed }; diff --git a/src/FunctionCompiler.cc b/src/FunctionCompiler.cc index 268b1508..51b966f3 100644 --- a/src/FunctionCompiler.cc +++ b/src/FunctionCompiler.cc @@ -26,9 +26,9 @@ bool function_compiler_available() { -std::string CompiledFunctionCode::generate_client_command( - const std::unordered_map& label_writes, - const std::string& suffix) const { +string CompiledFunctionCode::generate_client_command( + const unordered_map& label_writes, + const string& suffix) const { S_ExecuteCode_Footer_GC_B2 footer; footer.num_relocations = this->relocation_deltas.size(); footer.unused1.clear(); @@ -66,18 +66,39 @@ std::string CompiledFunctionCode::generate_client_command( return move(w.str()); } -shared_ptr compile_function_code(const std::string& text) { +shared_ptr compile_function_code( + const string& directory, const string& name, const string& text) { #ifndef HAVE_RESOURCE_FILE + (void)directory; + (void)name; (void)text; throw runtime_error("PowerPC assembler is not available"); #else - auto assembled = PPC32Emulator::assemble(text); + std::unordered_set get_include_stack; // For mutual recursion detection + function get_include = [&](const string& name) -> string { + if (!get_include_stack.emplace(name).second) { + throw runtime_error("mutual recursion between includes"); + } + + string filename = directory + "/" + name + ".inc.s"; + if (isfile(filename)) { + return PPC32Emulator::assemble(load_file(filename), get_include).code; + } + filename = directory + "/" + name + ".inc.bin"; + if (isfile(filename)) { + return load_file(filename); + } + throw runtime_error("data not found for include " + name); + }; shared_ptr ret(new CompiledFunctionCode()); + ret->name = name; + ret->index = 0; + + auto assembled = PPC32Emulator::assemble(text, get_include); ret->code = move(assembled.code); ret->label_offsets = move(assembled.label_offsets); - ret->index = 0xFF; set reloc_indexes; for (const auto& it : ret->label_offsets) { @@ -110,32 +131,101 @@ shared_ptr compile_function_code(const std::string& text) -FunctionCodeIndex::FunctionCodeIndex(const std::string& directory) { - this->index_to_function.resize(0x100); - +FunctionCodeIndex::FunctionCodeIndex(const string& directory) { if (!function_compiler_available()) { log(INFO, "Function compiler is not available"); return; } + uint32_t next_menu_item_id = 0; for (const auto& filename : list_directory(directory)) { - if (!ends_with(filename, ".s")) { + if (!ends_with(filename, ".s") || ends_with(filename, ".inc.s")) { continue; } - string name = filename.substr(0, filename.size() - 2); + bool is_patch = ends_with(filename, ".patch.s"); + string name = filename.substr(0, filename.size() - (is_patch ? 8 : 2)); try { string path = directory + "/" + filename; string text = load_file(path); - auto code = compile_function_code(text); - if (code->index < 0xFF) { - this->index_to_function.at(code->index) = code; + auto code = compile_function_code(directory, name, text); + if (code->index != 0) { + if (!this->index_to_function.emplace(code->index, code).second) { + throw runtime_error(string_printf( + "duplicate function index: %08" PRIX32, code->index)); + } } this->name_to_function.emplace(name, code); - log(WARNING, "Compiled function %s", name.c_str()); + if (is_patch) { + this->menu_item_id_to_patch_function.emplace(next_menu_item_id++, code); + this->name_to_patch_function.emplace(name, code); + } + if (code->index) { + log(INFO, "Compiled function %02X => %s", code->index, name.c_str()); + } else { + log(INFO, "Compiled function %s", name.c_str()); + } } catch (const exception& e) { log(WARNING, "Failed to compile function %s: %s", name.c_str(), e.what()); } } } + +vector FunctionCodeIndex::patch_menu() const { + vector ret; + ret.emplace_back(PatchesMenuItemID::GO_BACK, u"Go back", u"", 0); + for (const auto& it : this->name_to_patch_function) { + const auto& fn = it.second; + ret.emplace_back(fn->menu_item_id, decode_sjis(fn->name), u"", + MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + } + return ret; +} + + + +DOLFileIndex::DOLFileIndex(const string& directory) { + if (!function_compiler_available()) { + log(INFO, "Function compiler is not available"); + return; + } + if (!isdir(directory)) { + log(INFO, "DOL file directory is missing"); + return; + } + + uint32_t next_menu_item_id = 0; + for (const auto& filename : list_directory(directory)) { + if (!ends_with(filename, ".dol")) { + continue; + } + string name = filename.substr(0, filename.size() - 4); + + try { + shared_ptr dol(new DOLFile()); + dol->menu_item_id = next_menu_item_id++; + dol->name = name; + + string path = directory + "/" + filename; + dol->data = load_file(path); + + this->name_to_file.emplace(dol->name, dol); + this->item_id_to_file.emplace_back(dol); + log(WARNING, "Loaded DOL file %s", filename.c_str()); + + } catch (const exception& e) { + log(WARNING, "Failed to load DOL file %s: %s", filename.c_str(), e.what()); + } + } +} + +vector DOLFileIndex::menu() const { + vector ret; + ret.emplace_back(ProgramsMenuItemID::GO_BACK, u"Go back", u"", 0); + for (const auto& dol : this->item_id_to_file) { + ret.emplace_back(dol->menu_item_id, decode_sjis(dol->name), u"", + MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + } + return ret; +} diff --git a/src/FunctionCompiler.hh b/src/FunctionCompiler.hh index 6af9bc79..1f83f553 100644 --- a/src/FunctionCompiler.hh +++ b/src/FunctionCompiler.hh @@ -4,9 +4,12 @@ #include #include +#include #include #include +#include "Menu.hh" + bool function_compiler_available(); @@ -21,14 +24,19 @@ struct CompiledFunctionCode { std::vector relocation_deltas; std::unordered_map label_offsets; uint32_t entrypoint_offset_offset; - uint8_t index; // 00-FE (FF = index not specified) + std::string name; + uint32_t index; // 0 = unused (not registered in index_to_function) + uint32_t menu_item_id; std::string generate_client_command( const std::unordered_map& label_writes = {}, const std::string& suffix = "") const; }; -std::shared_ptr compile_function_code(const std::string& text); +std::shared_ptr compile_function_code( + const std::string& directory, + const std::string& name, + const std::string& text); @@ -36,5 +44,33 @@ struct FunctionCodeIndex { FunctionCodeIndex(const std::string& directory); std::unordered_map> name_to_function; - std::vector> index_to_function; + std::unordered_map> index_to_function; + std::unordered_map> menu_item_id_to_patch_function; + + std::map> name_to_patch_function; + + std::vector patch_menu() const; + inline bool patch_menu_empty() const { + return this->name_to_patch_function.empty(); + } +}; + + + +struct DOLFileIndex { + struct DOLFile { + uint32_t menu_item_id; + std::string name; + std::string data; + }; + + std::vector> item_id_to_file; + std::map> name_to_file; + + DOLFileIndex(const std::string& directory); + + std::vector menu() const; + inline bool empty() const { + return this->name_to_file.empty() && this->item_id_to_file.empty(); + } }; diff --git a/src/Main.cc b/src/Main.cc index d8bb599d..d06f187a 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -83,100 +83,6 @@ void populate_state_from_config(shared_ptr s, s->common_item_creator.reset(new CommonItemCreator(enemy_categories, box_categories, unit_types)); - shared_ptr> information_menu_pc(new vector()); - shared_ptr> information_menu_gc(new vector()); - shared_ptr> information_contents(new vector()); - - information_menu_gc->emplace_back(INFORMATION_MENU_GO_BACK, u"Go back", - u"Return to the\nmain menu", 0); - { - uint32_t item_id = 0; - for (const auto& item : d.at("InformationMenuContents")->as_list()) { - auto& v = item->as_list(); - information_menu_pc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), - decode_sjis(v.at(1)->as_string()), 0); - information_menu_gc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), - decode_sjis(v.at(1)->as_string()), MenuItem::Flag::REQUIRES_MESSAGE_BOXES); - information_contents->emplace_back(decode_sjis(v.at(2)->as_string())); - item_id++; - } - } - s->information_menu_pc = information_menu_pc; - s->information_menu_gc = information_menu_gc; - s->information_contents = information_contents; - - s->proxy_destinations_menu_pc.emplace_back(PROXY_DESTINATIONS_MENU_GO_BACK, - u"Go back", u"Return to the\nmain menu", 0); - s->proxy_destinations_menu_gc.emplace_back(PROXY_DESTINATIONS_MENU_GO_BACK, - u"Go back", u"Return to the\nmain menu", 0); - { - uint32_t item_id = 0; - for (const auto& item : d.at("ProxyDestinations-GC")->as_dict()) { - const string& netloc_str = item.second->as_string(); - s->proxy_destinations_menu_gc.emplace_back(item_id, decode_sjis(item.first), - decode_sjis(netloc_str), 0); - s->proxy_destinations_gc.emplace_back(parse_netloc(netloc_str)); - item_id++; - } - } - { - uint32_t item_id = 0; - for (const auto& item : d.at("ProxyDestinations-PC")->as_dict()) { - const string& netloc_str = item.second->as_string(); - s->proxy_destinations_menu_pc.emplace_back(item_id, decode_sjis(item.first), - decode_sjis(netloc_str), 0); - s->proxy_destinations_pc.emplace_back(parse_netloc(netloc_str)); - item_id++; - } - } - try { - const string& netloc_str = d.at("ProxyDestination-Patch")->as_string(); - s->proxy_destination_patch = parse_netloc(netloc_str); - log(INFO, "Patch server proxy is enabled with destination %s", netloc_str.c_str()); - for (auto& it : s->name_to_port_config) { - if (it.second->version == GameVersion::PATCH) { - it.second->behavior = ServerBehavior::PROXY_SERVER; - } - } - } catch (const out_of_range&) { - s->proxy_destination_patch.first = ""; - s->proxy_destination_patch.second = 0; - } - try { - const string& netloc_str = d.at("ProxyDestination-BB")->as_string(); - s->proxy_destination_bb = parse_netloc(netloc_str); - log(INFO, "BB proxy is enabled with destination %s", netloc_str.c_str()); - for (auto& it : s->name_to_port_config) { - if (it.second->version == GameVersion::BB) { - it.second->behavior = ServerBehavior::PROXY_SERVER; - } - } - } catch (const out_of_range&) { - s->proxy_destination_bb.first = ""; - s->proxy_destination_bb.second = 0; - } - - s->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby", - u"Join the lobby", 0); - s->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information", - u"View server information", MenuItem::Flag::REQUIRES_MESSAGE_BOXES); - if (!s->proxy_destinations_pc.empty()) { - s->main_menu.emplace_back(MAIN_MENU_PROXY_DESTINATIONS, u"Proxy server", - u"Connect to another\nserver", MenuItem::Flag::PC_ONLY); - } - if (!s->proxy_destinations_gc.empty()) { - s->main_menu.emplace_back(MAIN_MENU_PROXY_DESTINATIONS, u"Proxy server", - u"Connect to another\nserver", MenuItem::Flag::GC_ONLY); - } - s->main_menu.emplace_back(MAIN_MENU_DOWNLOAD_QUESTS, u"Download quests", - u"Download quests", MenuItem::Flag::INVISIBLE_ON_BB); - s->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect", - u"Disconnect", 0); - - try { - s->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string()); - } catch (const out_of_range&) { } - auto local_address_str = d.at("LocalAddress")->as_string(); try { s->local_address = s->all_addresses.at(local_address_str); @@ -437,6 +343,12 @@ int main(int argc, char** argv) { log(INFO, "Compiling client functions"); state->function_code_index.reset(new FunctionCodeIndex("system/ppc")); + log(INFO, "Loading DOL files"); + state->dol_file_index.reset(new DOLFileIndex("system/dol")); + + log(INFO, "Creating menus"); + state->create_menus(config_json); + shared_ptr dns_server; if (state->dns_server_port) { log(INFO, "Starting DNS server"); diff --git a/src/Menu.hh b/src/Menu.hh index 9c0ea482..de0c6bd3 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -6,21 +6,49 @@ -#define MAIN_MENU_ID 0x11000011 -#define INFORMATION_MENU_ID 0x22000022 -#define LOBBY_MENU_ID 0x33000033 -#define GAME_MENU_ID 0x44000044 -#define QUEST_MENU_ID 0x55000055 -#define QUEST_FILTER_MENU_ID 0x66000066 -#define PROXY_DESTINATIONS_MENU_ID 0x77000077 +// Note: These aren't enums because neither enum nor enum class does what we +// want. Specifically, we need GO_BACK to be valid in multiple enums (and enums +// aren't namespaced unless they're enum classes), so we can't use enums. But we +// also want to be able to use non-enum values in switch statements without +// casting values all over the place, so we can't use enum classes either. -#define MAIN_MENU_GO_TO_LOBBY 0x11AAAA11 -#define MAIN_MENU_INFORMATION 0x11BBBB11 -#define MAIN_MENU_DOWNLOAD_QUESTS 0x11CCCC11 -#define MAIN_MENU_PROXY_DESTINATIONS 0x11DDDD11 -#define MAIN_MENU_DISCONNECT 0x11EEEE11 -#define INFORMATION_MENU_GO_BACK 0x22FFFF22 -#define PROXY_DESTINATIONS_MENU_GO_BACK 0x77FFFF77 +namespace MenuID { + constexpr uint32_t MAIN = 0x11000011; + constexpr uint32_t INFORMATION = 0x22000022; + constexpr uint32_t LOBBY = 0x33000033; + constexpr uint32_t GAME = 0x44000044; + constexpr uint32_t QUEST = 0x55000055; + constexpr uint32_t QUEST_FILTER = 0x66000066; + constexpr uint32_t PROXY_DESTINATIONS = 0x77000077; + constexpr uint32_t PROGRAMS = 0x88000088; + constexpr uint32_t PATCHES = 0x99000099; +} + +namespace MainMenuItemID { + constexpr uint32_t GO_TO_LOBBY = 0x11222211; + constexpr uint32_t INFORMATION = 0x11333311; + constexpr uint32_t DOWNLOAD_QUESTS = 0x11444411; + constexpr uint32_t PROXY_DESTINATIONS = 0x11555511; + constexpr uint32_t PATCHES = 0x11666611; + constexpr uint32_t PROGRAMS = 0x11777711; + constexpr uint32_t DISCONNECT = 0x11888811; +} + +namespace InformationMenuItemID { + constexpr uint32_t GO_BACK = 0x22FFFF22; +}; + +namespace ProxyDestinationsMenuItemID { + constexpr uint32_t GO_BACK = 0x77FFFF77; +}; + +namespace ProgramsMenuItemID { + constexpr uint32_t GO_BACK = 0x88FFFF88; +}; + +namespace PatchesMenuItemID { + constexpr uint32_t GO_BACK = 0x99FFFF99; +}; @@ -35,6 +63,7 @@ struct MenuItem { GC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_BB, BB_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_GC, REQUIRES_MESSAGE_BOXES = 0x10, + REQUIRES_SEND_FUNCTION_CALL = 0x20, }; uint32_t item_id; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 24ecbc1a..de7ce9ca 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -462,7 +462,7 @@ static bool process_server_B2(shared_ptr, if (session.function_call_return_value >= 0) { session.log(INFO, "Blocking function call from server"); - C_ExecuteCodeResult_GC_BB_B3 cmd; + C_ExecuteCodeResult_B3 cmd; cmd.return_value = session.function_call_return_value; cmd.checksum = 0; session.send_to_end(true, 0xB3, flag, &cmd, sizeof(cmd)); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 9c6c1b42..b8d8f239 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -10,13 +10,14 @@ #include #include -#include "PSOProtocol.hh" -#include "FileContentsCache.hh" -#include "Text.hh" -#include "SendCommands.hh" -#include "ReceiveSubcommands.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; @@ -112,7 +113,7 @@ void process_login_complete(shared_ptr s, shared_ptr c) { (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(), MAIN_MENU_ID, s->main_menu, false); + send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu, false); } else { send_message_box(c, s->welcome_message.c_str()); } @@ -538,10 +539,10 @@ void process_message_box_closed(shared_ptr s, shared_ptr 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", INFORMATION_MENU_ID, + send_menu(c, u"Information", MenuID::INFORMATION, *s->information_menu_for_version(c->version), false); } else if (c->flags & Client::Flag::AT_WELCOME_MESSAGE) { - send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); + send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu, false); c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE; send_update_client_config(c); } @@ -552,31 +553,16 @@ void process_menu_item_info_request(shared_ptr s, shared_ptr(data); switch (cmd.menu_id) { - case MAIN_MENU_ID: - switch (cmd.item_id) { - case MAIN_MENU_GO_TO_LOBBY: - send_ship_info(c, u"Go to the lobby."); - break; - case MAIN_MENU_INFORMATION: - send_ship_info(c, u"View server\ninformation."); - break; - case MAIN_MENU_PROXY_DESTINATIONS: - send_ship_info(c, u"Connect to another\nserver."); - break; - case MAIN_MENU_DOWNLOAD_QUESTS: - send_ship_info(c, u"Download a quest."); - break; - case MAIN_MENU_DISCONNECT: - send_ship_info(c, u"End your session."); - break; - default: - send_ship_info(c, u"Incorrect menu item ID."); - break; + 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 INFORMATION_MENU_ID: - if (cmd.item_id == INFORMATION_MENU_GO_BACK) { + case MenuID::INFORMATION: + if (cmd.item_id == InformationMenuItemID::GO_BACK) { send_ship_info(c, u"Return to the\nmain menu."); } else { try { @@ -588,8 +574,8 @@ void process_menu_item_info_request(shared_ptr s, shared_ptr s, shared_ptrquest_index) { send_quest_info(c, u"$C6Quests are not available.", !c->lobby_id); break; @@ -616,6 +602,97 @@ void process_menu_item_info_request(shared_ptr s, shared_ptr 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; @@ -630,9 +707,9 @@ void process_menu_selection(shared_ptr s, shared_ptr c, sizeof(C_MenuSelection), sizeof(C_MenuSelection) + 0x10 * (1 + uses_unicode)); switch (cmd.menu_id) { - case MAIN_MENU_ID: { + case MenuID::MAIN: { switch (cmd.item_id) { - case MAIN_MENU_GO_TO_LOBBY: { + case MainMenuItemID::GO_TO_LOBBY: { static const vector version_to_port_name({ "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "bb-lobby"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); @@ -642,18 +719,18 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } - case MAIN_MENU_INFORMATION: - send_menu(c, u"Information", INFORMATION_MENU_ID, + case MainMenuItemID::INFORMATION: + send_menu(c, u"Information", MenuID::INFORMATION, *s->information_menu_for_version(c->version), true); c->flags |= Client::Flag::IN_INFORMATION_MENU; break; - case MAIN_MENU_PROXY_DESTINATIONS: - send_menu(c, u"Proxy server", PROXY_DESTINATIONS_MENU_ID, + case MainMenuItemID::PROXY_DESTINATIONS: + send_menu(c, u"Proxy server", MenuID::PROXY_DESTINATIONS, s->proxy_destinations_menu_for_version(c->version), false); break; - case MAIN_MENU_DOWNLOAD_QUESTS: + case MainMenuItemID::DOWNLOAD_QUESTS: if (c->flags & Client::Flag::EPISODE_3) { shared_ptr l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr; auto quests = s->quest_index->filter( @@ -665,14 +742,23 @@ void process_menu_selection(shared_ptr s, shared_ptr c, // 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, QUEST_MENU_ID, quests, true); + send_quest_menu(c, MenuID::QUEST, quests, true); } } else { - send_quest_menu(c, QUEST_FILTER_MENU_ID, quest_download_menu, true); + send_quest_menu(c, MenuID::QUEST_FILTER, quest_download_menu, true); } break; - case MAIN_MENU_DISCONNECT: + 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(), false); + break; + + case MainMenuItemID::DISCONNECT: c->should_disconnect = true; break; @@ -683,10 +769,10 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } - case INFORMATION_MENU_ID: { - if (cmd.item_id == INFORMATION_MENU_GO_BACK) { + case MenuID::INFORMATION: { + if (cmd.item_id == InformationMenuItemID::GO_BACK) { c->flags &= ~Client::Flag::IN_INFORMATION_MENU; - send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); + send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu, false); } else { try { @@ -698,9 +784,9 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } - case PROXY_DESTINATIONS_MENU_ID: { - if (cmd.item_id == PROXY_DESTINATIONS_MENU_GO_BACK) { - send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); + case MenuID::PROXY_DESTINATIONS: { + if (cmd.item_id == ProxyDestinationsMenuItemID::GO_BACK) { + send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu, false); } else { const pair* dest = nullptr; @@ -736,7 +822,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } - case GAME_MENU_ID: { + case MenuID::GAME: { auto game = s->find_lobby(cmd.item_id); if (!game) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because it no\nlonger exists."); @@ -799,7 +885,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } - case QUEST_FILTER_MENU_ID: { + case MenuID::QUEST_FILTER: { if (!s->quest_index) { send_lobby_message_box(c, u"$C6Quests are not available."); break; @@ -815,11 +901,11 @@ void process_menu_selection(shared_ptr s, shared_ptr c, // Hack: assume the menu to be sent is the download quest menu if the // client is not in any lobby - send_quest_menu(c, QUEST_MENU_ID, quests, !c->lobby_id); + send_quest_menu(c, MenuID::QUEST, quests, !c->lobby_id); break; } - case QUEST_MENU_ID: { + case MenuID::QUEST: { if (!s->quest_index) { send_lobby_message_box(c, u"$C6Quests are not available."); break; @@ -903,8 +989,40 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } - case LOBBY_MENU_ID: - // TODO; + case MenuID::PATCHES: + if (cmd.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(cmd.item_id)); + send_menu(c, u"Patches", MenuID::PATCHES, s->function_code_index->patch_menu()); + } + break; + + case MenuID::PROGRAMS: + if (cmd.item_id == ProgramsMenuItemID::GO_BACK) { + send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu, false); + + } 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(cmd.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: @@ -917,6 +1035,11 @@ void process_change_lobby(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 84 const auto& cmd = check_size_t(data); + if (cmd.menu_id != MenuID::LOBBY) { + send_message_box(c, u"Incorrect menu ID."); + return; + } + shared_ptr new_lobby; try { new_lobby = s->find_lobby(cmd.item_id); @@ -942,7 +1065,7 @@ void process_game_list_request(shared_ptr s, shared_ptr c, void process_information_menu_request_pc(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 1F check_size_v(data.size(), 0); - send_menu(c, u"Information", INFORMATION_MENU_ID, + send_menu(c, u"Information", MenuID::INFORMATION, *s->information_menu_for_version(c->version), true); } @@ -977,6 +1100,57 @@ void process_change_block(shared_ptr s, shared_ptr c, process_change_ship(s, c, command, flag, data); } +//////////////////////////////////////////////////////////////////////////////// +// DOL loading commands + +static void send_dol_file_chunk(shared_ptr s, shared_ptr 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(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 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 s, shared_ptr c, + uint16_t, uint32_t flag, const string& data) { // B3 + const auto& cmd = check_size_t(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 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 @@ -1018,7 +1192,7 @@ void process_quest_list_request(shared_ptr s, shared_ptr c, } } - send_quest_menu(c, QUEST_FILTER_MENU_ID, *menu, false); + send_quest_menu(c, MenuID::QUEST_FILTER, *menu, false); } } @@ -1105,6 +1279,9 @@ void process_player_data(shared_ptr s, shared_ptr c, case GameVersion::GC: { const PSOPlayerDataGC* 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(data); c->game_data.ep3_config.reset(new Ep3Config(pd3->ep3_config)); pd = reinterpret_cast(pd3); @@ -1890,7 +2067,7 @@ static process_command_t dc_handlers[0x100] = { nullptr, nullptr, nullptr, nullptr, // B0 - nullptr, process_server_time_request, nullptr, nullptr, + nullptr, process_server_time_request, nullptr, process_function_call_result, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, @@ -1973,7 +2150,7 @@ static process_command_t pc_handlers[0x100] = { nullptr, nullptr, nullptr, nullptr, // B0 - nullptr, process_server_time_request, nullptr, nullptr, + nullptr, process_server_time_request, nullptr, process_function_call_result, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, @@ -2057,7 +2234,7 @@ static process_command_t gc_handlers[0x100] = { process_quest_barrier, nullptr, nullptr, nullptr, // B0 - nullptr, process_server_time_request, nullptr, process_ignored_command, + nullptr, process_server_time_request, nullptr, process_function_call_result, nullptr, nullptr, nullptr, process_ignored_command, process_ignored_command, nullptr, process_ep3_jukebox, nullptr, nullptr, nullptr, nullptr, nullptr, @@ -2146,7 +2323,7 @@ static process_command_t bb_handlers[0x100] = { process_quest_barrier, nullptr, nullptr, nullptr, // B0 - nullptr, nullptr, nullptr, process_ignored_command, + nullptr, nullptr, nullptr, process_function_call_result, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index b76b3c34..0be6789d 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -332,7 +332,7 @@ void send_function_call( } string data; - uint8_t index = 0xFF; + uint32_t index = 0; if (code.get()) { data = code->generate_client_command(label_writes, suffix); index = code->index; @@ -688,7 +688,7 @@ void send_card_search_result_t( location_string = string_printf(",BLOCK00,%s", encoded_server_name.c_str()); } cmd.location_string = location_string; - cmd.menu_id = LOBBY_MENU_ID; + cmd.menu_id = MenuID::LOBBY; cmd.lobby_id = result->lobby_id; cmd.name = result->game_data.player()->disp.name; @@ -789,7 +789,8 @@ void send_menu_t( ((c->version == GameVersion::PC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_PC)) || ((c->version == GameVersion::GC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_GC)) || ((c->version == GameVersion::BB) && (item.flags & MenuItem::Flag::INVISIBLE_ON_BB)) || - ((item.flags & MenuItem::Flag::REQUIRES_MESSAGE_BOXES) && (c->flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION))) { + ((item.flags & MenuItem::Flag::REQUIRES_MESSAGE_BOXES) && (c->flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION)) || + ((item.flags & MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL) && (c->flags & Client::Flag::DOES_NOT_SUPPORT_SEND_FUNCTION_CALL))) { continue; } auto& e = entries.emplace_back(); @@ -820,7 +821,7 @@ void send_game_menu_t(shared_ptr c, shared_ptr s) { vector> entries; { auto& e = entries.emplace_back(); - e.menu_id = GAME_MENU_ID; + e.menu_id = MenuID::GAME; e.game_id = 0x00000000; e.difficulty_tag = 0x00; e.num_players = 0x00; @@ -839,7 +840,7 @@ void send_game_menu_t(shared_ptr c, shared_ptr s) { } auto& e = entries.emplace_back(); - e.menu_id = GAME_MENU_ID; + e.menu_id = MenuID::GAME; e.game_id = l->lobby_id; e.difficulty_tag = (l_is_ep3 ? 0x0A : (l->difficulty + 0x22)); e.num_players = l->count_clients(); @@ -941,7 +942,7 @@ void send_lobby_list(shared_ptr c, shared_ptr s) { continue; } auto& e = entries.emplace_back(); - e.menu_id = LOBBY_MENU_ID; + e.menu_id = MenuID::LOBBY; e.item_id = l->lobby_id; e.unused = 0; } diff --git a/src/ServerState.cc b/src/ServerState.cc index 7444ac26..034cd31f 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -3,10 +3,11 @@ #include #include +#include -#include "SendCommands.hh" -#include "NetworkAddresses.hh" #include "IPStackSimulator.hh" +#include "NetworkAddresses.hh" +#include "SendCommands.hh" #include "Text.hh" using namespace std; @@ -237,3 +238,111 @@ void ServerState::set_port_configuration( } } } + + + +void ServerState::create_menus(shared_ptr config_json) { + const auto& d = config_json->as_dict(); + + shared_ptr> information_menu_pc(new vector()); + shared_ptr> information_menu_gc(new vector()); + shared_ptr> information_contents(new vector()); + + information_menu_gc->emplace_back(InformationMenuItemID::GO_BACK, u"Go back", + u"Return to the\nmain menu", 0); + { + uint32_t item_id = 0; + for (const auto& item : d.at("InformationMenuContents")->as_list()) { + auto& v = item->as_list(); + information_menu_pc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), + decode_sjis(v.at(1)->as_string()), 0); + information_menu_gc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), + decode_sjis(v.at(1)->as_string()), MenuItem::Flag::REQUIRES_MESSAGE_BOXES); + information_contents->emplace_back(decode_sjis(v.at(2)->as_string())); + item_id++; + } + } + this->information_menu_pc = information_menu_pc; + this->information_menu_gc = information_menu_gc; + this->information_contents = information_contents; + + this->proxy_destinations_menu_pc.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, + u"Go back", u"Return to the\nmain menu", 0); + this->proxy_destinations_menu_gc.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, + u"Go back", u"Return to the\nmain menu", 0); + { + uint32_t item_id = 0; + for (const auto& item : d.at("ProxyDestinations-GC")->as_dict()) { + const string& netloc_str = item.second->as_string(); + this->proxy_destinations_menu_gc.emplace_back(item_id, decode_sjis(item.first), + decode_sjis(netloc_str), 0); + this->proxy_destinations_gc.emplace_back(parse_netloc(netloc_str)); + item_id++; + } + } + { + uint32_t item_id = 0; + for (const auto& item : d.at("ProxyDestinations-PC")->as_dict()) { + const string& netloc_str = item.second->as_string(); + this->proxy_destinations_menu_pc.emplace_back(item_id, decode_sjis(item.first), + decode_sjis(netloc_str), 0); + this->proxy_destinations_pc.emplace_back(parse_netloc(netloc_str)); + item_id++; + } + } + try { + const string& netloc_str = d.at("ProxyDestination-Patch")->as_string(); + this->proxy_destination_patch = parse_netloc(netloc_str); + log(INFO, "Patch server proxy is enabled with destination %s", netloc_str.c_str()); + for (auto& it : this->name_to_port_config) { + if (it.second->version == GameVersion::PATCH) { + it.second->behavior = ServerBehavior::PROXY_SERVER; + } + } + } catch (const out_of_range&) { + this->proxy_destination_patch.first = ""; + this->proxy_destination_patch.second = 0; + } + try { + const string& netloc_str = d.at("ProxyDestination-BB")->as_string(); + this->proxy_destination_bb = parse_netloc(netloc_str); + log(INFO, "BB proxy is enabled with destination %s", netloc_str.c_str()); + for (auto& it : this->name_to_port_config) { + if (it.second->version == GameVersion::BB) { + it.second->behavior = ServerBehavior::PROXY_SERVER; + } + } + } catch (const out_of_range&) { + this->proxy_destination_bb.first = ""; + this->proxy_destination_bb.second = 0; + } + + this->main_menu.emplace_back(MainMenuItemID::GO_TO_LOBBY, u"Go to lobby", + u"Join the lobby", 0); + this->main_menu.emplace_back(MainMenuItemID::INFORMATION, u"Information", + u"View server\ninformation", MenuItem::Flag::REQUIRES_MESSAGE_BOXES); + if (!this->proxy_destinations_pc.empty()) { + this->main_menu.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, u"Proxy server", + u"Connect to another\nserver", MenuItem::Flag::PC_ONLY); + } + if (!this->proxy_destinations_gc.empty()) { + this->main_menu.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, u"Proxy server", + u"Connect to another\nserver", MenuItem::Flag::GC_ONLY); + } + this->main_menu.emplace_back(MainMenuItemID::DOWNLOAD_QUESTS, u"Download quests", + u"Download quests", MenuItem::Flag::INVISIBLE_ON_BB); + if (!this->dol_file_index->empty()) { + this->main_menu.emplace_back(MainMenuItemID::PATCHES, u"Patches", + u"Change game\nbehaviors", MenuItem::Flag::GC_ONLY | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + } + if (!this->dol_file_index->empty()) { + this->main_menu.emplace_back(MainMenuItemID::PROGRAMS, u"Programs", + u"Run GameCube\nprograms", MenuItem::Flag::GC_ONLY | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + } + this->main_menu.emplace_back(MainMenuItemID::DISCONNECT, u"Disconnect", + u"Disconnect", 0); + + try { + this->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string()); + } catch (const out_of_range&) { } +} diff --git a/src/ServerState.hh b/src/ServerState.hh index 6f8415f6..a50c5e1c 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -2,20 +2,21 @@ #include #include -#include -#include #include -#include +#include #include +#include +#include +#include #include "Client.hh" +#include "FunctionCompiler.hh" #include "Items.hh" #include "LevelTable.hh" #include "License.hh" #include "Lobby.hh" #include "Menu.hh" #include "Quest.hh" -#include "FunctionCompiler.hh" @@ -48,6 +49,7 @@ struct ServerState { RunShellBehavior run_shell_behavior; std::vector> bb_private_keys; std::shared_ptr function_code_index; + std::shared_ptr dol_file_index; std::shared_ptr ep3_data_index; std::shared_ptr quest_index; std::shared_ptr level_table; @@ -111,4 +113,6 @@ struct ServerState { void set_port_configuration( const std::vector& port_configs); + + void create_menus(std::shared_ptr config_json); }; diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index c01a76a7..55cd37be 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -1,5 +1,7 @@ #include "StaticGameData.hh" +#include + using namespace std; @@ -253,6 +255,93 @@ uint8_t npc_for_name(const u16string& name) { +const char* name_for_char_class(uint8_t cls) { + static const array names = { + "HUmar", + "HUnewearl", + "HUcast", + "RAmar", + "RAcast", + "RAcaseal", + "FOmarl", + "FOnewm", + "FOnewearl", + "HUcaseal", + "FOmar", + "RAmarl", + }; + try { + return names.at(cls); + } catch (const out_of_range&) { + return "Unknown"; + } +} + +const char* abbreviation_for_char_class(uint8_t cls) { + static const array names = { + "HUmr", + "HUnl", + "HUcs", + "RAmr", + "RAcs", + "RAcl", + "FOml", + "FOnm", + "FOnl", + "HUcl", + "FOmr", + "RAml", + }; + try { + return names.at(cls); + } catch (const out_of_range&) { + return "???"; + } +} + + + +const char* name_for_difficulty(uint8_t difficulty) { + static const array names = { + "Normal", + "Hard", + "Very Hard", + "Ultimate", + }; + try { + return names.at(difficulty); + } catch (const out_of_range&) { + return "Unknown"; + } +} + +char abbreviation_for_difficulty(uint8_t difficulty) { + static const array names = {'N', 'H', 'V', 'U'}; + try { + return names.at(difficulty); + } catch (const out_of_range&) { + return '?'; + } +} + + + +const char* abbreviation_for_game_mode(uint8_t mode) { + static const array names = { + "Nml", + "Btl", + "Chl", + "Solo", + }; + try { + return names.at(mode); + } catch (const out_of_range&) { + return "???"; + } +} + + + size_t stack_size_for_item(uint8_t data0, uint8_t data1) { if (data0 == 4) { return 999999; diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index 73927905..19fb6bd8 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -40,4 +40,12 @@ std::u16string u16name_for_npc(uint8_t npc); uint8_t npc_for_name(const std::string& name); uint8_t npc_for_name(const std::u16string& name); +const char* name_for_char_class(uint8_t cls); +const char* abbreviation_for_char_class(uint8_t cls); + +const char* name_for_difficulty(uint8_t difficulty); +char abbreviation_for_difficulty(uint8_t difficulty); + +const char* abbreviation_for_game_mode(uint8_t); + std::string name_for_item(const ItemData& item, bool include_color_codes); diff --git a/system/ppc/FlushCachedCode.inc.s b/system/ppc/FlushCachedCode.inc.s new file mode 100644 index 00000000..da0b85f5 --- /dev/null +++ b/system/ppc/FlushCachedCode.inc.s @@ -0,0 +1,20 @@ +# This code flushes the data cache and invalidates the instruction cache for a +# block of newly-written code in memory. +# Arguments: +# r3 = address of written code +# r4 = number of bytes +# Returns: nothing +# Overwrites: r3, r4, r5 + lis r5, 0xFFFF + ori r5, r5, 0xFFF1 + and r5, r5, r3 + subf r3, r5, r3 + add r4, r4, r3 +flush_cached_code_writes__again: + dcbst r0, r5 + sync + icbi r0, r5 + addic r5, r5, 8 + subic. r4, r4, 8 + bge flush_cached_code_writes__again + isync diff --git a/system/ppc/InitClearCaches.inc.s b/system/ppc/InitClearCaches.inc.s new file mode 100644 index 00000000..b50524a1 --- /dev/null +++ b/system/ppc/InitClearCaches.inc.s @@ -0,0 +1,18 @@ +# This macro clears the data and instruction caches at the beginning of each +# function. This is necessary because apparently some versions of PSO don't do +# this correctly by themselves. + +# This macro expects to be run immediately at the entrypoint (usually the start +# label) for all functions. It returns the original return address in r12, and +# the address of the start label in r11. + mflr r12 # r12 = address to return to + mfctr r3 # r3 = address of start label (this code is called via bctrl) + addi r4, r3, 0x7C00 # r4 = end of relevant region +InitClearCaches__next_cache_block: + dcbst r0, r3 + sync + icbi r0, r3 + addi r3, r3, 0x20 + cmpl r3, r4 + blt InitClearCaches__next_cache_block + isync diff --git a/system/ppc/ReadMemoryWord.s b/system/ppc/ReadMemoryWord.s new file mode 100644 index 00000000..ee7eef24 --- /dev/null +++ b/system/ppc/ReadMemoryWord.s @@ -0,0 +1,21 @@ +# This function is required for loading DOLs. If it's not present, newserv can't +# serve DOL files to GameCube clients. + +newserv_index_E0: + +entry_ptr: +reloc0: + .offsetof start + +start: + .include InitClearCaches + + bl read +address: + .zero +read: + mflr r3 + lwz r3, [r3] + lwz r3, [r3] + mtlr r12 + blr diff --git a/system/ppc/RunDOL.s b/system/ppc/RunDOL.s new file mode 100644 index 00000000..b2770993 --- /dev/null +++ b/system/ppc/RunDOL.s @@ -0,0 +1,130 @@ +# This function is required for loading DOLs. If it's not present, newserv can't +# serve DOL files to GameCube clients. + +newserv_index_E2: + +entry_ptr: +reloc0: + .offsetof start + +start: + .include InitClearCaches + +disable_interrupts: + mfmsr r3 + rlwinm r3, r3, 0, 17, 15 + mtmsr r3 + + bl get_current_addr +dol_base_ptr: + .zero +get_current_addr: + mflr r31 + # TODO: It'd be nice to be able to use an expression for the immediate value + # here - something like (dol_base_ptr - start), for example + subi r31, r31, 0x38 # r31 = base of data to copy to low memory (start label) + + # If this code is not running from low memory (80001800-80003000), then copy + # it there and branch to it + lis r3, 0x8000 + ori r3, r3, 0x3000 + cmp r31, r3 + blt run_dol + +copy_code_to_low_memory: + bl get_end_ptr + sub r30, r3, r31 # r30 = size of code to copy (for cache flushing later) + subi r5, r3, 4 # r5 = end ptr + subi r4, r31, 4 + lis r3, 0x8000 + ori r3, r3, 0x17FC +copy_code_to_low_memory__again: + lwzu r0, [r4 + 4] + stwu [r3 + 4], r0 + cmp r4, r5 + bne copy_code_to_low_memory__again + + # Flush the data cache and clear the instruction cache before running the + # moved code + lis r3, 0x8000 + ori r3, r3, 0x1800 + mr r4, r30 + mtlr r3 + b flush_cached_code_writes + + + +run_dol: + lwz r30, [r31 + 0x38] # r30 = DOL base ptr + + # DOL files are very simple: they have up to 7 text sections, up to 11 data + # sections, and a BSS section and an entrypoint. No imports or other fancy + # things to do - we just have to move a bunch of bytes around. + mr r29, r30 # r29 = DOL header iterator + addi r28, r29, 0x48 # r28 = DOL header iterator end value + +run_dol__move_section: + lwz r4, [r29] # r4 = file offset of section data + add r4, r4, r30 # r4 = address of section data + lwz r3, [r29 + 0x48] # r3 = dest address of section data + lwz r5, [r29 + 0x90] # r5 = number of bytes to move + cmplwi r5, 0 # If size is 0, skip the section entirely + beq skip_section + subi r3, r3, 1 + subi r4, r4, 1 + add r5, r4, r5 # r5 = source end pointer +run_dol__move_section_data__again: + # TODO: We probably should implement memmove-like semantics here, in case the + # DOL loads at an unusually late address. This is probably very rare. + lbzu r0, [r4 + 1] + stbu [r3 + 1], r0 + cmp r4, r5 + bne run_dol__move_section_data__again + + # Flush the data cache and invalidate the instruction cache after copying the + # section data. Technically we don't have to do this for data sections, but + # I'm lazy and it doesn't take too long. + lwz r3, [r29 + 0x48] # r3 = dest address of section data + lwz r4, [r29 + 0x90] # r4 = size of section data + bl flush_cached_code_writes + +skip_section: + # Move to the next section + addi r29, r29, 4 + cmp r29, r28 + bne run_dol__move_section + +run_dol__zero_bss: + lwz r3, [r30 + 0xD8] # r3 = BSS address + lwz r4, [r30 + 0xDC] # r4 = BSS size + cmplwi r4, 0 + beq run_dol__skip_zero_bss + add r4, r3, r4 # r4 = BSS end address + subi r3, r3, 1 + li r0, 0 +run_dol__zero_bss__again: + stbu [r3 + 1], r0 + cmp r3, r4 + bne run_dol__zero_bss__again +run_dol__skip_zero_bss: + +run_dol__go_to_entrypoint: + lwz r0, [r30 + 0xE0] # r30 = entrypoint + mtctr r0 + bctr + + + +flush_cached_code_writes: + .include FlushCachedCode + blr + + + +return_end_ptr: + mflr r3 + bctr +get_end_ptr: + mflr r0 + mtctr r0 + bl return_end_ptr diff --git a/system/ppc/WriteMemory.s b/system/ppc/WriteMemory.s index 27eb693c..00c2c12f 100644 --- a/system/ppc/WriteMemory.s +++ b/system/ppc/WriteMemory.s @@ -1,11 +1,17 @@ -# This example shows how to use newserv's send_function_call function for PSO -# GameCube clients. This code writes a variable-length block of data to a -# specified address in the client's memory. +# This function is required for loading DOLs. If it's not present, newserv can't +# serve DOL files to GameCube clients. -# For example, to write the bytes 38 00 00 05 to the address 8010521C, -# send_function_call could be called like this: +# This is also the file I've chosen to document how to write code for newserv's +# functions subsystem. The code implemented in this file writes a +# variable-length block of data to a specified address in the client's memory. +# Note that WriteMemory is a general function that uses many of the subsystem's +# features. If you're writing a patch (not a general function), you cannot use +# the suffix or label_offsets features that are described here. + +# For example, to use this function to write the bytes 38 00 00 05 to the +# address 8010521C, send_function_call could be called like this: # auto fn = s->function_code_index->name_to_function.at("WriteMemory"); -# unordered_map label_writes( # {{"dest_addr", 0x8010521C}, {"size", 4}}); # string suffix("\x38\x00\x00\x05", 4); # send_function_call( @@ -15,77 +21,88 @@ # suffix); // Data to append after the code (not all functions use this) # The meanings of label_writes and suffix are described in the comments below. -# A label newserv_id_XX tells newserv what value to use in the flag field when -# sending the B2 command. This is needed if the server needs to do something -# when the B3 response is received. -newserv_id_C0: +# A label newserv_index_XX tells newserv what value to use in the flag field +# when sending the B2 command. This is needed if the server needs to do +# something when the B3 response is received. For GameCube functions, if +# specified, the index must be in the range 01-FF. The DOL loading +# functionality, which this function is a part of, uses indexes E0, E1, and E2, +# but this function can also be used for other purposes. +newserv_index_E1: -# The entry_ptr label is required. It should point to a .offsetof directive that -# itself points to the actual entrypoint. +# The entry_ptr label is required for all functions. It should point to a +# .offsetof directive that itself points to the actual entrypoint. entry_ptr: -# All labels starting with reloc signify that the following PPC word -# (be_uint32_t) is to be relocated at runtime. That is, when the code is run, -# the PPC word will contain the actual memory address relative to the running -# code instead of the offset that it holds at assembly time. The entry_ptr label -# should almost always have a reloc label next to it. +# All labels starting with reloc signify that the following PPC word (big-endian +# 32-bit value) is to be relocated at runtime. That is, when the code runs on +# the client, the PPC word will contain the actual memory address relative to +# the running code instead of the offset that it holds at assembly time. The +# entry_ptr label should almost always have a reloc label next to it. reloc0: .offsetof start +start: + # A .include directive essentially pastes in the code from the referenced + # file. Here, we use the code from the file InitClearCaches.inc.s. + # PSO GC doesn't properly clear the data and instruction caches when it + # executes functions, so we use this include in all functions to do so. Since + # all functions do this, this makes it safe to use more than one function in + # each client's session. + .include InitClearCaches + + bl get_block_ptr + mr r6, r3 # r6 = address of dest_addr label + copy_block: - # r8 = address to return to (LR, from start label) - mflr r6 # r6 = address of dest_addr label - mtlr r8 lwz r3, [r6] # r3 = dest ptr subi r3, r3, 1 # subtract 1 so we can use stbu lwz r5, [r6 + 4] # r5 = size (bytes remaining) - add r5, r5, r3 # r5 = dest end ptr + add r5, r5, r3 # r5 = dest end ptr (last byte to be written) addi r4, r6, 7 # r4 = src ptr (starting at -1 so we can use lbzu) - copy_block__again: lbzu r0, [r4 + 1] stbu [r3 + 1], r0 cmp r3, r5 bne copy_block__again + # Flush the data cache and clear the instruction cache at the written region lwz r3, [r6] # r3 = dest ptr lwz r4, [r6 + 4] # r4 = size + .include FlushCachedCode - # Flush the data cache and clear the instruction cache at the written region - lis r5, 0xFFFF - ori r5, r5, 0xFFF1 - and r5, r5, r3 - subf r3, r5, r3 - add r4, r4, r3 -flush_cached_code_writes__again: - dcbst r0, r5 - sync - icbi r0, r5 - addic r5, r5, 8 - subic. r4, r4, 8 - bge flush_cached_code_writes__again - isync - - # Return 0 (this value appears in the B3 command) - li r3, 0 + # Return the address after the last byte written. The value returned in r3 + # from the function is sent back to the server in a B3 command. newserv uses + # the return value during DOL loading to know which section of the DOL file to + # send next, or to send the RunDOL function if all sections have been loaded. + lwz r3, [r6] # r3 = dest ptr + lwz r4, [r6 + 4] # r4 = size + add r3, r3, r4 + mtlr r12 blr -start: +get_block_ptr__ret: + mflr r3 + mtlr r10 + blr +get_block_ptr: # We use a trick here to get the address of the dest_addr label: since bl puts # the immediately-following address into the link register, we "call" - # copy_block and get the dest_addr pointer out of the LR. We then put r8 back - # into the LR so copy_block can return normally. - mflr r8 - bl copy_block + # get_block_ptr__ret and get the dest_addr pointer out of the LR. We then put + # r10 back into the LR so get_block_ptr__ret returns to the caller. + mflr r10 + bl get_block_ptr__ret -# These fields are filled in when the B2 command is generated. Specifically, the -# label_writes argument to send_function_call is responsible for this. +# These fields are filled in right before the command is sent to the client. +# Specifically, the label_writes argument to send_function_call is responsible +# for this. The label_writes argument is a map of label name to value, and +# send_function_call simply writes the given values after the given labels. This +# is a way to pass arbitrary arguments to a function at call time. dest_addr: .zero size: .zero -# The data to be written is appended here at B2 construction time via the suffix -# argument to send_function_call. (This label is for documentation purposes -# only; the suffix argument always appends data after the end of all the -# assembled code.) +# Finally, we use the suffix argument to instruct send_function_call to append +# the data we want to write to memory immediately after the assembled code. +# (The data_to_write label here is for documentation purposes only; the suffix +# argument always appends data after the end of all the assembled code.) data_to_write: