add DOL file loader

This commit is contained in:
Martin Michelsen
2022-06-01 13:00:43 -07:00
parent 40aa08bd4f
commit 562bc4a40c
20 changed files with 948 additions and 264 deletions
+15 -2
View File
@@ -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.
+3 -1
View File
@@ -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) {
+14 -9
View File
@@ -4,13 +4,13 @@
#include <memory>
#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<DOLFileIndex::DOLFile> loading_dol_file;
Client(struct bufferevent* bev, GameVersion version,
ServerBehavior server_behavior);
+5 -2
View File
@@ -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<le_uint32_t> { };
// 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
};
+105 -15
View File
@@ -26,9 +26,9 @@ bool function_compiler_available() {
std::string CompiledFunctionCode::generate_client_command(
const std::unordered_map<std::string, uint32_t>& label_writes,
const std::string& suffix) const {
string CompiledFunctionCode::generate_client_command(
const unordered_map<string, uint32_t>& 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<CompiledFunctionCode> compile_function_code(const std::string& text) {
shared_ptr<CompiledFunctionCode> 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<string> get_include_stack; // For mutual recursion detection
function<string(const string&)> 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<CompiledFunctionCode> 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<uint32_t> reloc_indexes;
for (const auto& it : ret->label_offsets) {
@@ -110,32 +131,101 @@ shared_ptr<CompiledFunctionCode> 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<MenuItem> FunctionCodeIndex::patch_menu() const {
vector<MenuItem> 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<DOLFile> 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<MenuItem> DOLFileIndex::menu() const {
vector<MenuItem> 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;
}
+39 -3
View File
@@ -4,9 +4,12 @@
#include <string>
#include <unordered_map>
#include <map>
#include <vector>
#include <memory>
#include "Menu.hh"
bool function_compiler_available();
@@ -21,14 +24,19 @@ struct CompiledFunctionCode {
std::vector<uint16_t> relocation_deltas;
std::unordered_map<std::string, uint32_t> 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<std::string, uint32_t>& label_writes = {},
const std::string& suffix = "") const;
};
std::shared_ptr<CompiledFunctionCode> compile_function_code(const std::string& text);
std::shared_ptr<CompiledFunctionCode> 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<std::string, std::shared_ptr<CompiledFunctionCode>> name_to_function;
std::vector<std::shared_ptr<CompiledFunctionCode>> index_to_function;
std::unordered_map<uint32_t, std::shared_ptr<CompiledFunctionCode>> index_to_function;
std::unordered_map<uint32_t, std::shared_ptr<CompiledFunctionCode>> menu_item_id_to_patch_function;
std::map<std::string, std::shared_ptr<CompiledFunctionCode>> name_to_patch_function;
std::vector<MenuItem> 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<std::shared_ptr<DOLFile>> item_id_to_file;
std::map<std::string, std::shared_ptr<DOLFile>> name_to_file;
DOLFileIndex(const std::string& directory);
std::vector<MenuItem> menu() const;
inline bool empty() const {
return this->name_to_file.empty() && this->item_id_to_file.empty();
}
};
+6 -94
View File
@@ -83,100 +83,6 @@ void populate_state_from_config(shared_ptr<ServerState> s,
s->common_item_creator.reset(new CommonItemCreator(enemy_categories,
box_categories, unit_types));
shared_ptr<vector<MenuItem>> information_menu_pc(new vector<MenuItem>());
shared_ptr<vector<MenuItem>> information_menu_gc(new vector<MenuItem>());
shared_ptr<vector<u16string>> information_contents(new vector<u16string>());
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<DNSServer> dns_server;
if (state->dns_server_port) {
log(INFO, "Starting DNS server");
+43 -14
View File
@@ -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;
+1 -1
View File
@@ -462,7 +462,7 @@ static bool process_server_B2(shared_ptr<ServerState>,
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));
+238 -61
View File
@@ -10,13 +10,14 @@
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
#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<ServerState> s, shared_ptr<Client> 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<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", 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<ServerState> s, shared_ptr<Client
const auto& cmd = check_size_t<C_MenuItemInfoRequest_09>(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<ServerState> s, shared_ptr<Client
}
break;
case PROXY_DESTINATIONS_MENU_ID:
if (cmd.item_id == PROXY_DESTINATIONS_MENU_GO_BACK) {
case MenuID::PROXY_DESTINATIONS:
if (cmd.item_id == ProxyDestinationsMenuItemID::GO_BACK) {
send_ship_info(c, u"Return to the\nmain menu.");
} else {
try {
@@ -602,7 +588,7 @@ void process_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Client
}
break;
case QUEST_MENU_ID: {
case MenuID::QUEST: {
if (!s->quest_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<ServerState> s, shared_ptr<Client
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;
@@ -630,9 +707,9 @@ void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> 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<string> 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<size_t>(c->version));
@@ -642,18 +719,18 @@ void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> 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<Lobby> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<string, uint16_t>* dest = nullptr;
@@ -736,7 +822,7 @@ void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<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;
}
shared_ptr<Lobby> new_lobby;
try {
new_lobby = s->find_lobby(cmd.item_id);
@@ -942,7 +1065,7 @@ void process_game_list_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
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", 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<ServerState> s, shared_ptr<Client> c,
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
@@ -1018,7 +1192,7 @@ void process_quest_list_request(shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<PSOPlayerDataGCEp3>(data);
c->game_data.ep3_config.reset(new Ep3Config(pd3->ep3_config));
pd = reinterpret_cast<const PSOPlayerDataGC*>(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,
+7 -6
View File
@@ -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<Client> c, shared_ptr<ServerState> s) {
vector<S_GameMenuEntry<CharT>> 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<Client> c, shared_ptr<ServerState> 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<Client> c, shared_ptr<ServerState> 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;
}
+111 -2
View File
@@ -3,10 +3,11 @@
#include <string.h>
#include <memory>
#include <phosg/Network.hh>
#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<const JSONObject> config_json) {
const auto& d = config_json->as_dict();
shared_ptr<vector<MenuItem>> information_menu_pc(new vector<MenuItem>());
shared_ptr<vector<MenuItem>> information_menu_gc(new vector<MenuItem>());
shared_ptr<vector<u16string>> information_contents(new vector<u16string>());
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&) { }
}
+8 -4
View File
@@ -2,20 +2,21 @@
#include <atomic>
#include <map>
#include <unordered_map>
#include <string>
#include <memory>
#include <vector>
#include <phosg/JSON.hh>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
#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<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
std::shared_ptr<const FunctionCodeIndex> function_code_index;
std::shared_ptr<const DOLFileIndex> dol_file_index;
std::shared_ptr<const Ep3DataIndex> ep3_data_index;
std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const LevelTable> level_table;
@@ -111,4 +113,6 @@ struct ServerState {
void set_port_configuration(
const std::vector<PortConfiguration>& port_configs);
void create_menus(std::shared_ptr<const JSONObject> config_json);
};
+89
View File
@@ -1,5 +1,7 @@
#include "StaticGameData.hh"
#include <array>
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<const char*, 12> 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<const char*, 12> 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<const char*, 4> 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<char, 4> 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<const char*, 4> 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;
+8
View File
@@ -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);
+20
View File
@@ -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
+18
View File
@@ -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
+21
View File
@@ -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
+130
View File
@@ -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
+67 -50
View File
@@ -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<string, uint32_t label_writes(
# unordered_map<string, uint32_t> 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: