rewrite client function compiler
This commit is contained in:
+27
-25
@@ -1111,9 +1111,9 @@ ChatCommandDefinition cc_exit(
|
||||
a.c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE)) {
|
||||
co_await prepare_client_for_patches(a.c);
|
||||
auto s = a.c->require_server_state();
|
||||
shared_ptr<const CompiledFunctionCode> fn;
|
||||
shared_ptr<const ClientFunctionIndex::Function> fn;
|
||||
try {
|
||||
fn = s->function_code_index->get_patch("ExitAnywhere", a.c->specific_version);
|
||||
fn = s->client_functions->get("ExitAnywhere", a.c->specific_version);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
if (fn) {
|
||||
@@ -1571,7 +1571,7 @@ ChatCommandDefinition cc_loadchar(
|
||||
auto send_set_extended_player_info = [&a, &s]<typename CharT>(const CharT& char_file) -> asio::awaitable<void> {
|
||||
co_await prepare_client_for_patches(a.c);
|
||||
try {
|
||||
auto fn = s->function_code_index->get_patch("SetExtendedPlayerInfo", a.c->specific_version);
|
||||
auto fn = s->client_functions->get("SetExtendedPlayerInfo", a.c->specific_version);
|
||||
co_await send_function_call(a.c, fn, {}, &char_file, sizeof(CharT));
|
||||
auto l = a.c->lobby.lock();
|
||||
if (l) {
|
||||
@@ -1707,7 +1707,7 @@ ChatCommandDefinition cc_makeobj(
|
||||
|
||||
co_await prepare_client_for_patches(a.c);
|
||||
auto s = a.c->require_server_state();
|
||||
auto fn = s->function_code_index->get_patch("CreateObject", a.c->specific_version);
|
||||
auto fn = s->client_functions->get("CreateObject", a.c->specific_version);
|
||||
co_await send_function_call(a.c, fn, label_writes);
|
||||
});
|
||||
|
||||
@@ -1847,14 +1847,31 @@ ChatCommandDefinition cc_patch(
|
||||
try {
|
||||
auto s = a.c->require_server_state();
|
||||
// Note: We can't look this up before prepare_client_for_patches because specific_version may not be set
|
||||
auto fn = s->function_code_index->get_patch(patch_name, a.c->specific_version);
|
||||
auto fn = s->client_functions->get(patch_name, a.c->specific_version);
|
||||
|
||||
switch (fn->visibility) {
|
||||
case ClientFunctionIndex::Function::Visibility::DEBUG_ONLY:
|
||||
case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_ONLY:
|
||||
a.check_debug_enabled();
|
||||
break;
|
||||
case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY_WITH_CHEAT_MODE:
|
||||
a.check_cheats_enabled_or_allowed(true);
|
||||
break;
|
||||
case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY:
|
||||
case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_AND_CHAT_COMMAND:
|
||||
break;
|
||||
default:
|
||||
throw std::logic_error("Invalid client function visibility");
|
||||
}
|
||||
|
||||
auto ret = co_await send_function_call(a.c, fn, label_writes);
|
||||
if (fn->show_return_value) {
|
||||
send_text_message_fmt(a.c, "$C6Return value:$C7\nInt: {}\nHex: {:08X}\nFloat: {:g}",
|
||||
ret.return_value.load(), ret.return_value.load(), std::bit_cast<float>(ret.return_value.load()));
|
||||
}
|
||||
|
||||
} catch (const out_of_range&) {
|
||||
send_text_message(a.c, "$C6Invalid patch name");
|
||||
send_text_message(a.c, "$C6Invalid function");
|
||||
}
|
||||
co_return;
|
||||
});
|
||||
@@ -2277,15 +2294,10 @@ ChatCommandDefinition cc_readmem(
|
||||
|
||||
co_await prepare_client_for_patches(a.c);
|
||||
|
||||
shared_ptr<const CompiledFunctionCode> fn;
|
||||
shared_ptr<const ClientFunctionIndex::Function> fn;
|
||||
try {
|
||||
auto s = a.c->require_server_state();
|
||||
const char* function_name = is_dc(a.c->version())
|
||||
? "ReadMemoryWordDC"
|
||||
: is_gc(a.c->version())
|
||||
? "ReadMemoryWordGC"
|
||||
: "ReadMemoryWordX86";
|
||||
fn = s->function_code_index->name_to_function.at(function_name);
|
||||
fn = s->client_functions->get("ReadMemoryWord", a.c->specific_version);
|
||||
} catch (const out_of_range&) {
|
||||
throw precondition_failed("Invalid patch name");
|
||||
}
|
||||
@@ -3139,12 +3151,7 @@ ChatCommandDefinition cc_writemem(
|
||||
|
||||
try {
|
||||
auto s = a.c->require_server_state();
|
||||
const char* function_name = is_dc(a.c->version())
|
||||
? "WriteMemoryDC"
|
||||
: is_gc(a.c->version())
|
||||
? "WriteMemoryGC"
|
||||
: "WriteMemoryX86";
|
||||
auto fn = s->function_code_index->name_to_function.at(function_name);
|
||||
auto fn = s->client_functions->get("WriteMemory", a.c->specific_version);
|
||||
unordered_map<string, uint32_t> label_writes{{"dest_addr", addr}, {"size", data.size()}};
|
||||
co_await send_function_call(a.c, fn, label_writes, data.data(), data.size());
|
||||
} catch (const out_of_range&) {
|
||||
@@ -3184,12 +3191,7 @@ ChatCommandDefinition cc_nativecall(
|
||||
|
||||
try {
|
||||
auto s = a.c->require_server_state();
|
||||
const char* function_name = is_dc(a.c->version())
|
||||
? "CallNativeFunctionDC"
|
||||
: is_gc(a.c->version())
|
||||
? "CallNativeFunctionGC"
|
||||
: "CallNativeFunctionX86";
|
||||
auto fn = s->function_code_index->name_to_function.at(function_name);
|
||||
auto fn = s->client_functions->get("CallNativeFunction", a.c->specific_version);
|
||||
co_await send_function_call(a.c, fn, label_writes);
|
||||
} catch (const out_of_range&) {
|
||||
throw precondition_failed("Invalid patch name");
|
||||
|
||||
+1
-1
@@ -6,11 +6,11 @@
|
||||
#include "Account.hh"
|
||||
#include "AsyncUtils.hh"
|
||||
#include "Channel.hh"
|
||||
#include "ClientFunctionIndex.hh"
|
||||
#include "CommandFormats.hh"
|
||||
#include "Episode3/BattleRecord.hh"
|
||||
#include "Episode3/Tournament.hh"
|
||||
#include "FileContentsCache.hh"
|
||||
#include "FunctionCompiler.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "PatchFileIndex.hh"
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
#include "ClientFunctionIndex.hh"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Time.hh>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/SH4Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
|
||||
#include "CommandFormats.hh"
|
||||
#include "CommonFileFormats.hh"
|
||||
#include "Compression.hh"
|
||||
#include "Loggers.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
using Arch = ClientFunctionIndex::Function::Architecture;
|
||||
|
||||
const char* name_for_architecture(Arch arch) {
|
||||
switch (arch) {
|
||||
case Arch::SH4:
|
||||
return "SH-4";
|
||||
case Arch::POWERPC:
|
||||
return "PowerPC";
|
||||
case Arch::X86:
|
||||
return "x86";
|
||||
default:
|
||||
throw logic_error("invalid architecture");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t specific_version_for_architecture(Arch arch) {
|
||||
switch (arch) {
|
||||
case Arch::SH4:
|
||||
return SPECIFIC_VERSION_SH4_INDETERMINATE;
|
||||
case Arch::POWERPC:
|
||||
return SPECIFIC_VERSION_PPC_INDETERMINATE;
|
||||
case Arch::X86:
|
||||
return SPECIFIC_VERSION_X86_INDETERMINATE;
|
||||
default:
|
||||
throw logic_error("invalid architecture");
|
||||
}
|
||||
}
|
||||
|
||||
Arch architecture_for_specific_version(uint32_t specific_version) {
|
||||
if (specific_version == SPECIFIC_VERSION_SH4_INDETERMINATE) {
|
||||
return Arch::SH4;
|
||||
} else if (specific_version == SPECIFIC_VERSION_PPC_INDETERMINATE) {
|
||||
return Arch::POWERPC;
|
||||
} else if (specific_version == SPECIFIC_VERSION_X86_INDETERMINATE) {
|
||||
return Arch::X86;
|
||||
} else if (specific_version_is_dc(specific_version)) {
|
||||
return Arch::SH4;
|
||||
} else if (specific_version_is_gc(specific_version)) {
|
||||
return Arch::POWERPC;
|
||||
} else {
|
||||
return Arch::X86;
|
||||
}
|
||||
}
|
||||
|
||||
static inline std::string cache_key(const std::string& name, uint32_t specific_version) {
|
||||
return std::format("{}-{:08X}", name, specific_version);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T& get_with_sv_fallback(
|
||||
const std::unordered_map<std::string, T>& index, const std::string& name, uint32_t specific_version) {
|
||||
try {
|
||||
return index.at(cache_key(name, specific_version));
|
||||
} catch (const std::out_of_range&) {
|
||||
}
|
||||
uint32_t arch_specific_version = specific_version_for_architecture(architecture_for_specific_version(
|
||||
specific_version));
|
||||
if (arch_specific_version != specific_version) {
|
||||
try {
|
||||
return index.at(cache_key(name, arch_specific_version));
|
||||
} catch (const std::out_of_range&) {
|
||||
}
|
||||
}
|
||||
return index.at(name);
|
||||
}
|
||||
|
||||
template <bool BE>
|
||||
string ClientFunctionIndex::Function::generate_client_command_t(
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
uint32_t override_relocations_offset) const {
|
||||
using FooterT = RELFileFooterT<BE>;
|
||||
|
||||
FooterT footer;
|
||||
footer.num_relocations = this->relocation_deltas.size();
|
||||
footer.unused1.clear(0);
|
||||
footer.root_offset = this->entrypoint_offset_offset;
|
||||
footer.unused2.clear(0);
|
||||
|
||||
phosg::StringWriter w;
|
||||
if (!label_writes.empty()) {
|
||||
string modified_code = this->code;
|
||||
for (const auto& it : label_writes) {
|
||||
size_t offset = this->label_offsets.at(it.first);
|
||||
if (offset > modified_code.size() - 4) {
|
||||
throw runtime_error("label out of range");
|
||||
}
|
||||
*reinterpret_cast<U32T<FooterT::IsBE>*>(modified_code.data() + offset) = it.second;
|
||||
}
|
||||
w.write(modified_code);
|
||||
} else {
|
||||
w.write(this->code);
|
||||
}
|
||||
if (suffix_size) {
|
||||
w.write(suffix_data, suffix_size);
|
||||
}
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
|
||||
footer.relocations_offset = w.size();
|
||||
|
||||
// Always write at least 4 bytes even if there are no relocations
|
||||
if (this->relocation_deltas.empty()) {
|
||||
w.put_u32(0);
|
||||
}
|
||||
|
||||
if (override_relocations_offset) {
|
||||
footer.relocations_offset = override_relocations_offset;
|
||||
} else {
|
||||
for (uint16_t delta : this->relocation_deltas) {
|
||||
w.put<U16T<FooterT::IsBE>>(delta);
|
||||
}
|
||||
if (this->relocation_deltas.size() & 1) {
|
||||
w.put_u16(0);
|
||||
}
|
||||
}
|
||||
|
||||
w.put(footer);
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
string ClientFunctionIndex::Function::generate_client_command(
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
uint32_t override_relocations_offset) const {
|
||||
if (this->is_big_endian()) {
|
||||
return this->generate_client_command_t<true>(label_writes, suffix_data, suffix_size, override_relocations_offset);
|
||||
} else if ((this->arch == Architecture::X86) || (this->arch == Architecture::SH4)) {
|
||||
return this->generate_client_command_t<false>(label_writes, suffix_data, suffix_size, override_relocations_offset);
|
||||
} else {
|
||||
throw logic_error("invalid architecture");
|
||||
}
|
||||
}
|
||||
|
||||
static unordered_map<uint32_t, std::string> preprocess_function_code(const std::string& text) {
|
||||
std::unordered_set<uint32_t> all_specific_versions;
|
||||
struct Line {
|
||||
std::string text;
|
||||
std::unordered_map<uint32_t, size_t> new_specific_versions; // Nonempty iff line is a .versions directive
|
||||
bool enable_all_versions = false;
|
||||
};
|
||||
|
||||
std::vector<Line> lines;
|
||||
for (auto& line_text : phosg::split(text, '\n')) {
|
||||
auto& line = lines.emplace_back();
|
||||
line.text = std::move(line_text);
|
||||
|
||||
string stripped_line = line.text;
|
||||
phosg::strip_whitespace(stripped_line);
|
||||
|
||||
if (stripped_line == ".all_versions") {
|
||||
line.enable_all_versions = true;
|
||||
} else if (stripped_line.starts_with(".versions ")) {
|
||||
for (auto& vers_token : phosg::split(stripped_line.substr(10), ' ')) {
|
||||
phosg::strip_whitespace(vers_token);
|
||||
if (!vers_token.empty()) {
|
||||
uint32_t specific_version = specific_version_for_str(vers_token);
|
||||
size_t version_index = line.new_specific_versions.size();
|
||||
all_specific_versions.emplace(specific_version);
|
||||
line.new_specific_versions.emplace(std::move(specific_version), version_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const std::string empty_str = "";
|
||||
|
||||
unordered_map<uint32_t, std::string> ret;
|
||||
for (uint32_t specific_version : all_specific_versions) {
|
||||
std::deque<std::string> version_lines;
|
||||
bool include_current_line = true;
|
||||
size_t current_vers_index = all_specific_versions.size();
|
||||
for (size_t line_znum = 0; line_znum < lines.size(); line_znum++) {
|
||||
const auto& line = lines[line_znum];
|
||||
|
||||
if (line.enable_all_versions) {
|
||||
include_current_line = true;
|
||||
current_vers_index = all_specific_versions.size();
|
||||
version_lines.emplace_back(empty_str);
|
||||
|
||||
} else if (!line.new_specific_versions.empty()) {
|
||||
auto it = line.new_specific_versions.find(specific_version);
|
||||
if (it == line.new_specific_versions.end()) {
|
||||
include_current_line = false;
|
||||
current_vers_index = all_specific_versions.size();
|
||||
} else {
|
||||
include_current_line = true;
|
||||
current_vers_index = it->second;
|
||||
}
|
||||
version_lines.emplace_back(empty_str);
|
||||
|
||||
} else if (!include_current_line) {
|
||||
version_lines.emplace_back(empty_str);
|
||||
|
||||
} else {
|
||||
std::string line_text = line.text;
|
||||
size_t vers_offset = line_text.find("<VERS ");
|
||||
while (vers_offset != string::npos) {
|
||||
size_t end_offset = line_text.find('>', vers_offset + 6);
|
||||
if (end_offset == string::npos) {
|
||||
throw runtime_error(std::format("(version {}) (line {}) unterminated <VERS> replacement",
|
||||
str_for_specific_version(specific_version), line_znum + 1));
|
||||
}
|
||||
auto tokens = phosg::split(line_text.substr(vers_offset + 6, end_offset - vers_offset - 6), ' ');
|
||||
if (current_vers_index >= tokens.size()) {
|
||||
throw runtime_error(std::format("(version {}) (line {}) invalid <VERS> replacement",
|
||||
str_for_specific_version(specific_version), line_znum + 1));
|
||||
}
|
||||
line_text = line_text.substr(0, vers_offset) + tokens[current_vers_index] + line_text.substr(end_offset + 1);
|
||||
vers_offset = line_text.find("<VERS ");
|
||||
}
|
||||
version_lines.emplace_back(std::move(line_text));
|
||||
}
|
||||
}
|
||||
ret.emplace(specific_version, phosg::join(version_lines, "\n"));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ClientFunctionIndex::ClientFunctionIndex(const string& root_dir, bool raise_on_any_failure) {
|
||||
map<string, string> source_files;
|
||||
std::function<void(const std::string&)> add_directory = [&](const std::string& dir) -> void {
|
||||
for (const auto& item : std::filesystem::directory_iterator(dir)) {
|
||||
string item_name = item.path().filename().string();
|
||||
string item_path = dir.ends_with("/") ? (dir + item_name) : (dir + "/" + item_name);
|
||||
if (std::filesystem::is_directory(item_path)) {
|
||||
add_directory(item_path);
|
||||
} else if (item_path.ends_with(".s") && std::filesystem::is_regular_file(item_path)) {
|
||||
client_functions_log.debug_f("Adding {} from {}", item_name, item_path);
|
||||
if (!source_files.emplace(item_name, phosg::load_file(item_path)).second) {
|
||||
throw std::runtime_error(std::format("Duplicate source filename: {}", item_name));
|
||||
}
|
||||
} else if (item_path.ends_with(".bin") && std::filesystem::is_regular_file(item_path)) {
|
||||
client_functions_log.debug_f("Adding {} from {}", item_name, item_path);
|
||||
if (!source_files.emplace(item_name, phosg::load_file(item_path)).second) {
|
||||
throw std::runtime_error(std::format("Duplicate binary filename: {}", item_name));
|
||||
}
|
||||
} else {
|
||||
client_functions_log.debug_f("Ignoring {}", item_path);
|
||||
}
|
||||
}
|
||||
};
|
||||
add_directory(root_dir);
|
||||
|
||||
unordered_map<string, string> include_cache;
|
||||
uint32_t last_menu_item_id = 0;
|
||||
for (const auto& [source_filename, source] : source_files) {
|
||||
if (!source_filename.ends_with(".s")) {
|
||||
client_functions_log.debug_f("Skipping root compile for {} because it is not a .s file", source_filename);
|
||||
continue;
|
||||
}
|
||||
if (source_filename.ends_with(".inc.s")) {
|
||||
client_functions_log.debug_f("Skipping root compile for {} because it is an include", source_filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, std::string> preprocessed;
|
||||
try {
|
||||
preprocessed = preprocess_function_code(source);
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(std::format("({} preprocessing) {}", source_filename, e.what()));
|
||||
}
|
||||
|
||||
for (const auto& [specific_version, source] : preprocessed) {
|
||||
shared_ptr<Function> fn = make_shared<Function>();
|
||||
fn->short_name = source_filename.substr(0, source_filename.size() - 2);
|
||||
fn->specific_version = specific_version;
|
||||
fn->menu_item_id = ++last_menu_item_id;
|
||||
fn->arch = architecture_for_specific_version(fn->specific_version);
|
||||
|
||||
try {
|
||||
unordered_set<string> get_include_stack;
|
||||
function<string(const string&, uint32_t)> get_include_for_sv = [&include_cache, &source_files, &get_include_stack, &get_include_for_sv](const string& name, uint32_t specific_version) -> string {
|
||||
try {
|
||||
return get_with_sv_fallback(include_cache, name, specific_version);
|
||||
} catch (const std::out_of_range&) {
|
||||
}
|
||||
if (client_functions_log.should_log(phosg::LogLevel::L_DEBUG)) {
|
||||
client_functions_log.debug_f("({}) Include {}-{} needs to be compiled",
|
||||
get_include_stack.size(), name, str_for_specific_version(specific_version));
|
||||
}
|
||||
|
||||
auto it = source_files.find(name + ".inc.s");
|
||||
if (it != source_files.end()) {
|
||||
if (!get_include_stack.emplace(name).second) {
|
||||
throw runtime_error("Mutual recursion between includes: " + name);
|
||||
}
|
||||
for (const auto& [include_specific_version, include_source] : preprocess_function_code(it->second)) {
|
||||
ResourceDASM::EmulatorBase::AssembleResult ret;
|
||||
auto get_include = std::bind(get_include_for_sv, std::placeholders::_1, include_specific_version);
|
||||
switch (architecture_for_specific_version(include_specific_version)) {
|
||||
case Arch::POWERPC:
|
||||
ret = ResourceDASM::PPC32Emulator::assemble(include_source, get_include);
|
||||
break;
|
||||
case Arch::X86:
|
||||
ret = ResourceDASM::X86Emulator::assemble(include_source, get_include);
|
||||
break;
|
||||
case Arch::SH4:
|
||||
ret = ResourceDASM::SH4Emulator::assemble(include_source, get_include);
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
if (client_functions_log.should_log(phosg::LogLevel::L_DEBUG)) {
|
||||
client_functions_log.debug_f("({}) Compiled include {}-{}",
|
||||
get_include_stack.size(), name, str_for_specific_version(include_specific_version));
|
||||
}
|
||||
include_cache.emplace(cache_key(name, include_specific_version), std::move(ret.code));
|
||||
}
|
||||
get_include_stack.erase(name);
|
||||
|
||||
} else {
|
||||
it = source_files.find(name + ".inc.bin");
|
||||
if (it != source_files.end()) {
|
||||
include_cache.emplace(name, it->second).first->second;
|
||||
client_functions_log.debug_f("({}) Cached binary include {}", get_include_stack.size(), name);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return get_with_sv_fallback(include_cache, name, specific_version);
|
||||
} catch (const std::out_of_range&) {
|
||||
}
|
||||
throw runtime_error(std::format(
|
||||
"Data not found for include {} ({})", name, str_for_specific_version(specific_version)));
|
||||
};
|
||||
|
||||
try {
|
||||
ResourceDASM::EmulatorBase::AssembleResult assembled;
|
||||
auto get_include = std::bind(get_include_for_sv, std::placeholders::_1, specific_version);
|
||||
switch (fn->arch) {
|
||||
case Arch::POWERPC:
|
||||
assembled = ResourceDASM::PPC32Emulator::assemble(source, get_include);
|
||||
break;
|
||||
case Arch::X86:
|
||||
assembled = ResourceDASM::X86Emulator::assemble(source, get_include);
|
||||
break;
|
||||
case Arch::SH4:
|
||||
assembled = ResourceDASM::SH4Emulator::assemble(source, get_include);
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("invalid architecture");
|
||||
}
|
||||
|
||||
fn->code = std::move(assembled.code);
|
||||
fn->label_offsets = std::move(assembled.label_offsets);
|
||||
for (const auto& [key, value] : assembled.metadata_keys) {
|
||||
if (key == "visibility") {
|
||||
if (value == "hidden") {
|
||||
fn->visibility = Function::Visibility::DEBUG_ONLY;
|
||||
} else if (value == "cheat") {
|
||||
fn->visibility = Function::Visibility::CHAT_COMMAND_ONLY_WITH_CHEAT_MODE;
|
||||
} else if (value == "chat") {
|
||||
fn->visibility = Function::Visibility::CHAT_COMMAND_ONLY;
|
||||
} else if (value == "menu") {
|
||||
fn->visibility = Function::Visibility::PATCHES_MENU_ONLY;
|
||||
} else if (value == "all") {
|
||||
fn->visibility = Function::Visibility::PATCHES_MENU_AND_CHAT_COMMAND;
|
||||
} else {
|
||||
throw std::runtime_error("Invalid visibility value");
|
||||
}
|
||||
} else if (key == "key") {
|
||||
fn->short_name = value;
|
||||
} else if (key == "name") {
|
||||
fn->long_name = value;
|
||||
} else if (key == "description") {
|
||||
fn->description = value;
|
||||
} else if (key == "client_flag") {
|
||||
fn->client_flag = stoull(value, nullptr, 0);
|
||||
} else if (key == "show_return_value") {
|
||||
fn->show_return_value = true;
|
||||
} else {
|
||||
throw runtime_error("unknown metadata key: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fn->entrypoint_offset_offset = fn->label_offsets.at("entry_ptr");
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error("code does not contain entry_ptr label");
|
||||
}
|
||||
|
||||
set<uint32_t> reloc_indexes;
|
||||
for (const auto& it : fn->label_offsets) {
|
||||
if (it.first.starts_with("reloc")) {
|
||||
reloc_indexes.emplace(it.second / 4);
|
||||
}
|
||||
}
|
||||
uint32_t prev_index = 0;
|
||||
for (const auto& it : reloc_indexes) {
|
||||
uint32_t delta = it - prev_index;
|
||||
if (delta > 0xFFFF) {
|
||||
throw runtime_error("relocation delta too far away");
|
||||
}
|
||||
fn->relocation_deltas.emplace_back(delta);
|
||||
prev_index = it;
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
if (raise_on_any_failure) {
|
||||
throw;
|
||||
}
|
||||
client_functions_log.warning_f("Failed to compile function {} ({}): {}",
|
||||
fn->short_name, str_for_specific_version(specific_version), e.what());
|
||||
}
|
||||
|
||||
auto key = cache_key(fn->short_name, specific_version);
|
||||
if (!this->all_functions.emplace(key, fn).second) {
|
||||
throw std::runtime_error("Duplicate function key: " + key);
|
||||
}
|
||||
this->functions_by_specific_version[specific_version].emplace(key, fn);
|
||||
this->functions_by_menu_item_id.emplace(fn->menu_item_id, fn);
|
||||
|
||||
client_functions_log.debug_f("Compiled function {} ({}; {}; {})",
|
||||
fn->short_name, str_for_specific_version(fn->specific_version), name_for_architecture(fn->arch),
|
||||
phosg::name_for_enum(fn->visibility));
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(std::format(
|
||||
"({}-{}) {}", fn->short_name, str_for_specific_version(specific_version), e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const Menu> ClientFunctionIndex::patch_switches_menu(
|
||||
uint32_t specific_version,
|
||||
const std::unordered_set<std::string>& server_auto_patches_enabled,
|
||||
const std::unordered_set<std::string>& client_auto_patches_enabled) const {
|
||||
auto ret = make_shared<Menu>(MenuID::PATCH_SWITCHES, "Patches");
|
||||
ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
|
||||
|
||||
auto map_it = this->functions_by_specific_version.find(specific_version);
|
||||
if (map_it != this->functions_by_specific_version.end()) {
|
||||
for (auto [name, fn] : map_it->second) {
|
||||
if (fn->appears_in_patches_menu() && server_auto_patches_enabled.count(fn->short_name)) {
|
||||
string item_text;
|
||||
item_text.push_back(client_auto_patches_enabled.count(fn->short_name) ? '*' : '-');
|
||||
item_text += fn->long_name.empty() ? fn->short_name : fn->long_name;
|
||||
ret->items.emplace_back(
|
||||
fn->menu_item_id, item_text, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool ClientFunctionIndex::patch_menu_empty(uint32_t specific_version) const {
|
||||
uint32_t mask = specific_version_is_indeterminate(specific_version) ? 0xFF000000 : 0xFFFFFFFF;
|
||||
auto it = this->functions_by_specific_version.lower_bound(specific_version & mask);
|
||||
return ((it == this->functions_by_specific_version.end()) || ((it->first & mask) != (specific_version & mask)));
|
||||
}
|
||||
|
||||
std::shared_ptr<const ClientFunctionIndex::Function> ClientFunctionIndex::get(
|
||||
const std::string& name, uint32_t specific_version) const {
|
||||
return get_with_sv_fallback(this->all_functions, name, specific_version);
|
||||
}
|
||||
|
||||
std::shared_ptr<const ClientFunctionIndex::Function> ClientFunctionIndex::get(
|
||||
const std::string& name, Arch arch) const {
|
||||
return get_with_sv_fallback(this->all_functions, name, specific_version_for_architecture(arch));
|
||||
}
|
||||
|
||||
std::shared_ptr<const ClientFunctionIndex::Function> ClientFunctionIndex::get(const std::string& name) const {
|
||||
return this->all_functions.at(name);
|
||||
}
|
||||
|
||||
std::shared_ptr<const ClientFunctionIndex::Function> ClientFunctionIndex::get_by_menu_item_id(
|
||||
uint32_t menu_item_id) const {
|
||||
return this->functions_by_menu_item_id.at(menu_item_id);
|
||||
}
|
||||
|
||||
uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum) {
|
||||
static unordered_map<uint32_t, uint32_t> checksum_to_specific_version;
|
||||
if (checksum_to_specific_version.empty()) {
|
||||
struct {
|
||||
char system_code = 'G';
|
||||
char game_code1 = 'P';
|
||||
char game_code2;
|
||||
char region_code;
|
||||
char developer_code1 = '8';
|
||||
char developer_code2 = 'P';
|
||||
uint8_t disc_number = 0;
|
||||
uint8_t version_code;
|
||||
} __attribute__((packed)) data;
|
||||
for (const char* game_code2 = "OS"; *game_code2; game_code2++) {
|
||||
data.game_code2 = *game_code2;
|
||||
for (const char* region_code = "JEP"; *region_code; region_code++) {
|
||||
data.region_code = *region_code;
|
||||
for (uint8_t version_code = 0; version_code < 8; version_code++) {
|
||||
data.version_code = version_code;
|
||||
uint32_t checksum = phosg::crc32(&data, sizeof(data));
|
||||
uint32_t specific_version = 0x33000030 | (*game_code2 << 16) | (*region_code << 8) | version_code;
|
||||
if (!checksum_to_specific_version.emplace(checksum, specific_version).second) {
|
||||
throw logic_error("multiple specific_versions have same header checksum");
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// Generate entries for Trial Editions
|
||||
data.region_code = 'J';
|
||||
data.system_code = 'D';
|
||||
data.version_code = 0;
|
||||
uint32_t checksum = phosg::crc32(&data, sizeof(data));
|
||||
uint32_t specific_version = 0x33004A54 | (*game_code2 << 16);
|
||||
if (!checksum_to_specific_version.emplace(checksum, specific_version).second) {
|
||||
throw logic_error("multiple specific_versions have same header checksum");
|
||||
}
|
||||
data.system_code = 'G';
|
||||
}
|
||||
}
|
||||
}
|
||||
return checksum_to_specific_version.at(header_checksum);
|
||||
}
|
||||
|
||||
template <>
|
||||
const char* phosg::name_for_enum<ClientFunctionIndex::Function::Visibility>(
|
||||
ClientFunctionIndex::Function::Visibility vis) {
|
||||
switch (vis) {
|
||||
case ClientFunctionIndex::Function::Visibility::DEBUG_ONLY:
|
||||
return "DEBUG_ONLY";
|
||||
case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY_WITH_CHEAT_MODE:
|
||||
return "CHAT_COMMAND_ONLY_WITH_CHEAT_MODE";
|
||||
case ClientFunctionIndex::Function::Visibility::CHAT_COMMAND_ONLY:
|
||||
return "CHAT_COMMAND_ONLY";
|
||||
case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_ONLY:
|
||||
return "PATCHES_MENU_ONLY";
|
||||
case ClientFunctionIndex::Function::Visibility::PATCHES_MENU_AND_CHAT_COMMAND:
|
||||
return "PATCHES_MENU_AND_CHAT_COMMAND";
|
||||
default:
|
||||
throw std::logic_error("Invalid client function visibility");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "Menu.hh"
|
||||
|
||||
class ClientFunctionIndex {
|
||||
public:
|
||||
struct Function {
|
||||
enum class Architecture {
|
||||
UNKNOWN = 0,
|
||||
POWERPC, // GC
|
||||
X86, // PC, XB, BB
|
||||
SH4, // Dreamcast
|
||||
};
|
||||
Architecture arch = Architecture::UNKNOWN;
|
||||
std::string code;
|
||||
std::vector<uint16_t> relocation_deltas;
|
||||
std::unordered_map<std::string, uint32_t> label_offsets;
|
||||
uint32_t entrypoint_offset_offset = 0;
|
||||
std::string short_name; // Based on filename
|
||||
std::string long_name; // From .meta name directive
|
||||
std::string description; // From .meta description directive
|
||||
uint64_t client_flag = 0; // From .meta client_flag directive
|
||||
uint32_t menu_item_id = 0;
|
||||
enum class Visibility {
|
||||
DEBUG_ONLY = 0,
|
||||
CHAT_COMMAND_ONLY_WITH_CHEAT_MODE,
|
||||
CHAT_COMMAND_ONLY,
|
||||
PATCHES_MENU_ONLY,
|
||||
PATCHES_MENU_AND_CHAT_COMMAND,
|
||||
};
|
||||
Visibility visibility;
|
||||
bool show_return_value = false;
|
||||
uint32_t specific_version;
|
||||
|
||||
inline bool appears_in_patches_menu() const {
|
||||
return (this->visibility == Visibility::PATCHES_MENU_ONLY) ||
|
||||
(this->visibility == Visibility::PATCHES_MENU_AND_CHAT_COMMAND);
|
||||
}
|
||||
inline bool allowed_via_chat_command(bool cheat_mode_enabled) const {
|
||||
return (cheat_mode_enabled && (this->visibility == Visibility::CHAT_COMMAND_ONLY_WITH_CHEAT_MODE)) ||
|
||||
(this->visibility == Visibility::CHAT_COMMAND_ONLY) ||
|
||||
(this->visibility == Visibility::PATCHES_MENU_AND_CHAT_COMMAND);
|
||||
}
|
||||
|
||||
inline bool is_big_endian() const {
|
||||
return (this->arch == Architecture::POWERPC);
|
||||
}
|
||||
|
||||
template <bool BE>
|
||||
std::string generate_client_command_t(
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes,
|
||||
const void* suffix_data = nullptr,
|
||||
size_t suffix_size = 0,
|
||||
uint32_t override_relocations_offset = 0) const;
|
||||
std::string generate_client_command(
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes = {},
|
||||
const void* suffix_data = nullptr,
|
||||
size_t suffix_size = 0,
|
||||
uint32_t override_relocations_offset = 0) const;
|
||||
};
|
||||
|
||||
ClientFunctionIndex() = default;
|
||||
ClientFunctionIndex(const std::string& directory, bool raise_on_any_failure);
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<Function>> all_functions; // Key is "PatchName-SpecificVersion"
|
||||
std::map<uint32_t, std::map<std::string, std::shared_ptr<Function>>> functions_by_specific_version;
|
||||
std::map<uint32_t, std::shared_ptr<Function>> functions_by_menu_item_id;
|
||||
|
||||
std::shared_ptr<const Menu> patch_switches_menu(
|
||||
uint32_t specific_version,
|
||||
const std::unordered_set<std::string>& server_auto_patches_enabled,
|
||||
const std::unordered_set<std::string>& client_auto_patches_enabled) const;
|
||||
bool patch_menu_empty(uint32_t specific_version) const;
|
||||
|
||||
std::shared_ptr<const Function> get(const std::string& name, uint32_t specific_version) const;
|
||||
std::shared_ptr<const Function> get(const std::string& name, Function::Architecture arch) const;
|
||||
std::shared_ptr<const Function> get(const std::string& name) const;
|
||||
std::shared_ptr<const Function> get_by_menu_item_id(uint32_t menu_item_id) const;
|
||||
};
|
||||
|
||||
const char* name_for_architecture(ClientFunctionIndex::Function::Architecture arch);
|
||||
uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum);
|
||||
|
||||
template <>
|
||||
const char* phosg::name_for_enum<ClientFunctionIndex::Function::Visibility>(
|
||||
ClientFunctionIndex::Function::Visibility vis);
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "DOLFileIndex.hh"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Time.hh>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/SH4Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
|
||||
#include "CommandFormats.hh"
|
||||
#include "CommonFileFormats.hh"
|
||||
#include "Compression.hh"
|
||||
#include "Loggers.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
DOLFileIndex::DOLFileIndex(const string& directory) {
|
||||
if (!std::filesystem::is_directory(directory)) {
|
||||
client_functions_log.info_f("DOL file directory is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
auto menu = make_shared<Menu>(MenuID::PROGRAMS, "Programs");
|
||||
this->menu = menu;
|
||||
menu->items.emplace_back(ProgramsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
|
||||
|
||||
uint32_t next_menu_item_id = 0;
|
||||
for (const auto& item : std::filesystem::directory_iterator(directory)) {
|
||||
string filename = item.path().filename().string();
|
||||
bool is_dol = filename.ends_with(".dol");
|
||||
bool is_compressed_dol = filename.ends_with(".dol.prs");
|
||||
if (!is_dol && !is_compressed_dol) {
|
||||
continue;
|
||||
}
|
||||
string name = filename.substr(0, filename.size() - (is_compressed_dol ? 8 : 4));
|
||||
|
||||
try {
|
||||
auto dol = make_shared<File>();
|
||||
dol->menu_item_id = next_menu_item_id++;
|
||||
dol->name = name;
|
||||
|
||||
string path = directory + "/" + filename;
|
||||
string file_data = phosg::load_file(path);
|
||||
|
||||
string description;
|
||||
if (is_compressed_dol) {
|
||||
size_t decompressed_size = prs_decompress_size(file_data);
|
||||
|
||||
phosg::StringWriter w;
|
||||
w.put_u32b(file_data.size());
|
||||
w.put_u32b(decompressed_size);
|
||||
w.write(file_data);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
dol->data = std::move(w.str());
|
||||
|
||||
string compressed_size_str = phosg::format_size(file_data.size());
|
||||
string decompressed_size_str = phosg::format_size(decompressed_size);
|
||||
client_functions_log.debug_f("Loaded compressed DOL file {} ({} -> {})",
|
||||
dol->name, compressed_size_str, decompressed_size_str);
|
||||
description = std::format("$C6{}$C7\n{}\n{} (orig)", dol->name, compressed_size_str, decompressed_size_str);
|
||||
|
||||
} else {
|
||||
phosg::StringWriter w;
|
||||
w.put_u32b(0);
|
||||
w.put_u32b(file_data.size());
|
||||
w.write(file_data);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
dol->data = std::move(w.str());
|
||||
|
||||
string size_str = phosg::format_size(dol->data.size());
|
||||
client_functions_log.debug_f("Loaded DOL file {} ({})", filename, size_str);
|
||||
description = std::format("$C6{}$C7\n{}", dol->name, size_str);
|
||||
}
|
||||
|
||||
this->name_to_file.emplace(dol->name, dol);
|
||||
this->item_id_to_file.emplace_back(dol);
|
||||
|
||||
menu->items.emplace_back(dol->menu_item_id, dol->name, description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE);
|
||||
|
||||
} catch (const exception& e) {
|
||||
client_functions_log.warning_f("Failed to load DOL file {}: {}", filename, e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "Menu.hh"
|
||||
|
||||
struct DOLFileIndex {
|
||||
struct File {
|
||||
uint32_t menu_item_id;
|
||||
std::string name;
|
||||
std::string data;
|
||||
bool is_compressed;
|
||||
};
|
||||
|
||||
std::vector<std::shared_ptr<File>> item_id_to_file;
|
||||
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
||||
std::shared_ptr<const Menu> menu;
|
||||
|
||||
DOLFileIndex() = default;
|
||||
explicit DOLFileIndex(const std::string& directory);
|
||||
|
||||
inline bool empty() const {
|
||||
return this->name_to_file.empty() && this->item_id_to_file.empty();
|
||||
}
|
||||
};
|
||||
@@ -1,612 +0,0 @@
|
||||
#include "FunctionCompiler.hh"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Time.hh>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/SH4Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
|
||||
#include "CommandFormats.hh"
|
||||
#include "CommonFileFormats.hh"
|
||||
#include "Compression.hh"
|
||||
#include "Loggers.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
const char* name_for_architecture(CompiledFunctionCode::Architecture arch) {
|
||||
switch (arch) {
|
||||
case CompiledFunctionCode::Architecture::POWERPC:
|
||||
return "PowerPC";
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
return "x86";
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
return "SH-4";
|
||||
default:
|
||||
throw logic_error("invalid architecture");
|
||||
}
|
||||
}
|
||||
|
||||
template <bool BE>
|
||||
string CompiledFunctionCode::generate_client_command_t(
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
uint32_t override_relocations_offset) const {
|
||||
using FooterT = RELFileFooterT<BE>;
|
||||
|
||||
FooterT footer;
|
||||
footer.num_relocations = this->relocation_deltas.size();
|
||||
footer.unused1.clear(0);
|
||||
footer.root_offset = this->entrypoint_offset_offset;
|
||||
footer.unused2.clear(0);
|
||||
|
||||
phosg::StringWriter w;
|
||||
if (!label_writes.empty()) {
|
||||
string modified_code = this->code;
|
||||
for (const auto& it : label_writes) {
|
||||
size_t offset = this->label_offsets.at(it.first);
|
||||
if (offset > modified_code.size() - 4) {
|
||||
throw runtime_error("label out of range");
|
||||
}
|
||||
*reinterpret_cast<U32T<FooterT::IsBE>*>(modified_code.data() + offset) = it.second;
|
||||
}
|
||||
w.write(modified_code);
|
||||
} else {
|
||||
w.write(this->code);
|
||||
}
|
||||
if (suffix_size) {
|
||||
w.write(suffix_data, suffix_size);
|
||||
}
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
|
||||
footer.relocations_offset = w.size();
|
||||
|
||||
// Always write at least 4 bytes even if there are no relocations
|
||||
if (this->relocation_deltas.empty()) {
|
||||
w.put_u32(0);
|
||||
}
|
||||
|
||||
if (override_relocations_offset) {
|
||||
footer.relocations_offset = override_relocations_offset;
|
||||
} else {
|
||||
for (uint16_t delta : this->relocation_deltas) {
|
||||
w.put<U16T<FooterT::IsBE>>(delta);
|
||||
}
|
||||
if (this->relocation_deltas.size() & 1) {
|
||||
w.put_u16(0);
|
||||
}
|
||||
}
|
||||
|
||||
w.put(footer);
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
string CompiledFunctionCode::generate_client_command(
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
uint32_t override_relocations_offset) const {
|
||||
if (this->arch == Architecture::POWERPC) {
|
||||
return this->generate_client_command_t<true>(label_writes, suffix_data, suffix_size, override_relocations_offset);
|
||||
} else if ((this->arch == Architecture::X86) || (this->arch == Architecture::SH4)) {
|
||||
return this->generate_client_command_t<false>(label_writes, suffix_data, suffix_size, override_relocations_offset);
|
||||
} else {
|
||||
throw logic_error("invalid architecture");
|
||||
}
|
||||
}
|
||||
|
||||
bool CompiledFunctionCode::is_big_endian() const {
|
||||
return (this->arch == Architecture::POWERPC);
|
||||
}
|
||||
|
||||
static unordered_map<uint32_t, std::string> preprocess_function_code(const std::string& text) {
|
||||
auto parse_specific_version_list = +[](std::string&& text) -> vector<uint32_t> {
|
||||
phosg::strip_whitespace(text);
|
||||
vector<uint32_t> ret;
|
||||
for (auto& vers_token : phosg::split(text, ' ')) {
|
||||
phosg::strip_whitespace(vers_token);
|
||||
if (vers_token.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (vers_token.size() != 4) {
|
||||
throw std::runtime_error("invalid specific_version: " + vers_token);
|
||||
}
|
||||
ret.emplace_back(*reinterpret_cast<const be_uint32_t*>(vers_token.data()));
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Find a .versions directive and populate specific_versions
|
||||
vector<uint32_t> specific_versions;
|
||||
auto lines = phosg::split(text, '\n');
|
||||
for (auto& line : lines) {
|
||||
if (line.starts_with(".versions ")) {
|
||||
if (!specific_versions.empty()) {
|
||||
throw std::runtime_error("multiple .versions directives in file");
|
||||
}
|
||||
specific_versions = parse_specific_version_list(line.substr(10));
|
||||
if (specific_versions.empty()) {
|
||||
throw std::runtime_error(".versions directive does not specify any versions");
|
||||
}
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no .versions directive, just return the text as-is
|
||||
if (specific_versions.empty()) {
|
||||
return {{0, std::move(text)}};
|
||||
}
|
||||
|
||||
vector<deque<string>> version_lines;
|
||||
version_lines.resize(specific_versions.size());
|
||||
|
||||
size_t line_num = 1;
|
||||
vector<uint32_t> current_only_versions;
|
||||
unordered_set<uint32_t> current_only_versions_set;
|
||||
auto add_blank_line = [&]() -> void {
|
||||
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
|
||||
version_lines[vers_index].emplace_back("");
|
||||
}
|
||||
};
|
||||
for (auto& line : lines) {
|
||||
phosg::strip_whitespace(line);
|
||||
if (line.starts_with(".only_versions ")) {
|
||||
current_only_versions = parse_specific_version_list(line.substr(15));
|
||||
current_only_versions_set.clear();
|
||||
for (uint32_t specific_version : current_only_versions) {
|
||||
current_only_versions_set.emplace(specific_version);
|
||||
}
|
||||
add_blank_line();
|
||||
|
||||
} else if (line == ".all_versions") {
|
||||
current_only_versions.clear();
|
||||
current_only_versions_set.clear();
|
||||
add_blank_line();
|
||||
|
||||
} else {
|
||||
size_t vers_offset = line.find("<VERS ");
|
||||
if (vers_offset == string::npos) {
|
||||
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
|
||||
if (current_only_versions.empty() || current_only_versions_set.count(specific_versions[vers_index])) {
|
||||
version_lines[vers_index].emplace_back(line);
|
||||
} else {
|
||||
version_lines[vers_index].emplace_back("");
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
size_t token_index = 0;
|
||||
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
|
||||
if (current_only_versions.empty() || current_only_versions_set.count(specific_versions[vers_index])) {
|
||||
string version_line = line;
|
||||
size_t vers_offset = line.find("<VERS ");
|
||||
while (vers_offset != string::npos) {
|
||||
size_t end_offset = version_line.find('>', vers_offset + 6);
|
||||
if (end_offset == string::npos) {
|
||||
throw runtime_error(std::format("(line {}) unterminated <VERS> replacement", line_num));
|
||||
}
|
||||
auto tokens = phosg::split(version_line.substr(vers_offset + 6, end_offset - vers_offset - 6), ' ');
|
||||
if (tokens.size() <= token_index) {
|
||||
throw runtime_error(std::format("(line {}) invalid <VERS> replacement", line_num));
|
||||
}
|
||||
version_line = version_line.substr(0, vers_offset) + tokens.at(token_index) + version_line.substr(end_offset + 1);
|
||||
vers_offset = version_line.find("<VERS ");
|
||||
}
|
||||
version_lines[vers_index].emplace_back(version_line);
|
||||
token_index++;
|
||||
} else {
|
||||
version_lines[vers_index].emplace_back("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line_num++;
|
||||
}
|
||||
|
||||
unordered_map<uint32_t, string> ret;
|
||||
for (size_t z = 0; z < specific_versions.size(); z++) {
|
||||
ret.emplace(specific_versions[z], phosg::join(version_lines.at(z), "\n"));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
|
||||
CompiledFunctionCode::Architecture arch,
|
||||
const string& function_directory,
|
||||
const string& system_directory,
|
||||
const string& name,
|
||||
const string& text,
|
||||
bool raise_on_any_failure) {
|
||||
unordered_set<string> get_include_stack;
|
||||
function<string(const string&)> get_include = [&](const string& name) -> string {
|
||||
const char* arch_name_token;
|
||||
switch (arch) {
|
||||
case CompiledFunctionCode::Architecture::POWERPC:
|
||||
arch_name_token = "ppc";
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
arch_name_token = "x86";
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
arch_name_token = "sh4";
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
|
||||
// Look in the function directory first, then the system directory
|
||||
string asm_filename = std::format("{}/{}.{}.inc.s", function_directory, name, arch_name_token);
|
||||
if (!std::filesystem::is_regular_file(asm_filename)) {
|
||||
asm_filename = std::format("{}/{}.{}.inc.s", system_directory, name, arch_name_token);
|
||||
}
|
||||
if (std::filesystem::is_regular_file(asm_filename)) {
|
||||
if (!get_include_stack.emplace(name).second) {
|
||||
throw runtime_error("mutual recursion between includes: " + name);
|
||||
}
|
||||
ResourceDASM::EmulatorBase::AssembleResult ret;
|
||||
switch (arch) {
|
||||
case CompiledFunctionCode::Architecture::POWERPC:
|
||||
ret = ResourceDASM::PPC32Emulator::assemble(phosg::load_file(asm_filename), get_include);
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
ret = ResourceDASM::X86Emulator::assemble(phosg::load_file(asm_filename), get_include);
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
ret = ResourceDASM::SH4Emulator::assemble(phosg::load_file(asm_filename), get_include);
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
get_include_stack.erase(name);
|
||||
return ret.code;
|
||||
}
|
||||
|
||||
string bin_filename = function_directory + "/" + name + ".inc.bin";
|
||||
if (std::filesystem::is_regular_file(bin_filename)) {
|
||||
return phosg::load_file(bin_filename);
|
||||
}
|
||||
bin_filename = system_directory + "/" + name + ".inc.bin";
|
||||
if (std::filesystem::is_regular_file(bin_filename)) {
|
||||
return phosg::load_file(bin_filename);
|
||||
}
|
||||
throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")");
|
||||
};
|
||||
|
||||
auto version_texts = preprocess_function_code(text);
|
||||
|
||||
vector<shared_ptr<CompiledFunctionCode>> ret;
|
||||
for (const auto& [specific_version, version_text] : version_texts) {
|
||||
try {
|
||||
ResourceDASM::EmulatorBase::AssembleResult assembled;
|
||||
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
|
||||
assembled = ResourceDASM::PPC32Emulator::assemble(version_text, get_include);
|
||||
} else if (arch == CompiledFunctionCode::Architecture::X86) {
|
||||
assembled = ResourceDASM::X86Emulator::assemble(version_text, get_include);
|
||||
} else if (arch == CompiledFunctionCode::Architecture::SH4) {
|
||||
assembled = ResourceDASM::SH4Emulator::assemble(version_text, get_include);
|
||||
} else {
|
||||
throw runtime_error("invalid architecture");
|
||||
}
|
||||
|
||||
auto compiled = ret.emplace_back(make_shared<CompiledFunctionCode>());
|
||||
compiled->arch = arch;
|
||||
compiled->short_name = name;
|
||||
compiled->specific_version = specific_version;
|
||||
compiled->code = std::move(assembled.code);
|
||||
compiled->label_offsets = std::move(assembled.label_offsets);
|
||||
for (const auto& it : assembled.metadata_keys) {
|
||||
if (it.first == "hide_from_patches_menu") {
|
||||
compiled->hide_from_patches_menu = true;
|
||||
} else if (it.first == "name") {
|
||||
compiled->long_name = it.second;
|
||||
} else if (it.first == "description") {
|
||||
compiled->description = it.second;
|
||||
} else if (it.first == "client_flag") {
|
||||
compiled->client_flag = stoull(it.second, nullptr, 0);
|
||||
} else if (it.first == "show_return_value") {
|
||||
compiled->show_return_value = true;
|
||||
} else {
|
||||
throw runtime_error("unknown metadata key: " + it.first);
|
||||
}
|
||||
}
|
||||
|
||||
set<uint32_t> reloc_indexes;
|
||||
for (const auto& it : compiled->label_offsets) {
|
||||
if (it.first.starts_with("reloc")) {
|
||||
reloc_indexes.emplace(it.second / 4);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
compiled->entrypoint_offset_offset = compiled->label_offsets.at("entry_ptr");
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error("code does not contain entry_ptr label");
|
||||
}
|
||||
|
||||
uint32_t prev_index = 0;
|
||||
for (const auto& it : reloc_indexes) {
|
||||
uint32_t delta = it - prev_index;
|
||||
if (delta > 0xFFFF) {
|
||||
throw runtime_error("relocation delta too far away");
|
||||
}
|
||||
compiled->relocation_deltas.emplace_back(delta);
|
||||
prev_index = it;
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
string version_str = specific_version ? (" (" + str_for_specific_version(specific_version) + ")") : "";
|
||||
if (raise_on_any_failure) {
|
||||
throw;
|
||||
}
|
||||
function_compiler_log.warning_f("Failed to compile function {}{}: {}", name, version_str, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FunctionCodeIndex::FunctionCodeIndex(const string& directory, bool raise_on_any_failure) {
|
||||
string system_dir_path = directory.ends_with("/") ? (directory + "System") : (directory + "/System");
|
||||
|
||||
uint32_t next_menu_item_id = 1;
|
||||
for (const auto& item : std::filesystem::directory_iterator(directory)) {
|
||||
string subdir_name = item.path().filename().string();
|
||||
string subdir_path = directory.ends_with("/") ? (directory + subdir_name) : (directory + "/" + subdir_name);
|
||||
|
||||
auto add_file = [&](string filename) -> void {
|
||||
try {
|
||||
if (!filename.ends_with(".s")) {
|
||||
return;
|
||||
}
|
||||
|
||||
string name = filename.substr(0, filename.size() - 2);
|
||||
if (name.ends_with(".inc")) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_patch = name.ends_with(".patch");
|
||||
if (is_patch) {
|
||||
name.resize(name.size() - 6);
|
||||
}
|
||||
|
||||
// Figure out the version or specific_version
|
||||
CompiledFunctionCode::Architecture arch = CompiledFunctionCode::Architecture::UNKNOWN;
|
||||
uint32_t specific_version = 0;
|
||||
string short_name = name;
|
||||
if (name.ends_with(".ppc")) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (name.ends_with(".x86")) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (name.ends_with(".sh4")) {
|
||||
arch = CompiledFunctionCode::Architecture::SH4;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (is_patch && (name.size() >= 5) && (name[name.size() - 5] == '.')) {
|
||||
specific_version = (name[name.size() - 4] << 24) | (name[name.size() - 3] << 16) | (name[name.size() - 2] << 8) | name[name.size() - 1];
|
||||
if (specific_version_is_dc(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::SH4;
|
||||
} else if (specific_version_is_gc(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
} else if (specific_version_is_pc_v2(specific_version) ||
|
||||
specific_version_is_xb(specific_version) ||
|
||||
specific_version_is_bb(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
} else {
|
||||
throw runtime_error("unable to determine architecture from specific_version");
|
||||
}
|
||||
short_name = name.substr(0, name.size() - 5);
|
||||
}
|
||||
|
||||
if (arch == CompiledFunctionCode::Architecture::UNKNOWN) {
|
||||
throw runtime_error("unable to determine architecture");
|
||||
}
|
||||
|
||||
string path = subdir_path + "/" + filename;
|
||||
string text = phosg::load_file(path);
|
||||
for (auto code : compile_function_code(arch, subdir_path, system_dir_path, name, text, raise_on_any_failure)) {
|
||||
if (code->specific_version == 0) {
|
||||
code->specific_version = specific_version;
|
||||
}
|
||||
code->source_path = path;
|
||||
code->short_name = short_name;
|
||||
this->name_to_function.emplace(name, code);
|
||||
if (is_patch) {
|
||||
code->menu_item_id = next_menu_item_id++;
|
||||
this->menu_item_id_and_specific_version_to_patch_function.emplace(
|
||||
static_cast<uint64_t>(code->menu_item_id) << 32 | code->specific_version, code);
|
||||
this->name_and_specific_version_to_patch_function.emplace(
|
||||
std::format("{}-{:08X}", code->short_name, code->specific_version), code);
|
||||
}
|
||||
|
||||
string patch_prefix = is_patch ? std::format("[{:08X}] ", code->menu_item_id) : "";
|
||||
function_compiler_log.debug_f("Compiled function {}{} ({}; {})",
|
||||
patch_prefix, name, str_for_specific_version(code->specific_version), name_for_architecture(code->arch));
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
if (raise_on_any_failure) {
|
||||
throw runtime_error(format("({}) {}", filename, e.what()));
|
||||
}
|
||||
function_compiler_log.warning_f("Failed to compile function {}: {}", filename, e.what());
|
||||
}
|
||||
};
|
||||
|
||||
if (std::filesystem::is_regular_file(subdir_path)) {
|
||||
add_file(subdir_path);
|
||||
} else if (std::filesystem::is_directory(subdir_path)) {
|
||||
for (const auto& item : std::filesystem::directory_iterator(subdir_path)) {
|
||||
string filename = item.path().filename().string();
|
||||
add_file(filename);
|
||||
}
|
||||
} else {
|
||||
function_compiler_log.warning_f("Skipping {} (unknown file type)", subdir_name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const Menu> FunctionCodeIndex::patch_switches_menu(
|
||||
uint32_t specific_version,
|
||||
const std::unordered_set<std::string>& server_auto_patches_enabled,
|
||||
const std::unordered_set<std::string>& client_auto_patches_enabled) const {
|
||||
auto suffix = std::format("-{:08X}", specific_version);
|
||||
|
||||
auto ret = make_shared<Menu>(MenuID::PATCH_SWITCHES, "Patches");
|
||||
ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
|
||||
for (const auto& it : this->name_and_specific_version_to_patch_function) {
|
||||
const auto& fn = it.second;
|
||||
if (fn->hide_from_patches_menu || !it.first.ends_with(suffix) || server_auto_patches_enabled.count(fn->short_name)) {
|
||||
continue;
|
||||
}
|
||||
string name;
|
||||
name.push_back(client_auto_patches_enabled.count(fn->short_name) ? '*' : '-');
|
||||
name += fn->long_name.empty() ? fn->short_name : fn->long_name;
|
||||
ret->items.emplace_back(fn->menu_item_id, name, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool FunctionCodeIndex::patch_menu_empty(uint32_t specific_version) const {
|
||||
uint32_t mask = specific_version_is_indeterminate(specific_version) ? 0xFF000000 : 0xFFFFFFFF;
|
||||
for (const auto& it : this->menu_item_id_and_specific_version_to_patch_function) {
|
||||
if ((it.first & mask) == (specific_version & mask)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<const CompiledFunctionCode> FunctionCodeIndex::get_patch(
|
||||
const std::string& name, uint32_t specific_version) const {
|
||||
return this->name_and_specific_version_to_patch_function.at(std::format("{}-{:08X}", name, specific_version));
|
||||
}
|
||||
|
||||
DOLFileIndex::DOLFileIndex(const string& directory) {
|
||||
if (!std::filesystem::is_directory(directory)) {
|
||||
function_compiler_log.info_f("DOL file directory is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
auto menu = make_shared<Menu>(MenuID::PROGRAMS, "Programs");
|
||||
this->menu = menu;
|
||||
menu->items.emplace_back(ProgramsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
|
||||
|
||||
uint32_t next_menu_item_id = 0;
|
||||
for (const auto& item : std::filesystem::directory_iterator(directory)) {
|
||||
string filename = item.path().filename().string();
|
||||
bool is_dol = filename.ends_with(".dol");
|
||||
bool is_compressed_dol = filename.ends_with(".dol.prs");
|
||||
if (!is_dol && !is_compressed_dol) {
|
||||
continue;
|
||||
}
|
||||
string name = filename.substr(0, filename.size() - (is_compressed_dol ? 8 : 4));
|
||||
|
||||
try {
|
||||
auto dol = make_shared<File>();
|
||||
dol->menu_item_id = next_menu_item_id++;
|
||||
dol->name = name;
|
||||
|
||||
string path = directory + "/" + filename;
|
||||
string file_data = phosg::load_file(path);
|
||||
|
||||
string description;
|
||||
if (is_compressed_dol) {
|
||||
size_t decompressed_size = prs_decompress_size(file_data);
|
||||
|
||||
phosg::StringWriter w;
|
||||
w.put_u32b(file_data.size());
|
||||
w.put_u32b(decompressed_size);
|
||||
w.write(file_data);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
dol->data = std::move(w.str());
|
||||
|
||||
string compressed_size_str = phosg::format_size(file_data.size());
|
||||
string decompressed_size_str = phosg::format_size(decompressed_size);
|
||||
function_compiler_log.debug_f("Loaded compressed DOL file {} ({} -> {})",
|
||||
dol->name, compressed_size_str, decompressed_size_str);
|
||||
description = std::format("$C6{}$C7\n{}\n{} (orig)", dol->name, compressed_size_str, decompressed_size_str);
|
||||
|
||||
} else {
|
||||
phosg::StringWriter w;
|
||||
w.put_u32b(0);
|
||||
w.put_u32b(file_data.size());
|
||||
w.write(file_data);
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
dol->data = std::move(w.str());
|
||||
|
||||
string size_str = phosg::format_size(dol->data.size());
|
||||
function_compiler_log.debug_f("Loaded DOL file {} ({})", filename, size_str);
|
||||
description = std::format("$C6{}$C7\n{}", dol->name, size_str);
|
||||
}
|
||||
|
||||
this->name_to_file.emplace(dol->name, dol);
|
||||
this->item_id_to_file.emplace_back(dol);
|
||||
|
||||
menu->items.emplace_back(dol->menu_item_id, dol->name, description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE);
|
||||
|
||||
} catch (const exception& e) {
|
||||
function_compiler_log.warning_f("Failed to load DOL file {}: {}", filename, e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum) {
|
||||
static unordered_map<uint32_t, uint32_t> checksum_to_specific_version;
|
||||
if (checksum_to_specific_version.empty()) {
|
||||
struct {
|
||||
char system_code = 'G';
|
||||
char game_code1 = 'P';
|
||||
char game_code2;
|
||||
char region_code;
|
||||
char developer_code1 = '8';
|
||||
char developer_code2 = 'P';
|
||||
uint8_t disc_number = 0;
|
||||
uint8_t version_code;
|
||||
} __attribute__((packed)) data;
|
||||
for (const char* game_code2 = "OS"; *game_code2; game_code2++) {
|
||||
data.game_code2 = *game_code2;
|
||||
for (const char* region_code = "JEP"; *region_code; region_code++) {
|
||||
data.region_code = *region_code;
|
||||
for (uint8_t version_code = 0; version_code < 8; version_code++) {
|
||||
data.version_code = version_code;
|
||||
uint32_t checksum = phosg::crc32(&data, sizeof(data));
|
||||
uint32_t specific_version = 0x33000030 | (*game_code2 << 16) | (*region_code << 8) | version_code;
|
||||
if (!checksum_to_specific_version.emplace(checksum, specific_version).second) {
|
||||
throw logic_error("multiple specific_versions have same header checksum");
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// Generate entries for Trial Editions
|
||||
data.region_code = 'J';
|
||||
data.system_code = 'D';
|
||||
data.version_code = 0;
|
||||
uint32_t checksum = phosg::crc32(&data, sizeof(data));
|
||||
uint32_t specific_version = 0x33004A54 | (*game_code2 << 16);
|
||||
if (!checksum_to_specific_version.emplace(checksum, specific_version).second) {
|
||||
throw logic_error("multiple specific_versions have same header checksum");
|
||||
}
|
||||
data.system_code = 'G';
|
||||
}
|
||||
}
|
||||
}
|
||||
return checksum_to_specific_version.at(header_checksum);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "Menu.hh"
|
||||
|
||||
// TODO: Support x86 and SH4 function calls in the future. Currently we only
|
||||
// support PPC32 because I haven't written an appropriate x86 assembler yet.
|
||||
|
||||
struct CompiledFunctionCode {
|
||||
enum class Architecture {
|
||||
UNKNOWN = 0,
|
||||
POWERPC, // GC
|
||||
X86, // PC, XB, BB
|
||||
SH4, // Dreamcast
|
||||
};
|
||||
Architecture arch;
|
||||
std::string code;
|
||||
std::vector<uint16_t> relocation_deltas;
|
||||
std::unordered_map<std::string, uint32_t> label_offsets;
|
||||
uint32_t entrypoint_offset_offset = 0;
|
||||
std::string source_path; // Path to source file from newserv root
|
||||
std::string short_name; // Based on filename
|
||||
std::string long_name; // From .meta name directive
|
||||
std::string description; // From .meta description directive
|
||||
uint64_t client_flag = 0; // From .meta client_flag directive
|
||||
uint32_t menu_item_id = 0;
|
||||
bool hide_from_patches_menu = false;
|
||||
bool show_return_value = false;
|
||||
uint32_t specific_version = 0; // 0 = not a client-selectable patch
|
||||
|
||||
bool is_big_endian() const;
|
||||
|
||||
template <bool BE>
|
||||
std::string generate_client_command_t(
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes,
|
||||
const void* suffix_data = nullptr,
|
||||
size_t suffix_size = 0,
|
||||
uint32_t override_relocations_offset = 0) const;
|
||||
std::string generate_client_command(
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes = {},
|
||||
const void* suffix_data = nullptr,
|
||||
size_t suffix_size = 0,
|
||||
uint32_t override_relocations_offset = 0) const;
|
||||
};
|
||||
|
||||
const char* name_for_architecture(CompiledFunctionCode::Architecture arch);
|
||||
|
||||
struct FunctionCodeIndex {
|
||||
FunctionCodeIndex() = default;
|
||||
FunctionCodeIndex(const std::string& directory, bool raise_on_any_failure);
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<CompiledFunctionCode>> name_to_function;
|
||||
std::unordered_map<uint8_t, std::shared_ptr<CompiledFunctionCode>> index_to_function;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<CompiledFunctionCode>> menu_item_id_and_specific_version_to_patch_function;
|
||||
// Key here is e.g. "PATCHNAME-SPECIFICVERSION", with the latter in hex
|
||||
std::map<std::string, std::shared_ptr<CompiledFunctionCode>> name_and_specific_version_to_patch_function;
|
||||
|
||||
std::shared_ptr<const Menu> patch_switches_menu(
|
||||
uint32_t specific_version,
|
||||
const std::unordered_set<std::string>& server_auto_patches_enabled,
|
||||
const std::unordered_set<std::string>& client_auto_patches_enabled) const;
|
||||
bool patch_menu_empty(uint32_t specific_version) const;
|
||||
|
||||
std::shared_ptr<const CompiledFunctionCode> get_patch(const std::string& name, uint32_t specific_version) const;
|
||||
};
|
||||
|
||||
struct DOLFileIndex {
|
||||
struct File {
|
||||
uint32_t menu_item_id;
|
||||
std::string name;
|
||||
std::string data;
|
||||
bool is_compressed;
|
||||
};
|
||||
|
||||
std::vector<std::shared_ptr<File>> item_id_to_file;
|
||||
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
||||
std::shared_ptr<const Menu> menu;
|
||||
|
||||
DOLFileIndex() = default;
|
||||
explicit DOLFileIndex(const std::string& directory);
|
||||
|
||||
inline bool empty() const {
|
||||
return this->name_to_file.empty() && this->item_id_to_file.empty();
|
||||
}
|
||||
};
|
||||
|
||||
uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum);
|
||||
+3
-3
@@ -5,11 +5,11 @@
|
||||
using namespace std;
|
||||
|
||||
phosg::PrefixedLogger channel_exceptions_log("[Channel] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger client_functions_log("[ClientFunctionIndex] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger client_log("", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger command_data_log("[Commands] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger config_log("[Config] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger dns_server_log("[DNSServer] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger function_compiler_log("[FunctionCompiler] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger ip_stack_simulator_log("[IPStackSimulator] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger lobby_log("", phosg::LogLevel::L_USE_DEFAULT);
|
||||
phosg::PrefixedLogger patch_index_log("[PatchFileIndex] ", phosg::LogLevel::L_USE_DEFAULT);
|
||||
@@ -30,11 +30,11 @@ static void set_log_level_from_json(
|
||||
|
||||
void set_all_log_levels(phosg::LogLevel level) {
|
||||
channel_exceptions_log.min_level = level;
|
||||
client_functions_log.min_level = level;
|
||||
client_log.min_level = level;
|
||||
command_data_log.min_level = level;
|
||||
config_log.min_level = level;
|
||||
dns_server_log.min_level = level;
|
||||
function_compiler_log.min_level = level;
|
||||
ip_stack_simulator_log.min_level = level;
|
||||
lobby_log.min_level = level;
|
||||
patch_index_log.min_level = level;
|
||||
@@ -47,11 +47,11 @@ void set_all_log_levels(phosg::LogLevel level) {
|
||||
|
||||
void set_log_levels_from_json(const phosg::JSON& json) {
|
||||
set_log_level_from_json(channel_exceptions_log, json, "ChannelExceptions");
|
||||
set_log_level_from_json(client_functions_log, json, "ClientFunctionIndex");
|
||||
set_log_level_from_json(client_log, json, "Clients");
|
||||
set_log_level_from_json(command_data_log, json, "CommandData");
|
||||
set_log_level_from_json(config_log, json, "Config");
|
||||
set_log_level_from_json(dns_server_log, json, "DNSServer");
|
||||
set_log_level_from_json(function_compiler_log, json, "FunctionCompiler");
|
||||
set_log_level_from_json(ip_stack_simulator_log, json, "IPStackSimulator");
|
||||
set_log_level_from_json(lobby_log, json, "Lobbies");
|
||||
set_log_level_from_json(patch_index_log, json, "PatchFileIndex");
|
||||
|
||||
+1
-1
@@ -4,11 +4,11 @@
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
extern phosg::PrefixedLogger channel_exceptions_log;
|
||||
extern phosg::PrefixedLogger client_functions_log;
|
||||
extern phosg::PrefixedLogger client_log;
|
||||
extern phosg::PrefixedLogger command_data_log;
|
||||
extern phosg::PrefixedLogger config_log;
|
||||
extern phosg::PrefixedLogger dns_server_log;
|
||||
extern phosg::PrefixedLogger function_compiler_log;
|
||||
extern phosg::PrefixedLogger ip_stack_simulator_log;
|
||||
extern phosg::PrefixedLogger lobby_log;
|
||||
extern phosg::PrefixedLogger patch_index_log;
|
||||
|
||||
+18
-29
@@ -27,6 +27,7 @@
|
||||
#include "Compression.hh"
|
||||
#include "DCSerialNumbers.hh"
|
||||
#include "DNSServer.hh"
|
||||
#include "DOLFileIndex.hh"
|
||||
#include "DownloadSession.hh"
|
||||
#include "GSLArchive.hh"
|
||||
#include "GameServer.hh"
|
||||
@@ -1751,18 +1752,22 @@ Action a_assemble_quest_script(
|
||||
write_output_data(args, result_data.data(), result_data.size(), compress ? "bin" : "bind");
|
||||
});
|
||||
|
||||
Action a_assemble_all_patches(
|
||||
"assemble-all-patches", "\
|
||||
assemble-all-patches [--skip-encrypted]\n\
|
||||
Action a_assemble_all_client_functions(
|
||||
"assemble-all-client-functions", "\
|
||||
assemble-all-client-functions [--skip-encrypted] OUTPUT-DIRECTORY\n\
|
||||
Assemble all patches in the system/client-functions directory, and produce\n\
|
||||
two compiled .bin files for each patch (one unencrypted, for most PSO\n\
|
||||
two compiled .bin files for each patch: one unencrypted, for most PSO\n\
|
||||
versions, and one encrypted, for PSO GC JP v1.4, JP Ep3, and Ep3 Trial\n\
|
||||
Edition). The output files are saved in system/client-functions.\n",
|
||||
Edition. If --skip-encrypted is given, only the unencrypted .bin files are\n\
|
||||
created.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
auto fci = make_shared<FunctionCodeIndex>("system/client-functions", false);
|
||||
auto fci = make_shared<ClientFunctionIndex>("system/client-functions", false);
|
||||
|
||||
const std::string& output_dir = args.get<string>(1);
|
||||
std::filesystem::create_directories(output_dir);
|
||||
|
||||
bool skip_encrypted = args.get<bool>("skip-encrypted");
|
||||
auto process_code = [&](shared_ptr<const CompiledFunctionCode> code,
|
||||
auto process_code = [&](shared_ptr<const ClientFunctionIndex::Function> code,
|
||||
uint32_t checksum_addr,
|
||||
uint32_t checksum_size,
|
||||
uint32_t override_start_addr) -> void {
|
||||
@@ -1775,34 +1780,18 @@ Action a_assemble_all_patches(
|
||||
code, {}, nullptr, 0, checksum_addr, checksum_size, override_start_addr, encrypted);
|
||||
w.put(PSOCommandHeaderDCV3{.command = 0xB2, .flag = 0x00, .size = data.size() + 4});
|
||||
w.write(data);
|
||||
string out_path = std::format("{}.{}.{}.bin",
|
||||
code->source_path, str_for_specific_version(code->specific_version), (encrypted ? "enc" : "std"));
|
||||
string out_path = std::format("{}/{}.{}.{}.bin",
|
||||
output_dir, code->short_name, str_for_specific_version(code->specific_version), (encrypted ? "enc" : "std"));
|
||||
phosg::save_file(out_path, w.str());
|
||||
phosg::fwrite_fmt(stderr, "... {}\n", out_path);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& it : fci->name_and_specific_version_to_patch_function) {
|
||||
process_code(it.second, 0, 0, 0);
|
||||
for (const auto& [_, fn] : fci->all_functions) {
|
||||
process_code(fn, 0, 0, 0);
|
||||
}
|
||||
try {
|
||||
process_code(fci->name_to_function.at("VersionDetectDC"), 0, 0, 0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(fci->name_to_function.at("VersionDetectGC"), 0, 0, 0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(fci->name_to_function.at("VersionDetectXB"), 0, 0, 0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(fci->name_to_function.at("CacheClearFix-Phase1"), 0x80000000, 8, 0x7F2734EC);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(fci->name_to_function.at("CacheClearFix-Phase2"), 0, 0, 0);
|
||||
process_code(fci->get("CacheClearFix-Phase1", SPECIFIC_VERSION_PPC_INDETERMINATE), 0x80000000, 8, 0x7F2734EC);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
});
|
||||
@@ -3685,7 +3674,7 @@ Action a_check_client_functions(
|
||||
"check-client-functions", nullptr,
|
||||
+[](phosg::Arguments&) {
|
||||
set_all_log_levels(phosg::LogLevel::L_DEBUG);
|
||||
FunctionCodeIndex fci("system/client-functions", true);
|
||||
ClientFunctionIndex index("system/client-functions", true);
|
||||
phosg::fwrite_fmt(stdout, "All client functions compiled\n");
|
||||
});
|
||||
|
||||
|
||||
+16
-16
@@ -132,7 +132,7 @@ static void send_main_menu(shared_ptr<Client> c) {
|
||||
|
||||
main_menu->items.emplace_back(MainMenuItemID::DOWNLOAD_QUESTS, "Download quests",
|
||||
"Download quests", MenuItem::Flag::INVISIBLE_ON_DC_PROTOS | MenuItem::Flag::INVISIBLE_ON_PC_NTE | MenuItem::Flag::INVISIBLE_ON_BB);
|
||||
if (!s->function_code_index->patch_menu_empty(c->specific_version)) {
|
||||
if (!s->client_functions->patch_menu_empty(c->specific_version)) {
|
||||
main_menu->items.emplace_back(MainMenuItemID::PATCH_SWITCHES, "Patches",
|
||||
"Change game\nbehaviors", MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE);
|
||||
}
|
||||
@@ -240,14 +240,14 @@ static asio::awaitable<void> send_auto_patches_if_needed(shared_ptr<Client> c) {
|
||||
c->set_flag(Client::Flag::HAS_AUTO_PATCHES);
|
||||
co_await prepare_client_for_patches(c);
|
||||
|
||||
unordered_set<shared_ptr<const CompiledFunctionCode>> functions_to_send;
|
||||
unordered_set<shared_ptr<const ClientFunctionIndex::Function>> functions_to_send;
|
||||
if (c->version() == Version::BB_V4) {
|
||||
for (const auto& patch_name : s->bb_required_patches) {
|
||||
try {
|
||||
functions_to_send.emplace(s->function_code_index->get_patch(patch_name, c->specific_version));
|
||||
functions_to_send.emplace(s->client_functions->get(patch_name, c->specific_version));
|
||||
} catch (const out_of_range&) {
|
||||
string message = std::format(
|
||||
"Your client is not compatible with a\nrequired patch on this server.\n\nClient version: {:08X}\nPatch name: {}", c->specific_version, patch_name);
|
||||
"Your client is not compatible with a\nrequired patch on this server.\n\nClient version: {}\nPatch name: {}", str_for_specific_version(c->specific_version), patch_name);
|
||||
send_message_box(c, message);
|
||||
c->channel->disconnect();
|
||||
co_return;
|
||||
@@ -256,18 +256,18 @@ static asio::awaitable<void> send_auto_patches_if_needed(shared_ptr<Client> c) {
|
||||
}
|
||||
for (const auto& patch_name : s->auto_patches) {
|
||||
try {
|
||||
functions_to_send.emplace(s->function_code_index->get_patch(patch_name, c->specific_version));
|
||||
functions_to_send.emplace(s->client_functions->get(patch_name, c->specific_version));
|
||||
} catch (const out_of_range&) {
|
||||
c->log.warning_f("Server has auto patch {} enabled, but it is not available for specific_version {:08X}",
|
||||
patch_name, c->specific_version);
|
||||
c->log.warning_f("Server has auto patch {} enabled, but it is not available for specific_version {}",
|
||||
patch_name, str_for_specific_version(c->specific_version));
|
||||
}
|
||||
}
|
||||
for (const auto& patch_name : c->login->account->auto_patches_enabled) {
|
||||
try {
|
||||
functions_to_send.emplace(s->function_code_index->get_patch(patch_name, c->specific_version));
|
||||
functions_to_send.emplace(s->client_functions->get(patch_name, c->specific_version));
|
||||
} catch (const out_of_range&) {
|
||||
c->log.warning_f("Client has auto patch {} enabled, but it is not available for specific_version {:08X}",
|
||||
patch_name, c->specific_version);
|
||||
c->log.warning_f("Client has auto patch {} enabled, but it is not available for specific_version {}",
|
||||
patch_name, str_for_specific_version(c->specific_version));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1319,7 +1319,7 @@ static asio::awaitable<void> on_9D_9E(shared_ptr<Client> c, Channel::Message& ms
|
||||
// not; we'll call on_login_complete once we receive the B3 response
|
||||
if (c->version() == Version::PC_V2) {
|
||||
try {
|
||||
auto code = s->function_code_index->name_to_function.at("ReturnTokenX86");
|
||||
auto code = s->client_functions->get("ReturnToken", SPECIFIC_VERSION_X86_INDETERMINATE);
|
||||
unordered_map<string, uint32_t> label_writes{{"token", c->login->account->account_id}};
|
||||
auto resp = co_await send_function_call(c, code, label_writes, nullptr, 0, 0x00400000, 0x0000E000, 0, true);
|
||||
|
||||
@@ -1342,7 +1342,8 @@ static asio::awaitable<void> on_9D_9E(shared_ptr<Client> c, Channel::Message& ms
|
||||
c->specific_version = SPECIFIC_VERSION_PC_V2_INDETERMINATE;
|
||||
c->log.info_f("Version cannot be determined from PE header checksum {:08X}", resp.checksum);
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
} catch (const out_of_range& e) {
|
||||
c->log.info_f("Cannot determine PSO PC specific version: {}", e.what());
|
||||
}
|
||||
}
|
||||
co_await on_login_complete(c);
|
||||
@@ -2560,7 +2561,7 @@ static asio::awaitable<void> on_10_main_menu(shared_ptr<Client> c, uint32_t item
|
||||
// We have to prepare the client for patches here, even though we don't send them from this mennu, because we
|
||||
// need to know the client's specific_version before sending the menu.
|
||||
co_await prepare_client_for_patches(c);
|
||||
send_menu(c, c->require_server_state()->function_code_index->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled));
|
||||
send_menu(c, c->require_server_state()->client_functions->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2914,13 +2915,12 @@ static void on_10_patch_switches(shared_ptr<Client> c, uint32_t item_id) {
|
||||
}
|
||||
|
||||
auto s = c->require_server_state();
|
||||
uint64_t key = (static_cast<uint64_t>(item_id) << 32) | c->specific_version;
|
||||
auto fn = s->function_code_index->menu_item_id_and_specific_version_to_patch_function.at(key);
|
||||
auto fn = s->client_functions->get_by_menu_item_id(item_id);
|
||||
if (!c->login->account->auto_patches_enabled.emplace(fn->short_name).second) {
|
||||
c->login->account->auto_patches_enabled.erase(fn->short_name);
|
||||
}
|
||||
c->login->account->save();
|
||||
send_menu(c, s->function_code_index->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled));
|
||||
send_menu(c, s->client_functions->patch_switches_menu(c->specific_version, s->auto_patches, c->login->account->auto_patches_enabled));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -335,7 +335,7 @@ asio::awaitable<void> prepare_client_for_patches(shared_ptr<Client> c) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
if (!c->check_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH)) {
|
||||
auto fn = s->function_code_index->name_to_function.at("CacheClearFix-Phase1");
|
||||
auto fn = s->client_functions->get("CacheClearFix-Phase1", ClientFunctionIndex::Function::Architecture::POWERPC);
|
||||
unordered_map<string, uint32_t> label_writes;
|
||||
auto call1_res = co_await send_function_call(c, fn, label_writes, nullptr, 0, 0x80000000, 8, 0x7F2734EC);
|
||||
try {
|
||||
@@ -344,29 +344,29 @@ asio::awaitable<void> prepare_client_for_patches(shared_ptr<Client> c) {
|
||||
} catch (const out_of_range&) {
|
||||
c->log.info_f("Could not detect specific version from header checksum {:08X}", call1_res.checksum);
|
||||
}
|
||||
co_await send_function_call(c, s->function_code_index->name_to_function.at("CacheClearFix-Phase2"));
|
||||
co_await send_function_call(c, s->client_functions->get("CacheClearFix-Phase2", ClientFunctionIndex::Function::Architecture::POWERPC));
|
||||
c->log.info_f("Client cache behavior patched");
|
||||
c->set_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
}
|
||||
|
||||
const char* version_detect_name = nullptr;
|
||||
ClientFunctionIndex::Function::Architecture arch = ClientFunctionIndex::Function::Architecture::UNKNOWN;
|
||||
if (c->version() == Version::DC_V2) {
|
||||
version_detect_name = "VersionDetectDC";
|
||||
arch = ClientFunctionIndex::Function::Architecture::SH4;
|
||||
} else if (is_gc(c->version())) {
|
||||
version_detect_name = "VersionDetectGC";
|
||||
arch = ClientFunctionIndex::Function::Architecture::POWERPC;
|
||||
} else if (c->version() == Version::XB_V3) {
|
||||
version_detect_name = "VersionDetectXB";
|
||||
arch = ClientFunctionIndex::Function::Architecture::X86;
|
||||
}
|
||||
if (version_detect_name && specific_version_is_indeterminate(c->specific_version)) {
|
||||
auto vers_detect_res = co_await send_function_call(
|
||||
c, s->function_code_index->name_to_function.at(version_detect_name));
|
||||
if ((arch != ClientFunctionIndex::Function::Architecture::UNKNOWN) &&
|
||||
specific_version_is_indeterminate(c->specific_version)) {
|
||||
auto vers_detect_res = co_await send_function_call(c, s->client_functions->get("VersionDetect", arch));
|
||||
c->specific_version = vers_detect_res.return_value;
|
||||
c->log.info_f("Version detected as {:08X}", c->specific_version);
|
||||
}
|
||||
}
|
||||
|
||||
string prepare_send_function_call_data(
|
||||
shared_ptr<const CompiledFunctionCode> code,
|
||||
shared_ptr<const ClientFunctionIndex::Function> code,
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
@@ -416,7 +416,7 @@ string prepare_send_function_call_data(
|
||||
|
||||
asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
|
||||
shared_ptr<Client> c,
|
||||
shared_ptr<const CompiledFunctionCode> code,
|
||||
shared_ptr<const ClientFunctionIndex::Function> code,
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
@@ -445,7 +445,7 @@ asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
|
||||
}
|
||||
|
||||
asio::awaitable<void> send_function_call_multi(
|
||||
shared_ptr<Client> c, unordered_set<shared_ptr<const CompiledFunctionCode>> codes) {
|
||||
shared_ptr<Client> c, unordered_set<shared_ptr<const ClientFunctionIndex::Function>> codes) {
|
||||
if (codes.empty()) {
|
||||
co_return;
|
||||
}
|
||||
@@ -468,7 +468,7 @@ asio::awaitable<void> send_function_call_multi(
|
||||
void send_function_call(
|
||||
shared_ptr<Channel> ch,
|
||||
uint64_t client_enabled_flags,
|
||||
shared_ptr<const CompiledFunctionCode> code,
|
||||
shared_ptr<const ClientFunctionIndex::Function> code,
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
@@ -525,7 +525,7 @@ asio::awaitable<bool> send_protected_command(std::shared_ptr<Client> c, const vo
|
||||
co_await prepare_client_for_patches(c);
|
||||
|
||||
try {
|
||||
auto fn = s->function_code_index->get_patch("CallProtectedHandler", c->specific_version);
|
||||
auto fn = s->client_functions->get("CallProtectedHandler", c->specific_version);
|
||||
unordered_map<string, uint32_t> label_writes{{"size", size}};
|
||||
co_await send_function_call(c, fn, label_writes, data, size);
|
||||
auto l = echo_to_lobby ? c->lobby.lock() : nullptr;
|
||||
@@ -549,7 +549,7 @@ asio::awaitable<void> send_dol_file(shared_ptr<Client> c, shared_ptr<DOLFileInde
|
||||
// Determine the necessary start address for the data
|
||||
unordered_map<string, uint32_t> label_writes{{"address", 0x80000034}}; // ArenaHigh from GC globals
|
||||
auto addr_ret = co_await send_function_call(
|
||||
c, s->function_code_index->name_to_function.at("ReadMemoryWordGC"), label_writes);
|
||||
c, s->client_functions->get("ReadMemoryWord", c->specific_version), label_writes);
|
||||
uint32_t dol_base_addr = (addr_ret.return_value - dol->data.size()) & (~3);
|
||||
|
||||
// Write the file in multiple chunks
|
||||
@@ -561,7 +561,7 @@ asio::awaitable<void> send_dol_file(shared_ptr<Client> c, shared_ptr<DOLFileInde
|
||||
string data_to_send = dol->data.substr(offset, bytes_to_send);
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto fn = s->function_code_index->name_to_function.at("WriteMemoryGC");
|
||||
auto fn = s->client_functions->get("WriteMemory", c->specific_version);
|
||||
label_writes = {{"dest_addr", (dol_base_addr + offset)}, {"size", bytes_to_send}};
|
||||
co_await send_function_call(c, fn, label_writes, data_to_send.data(), data_to_send.size());
|
||||
|
||||
@@ -572,7 +572,7 @@ asio::awaitable<void> send_dol_file(shared_ptr<Client> c, shared_ptr<DOLFileInde
|
||||
}
|
||||
|
||||
// Send the final function, which moves the DOL's sections into place and calls the entrypoint
|
||||
auto fn = s->function_code_index->name_to_function.at("RunDOL");
|
||||
auto fn = s->client_functions->get("RunDOL", c->specific_version);
|
||||
label_writes = {{"dol_base_ptr", dol_base_addr}};
|
||||
co_await send_function_call(c, fn, label_writes);
|
||||
// The client will stop running PSO after this, so disconnect them
|
||||
@@ -2456,7 +2456,7 @@ asio::awaitable<GetPlayerInfoResult> send_get_player_info(shared_ptr<Client> c,
|
||||
}
|
||||
try {
|
||||
auto s = c->require_server_state();
|
||||
auto fn = s->function_code_index->get_patch("GetExtendedPlayerInfo", c->specific_version);
|
||||
auto fn = s->client_functions->get("GetExtendedPlayerInfo", c->specific_version);
|
||||
send_function_call(c->channel, c->enabled_flags, fn);
|
||||
c->function_call_response_queue.emplace_back(make_shared<AsyncPromise<C_ExecuteCodeResult_B3>>());
|
||||
full_req_sent = true;
|
||||
|
||||
+5
-5
@@ -9,8 +9,8 @@
|
||||
#include <unordered_set>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "ClientFunctionIndex.hh"
|
||||
#include "CommandFormats.hh"
|
||||
#include "FunctionCompiler.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Menu.hh"
|
||||
#include "Quest.hh"
|
||||
@@ -150,7 +150,7 @@ void send_patch_change_to_directory(
|
||||
|
||||
asio::awaitable<void> prepare_client_for_patches(std::shared_ptr<Client> c);
|
||||
std::string prepare_send_function_call_data(
|
||||
std::shared_ptr<const CompiledFunctionCode> code,
|
||||
std::shared_ptr<const ClientFunctionIndex::Function> code,
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes,
|
||||
const void* suffix_data,
|
||||
size_t suffix_size,
|
||||
@@ -163,7 +163,7 @@ std::string prepare_send_function_call_data(
|
||||
void send_function_call(
|
||||
std::shared_ptr<Channel> ch,
|
||||
uint64_t client_enabled_flags,
|
||||
std::shared_ptr<const CompiledFunctionCode> code,
|
||||
std::shared_ptr<const ClientFunctionIndex::Function> code,
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes = {},
|
||||
const void* suffix_data = nullptr,
|
||||
size_t suffix_size = 0,
|
||||
@@ -173,7 +173,7 @@ void send_function_call(
|
||||
bool ignore_actually_runs_code_flag = false);
|
||||
asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
|
||||
std::shared_ptr<Client> c,
|
||||
std::shared_ptr<const CompiledFunctionCode> code,
|
||||
std::shared_ptr<const ClientFunctionIndex::Function> code,
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes = {},
|
||||
const void* suffix_data = nullptr,
|
||||
size_t suffix_size = 0,
|
||||
@@ -182,7 +182,7 @@ asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
|
||||
uint32_t override_relocations_offset = 0,
|
||||
bool ignore_actually_runs_code_flag = false);
|
||||
asio::awaitable<void> send_function_call_multi(
|
||||
std::shared_ptr<Client> c, std::unordered_set<std::shared_ptr<const CompiledFunctionCode>> codes);
|
||||
std::shared_ptr<Client> c, std::unordered_set<std::shared_ptr<const ClientFunctionIndex::Function>> codes);
|
||||
asio::awaitable<bool> send_protected_command(std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby);
|
||||
asio::awaitable<void> send_dol_file(std::shared_ptr<Client> c, std::shared_ptr<DOLFileIndex::File> dol);
|
||||
|
||||
|
||||
+1
-1
@@ -2231,7 +2231,7 @@ void ServerState::load_quest_index(bool raise_on_any_failure) {
|
||||
|
||||
void ServerState::compile_functions(bool raise_on_any_failure) {
|
||||
config_log.info_f("Compiling client functions");
|
||||
this->function_code_index = make_shared<FunctionCodeIndex>("system/client-functions", raise_on_any_failure);
|
||||
this->client_functions = make_shared<ClientFunctionIndex>("system/client-functions", raise_on_any_failure);
|
||||
}
|
||||
|
||||
void ServerState::load_dol_files() {
|
||||
|
||||
+3
-2
@@ -11,11 +11,12 @@
|
||||
|
||||
#include "Account.hh"
|
||||
#include "Client.hh"
|
||||
#include "ClientFunctionIndex.hh"
|
||||
#include "CommonItemSet.hh"
|
||||
#include "DNSServer.hh"
|
||||
#include "DOLFileIndex.hh"
|
||||
#include "Episode3/DataIndexes.hh"
|
||||
#include "Episode3/Tournament.hh"
|
||||
#include "FunctionCompiler.hh"
|
||||
#include "GSLArchive.hh"
|
||||
#include "IPV4RangeSet.hh"
|
||||
#include "ItemNameIndex.hh"
|
||||
@@ -183,7 +184,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
|
||||
std::shared_ptr<const parray<uint8_t, 0x16C>> bb_default_keyboard_config;
|
||||
std::shared_ptr<const parray<uint8_t, 0x38>> bb_default_joystick_config;
|
||||
std::shared_ptr<const FunctionCodeIndex> function_code_index;
|
||||
std::shared_ptr<const ClientFunctionIndex> client_functions;
|
||||
std::shared_ptr<const PatchFileIndex> pc_patch_file_index;
|
||||
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<const MapFile>> map_file_for_source_hash;
|
||||
|
||||
+22
-5
@@ -140,7 +140,7 @@ uint32_t default_sub_version_for_version(Version version) {
|
||||
uint32_t default_specific_version_for_version(Version version, int64_t sub_version) {
|
||||
// For versions that don't support send_function_call by default, we need to set the specific_version based on
|
||||
// sub_version. Fortunately, all versions that share sub_version values also support send_function_call, so for those
|
||||
// versions we get the specific_version later by sending VersionDetectDC, VersionDetectGC, or VersionDetectXB.
|
||||
// versions we get the specific_version later by sending VersionDetect.
|
||||
switch (version) {
|
||||
case Version::DC_NTE:
|
||||
return SPECIFIC_VERSION_DC_NTE; // 1OJ1 (NTE)
|
||||
@@ -159,7 +159,7 @@ uint32_t default_specific_version_for_version(Version version, int64_t sub_versi
|
||||
return SPECIFIC_VERSION_DC_V1_INDETERMINATE;
|
||||
}
|
||||
case Version::DC_V2:
|
||||
return SPECIFIC_VERSION_DC_V2_INDETERMINATE; // 2___; need to send VersionDetectDC
|
||||
return SPECIFIC_VERSION_DC_V2_INDETERMINATE; // 2___; need to send VersionDetect
|
||||
case Version::PC_NTE:
|
||||
return SPECIFIC_VERSION_PC_V2_NTE; // 2OJT
|
||||
case Version::PC_V2:
|
||||
@@ -184,7 +184,7 @@ uint32_t default_specific_version_for_version(Version version, int64_t sub_versi
|
||||
case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.2, at least one version of PSO XB
|
||||
case 0x31: // GC Ep1&2 US v1.0, GC US v1.1, XB US
|
||||
default:
|
||||
return SPECIFIC_VERSION_GC_V3_INDETERMINATE; // 3O__; need to send VersionDetectGC
|
||||
return SPECIFIC_VERSION_GC_V3_INDETERMINATE; // 3O__; need to send VersionDetect
|
||||
}
|
||||
throw logic_error("this should be impossible");
|
||||
case Version::GC_EP3_NTE:
|
||||
@@ -199,10 +199,10 @@ uint32_t default_specific_version_for_version(Version version, int64_t sub_versi
|
||||
case -1: // Initial check (before sub_version recognition)
|
||||
case 0x40: // GC Ep3 trial and GC Ep3 JP
|
||||
default:
|
||||
return SPECIFIC_VERSION_GC_EP3_INDETERMINATE; // 3SJ_; need to send VersionDetectGC
|
||||
return SPECIFIC_VERSION_GC_EP3_INDETERMINATE; // 3SJ_; need to send VersionDetect
|
||||
}
|
||||
case Version::XB_V3:
|
||||
return SPECIFIC_VERSION_XB_V3_INDETERMINATE; // 4O__; need to send VersionDetectXB
|
||||
return SPECIFIC_VERSION_XB_V3_INDETERMINATE; // 4O__; need to send VersionDetect
|
||||
case Version::BB_V4:
|
||||
return SPECIFIC_VERSION_BB_V4_INDETERMINATE; // 5___; we should be able to determine version from initial login
|
||||
default:
|
||||
@@ -244,6 +244,23 @@ bool specific_version_is_bb(uint32_t specific_version) {
|
||||
return ((specific_version & 0xFF000000) == 0x35000000);
|
||||
}
|
||||
|
||||
uint32_t specific_version_for_str(const std::string& s) {
|
||||
switch (s.size()) {
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
return (s[0] << 24);
|
||||
case 2:
|
||||
return (s[0] << 24) | (s[1] << 16);
|
||||
case 3:
|
||||
return (s[0] << 24) | (s[1] << 16) | (s[2] << 8);
|
||||
case 4:
|
||||
return (s[0] << 24) | (s[1] << 16) | (s[2] << 8) | s[3];
|
||||
default:
|
||||
throw std::runtime_error("Invalid specific_version string");
|
||||
}
|
||||
}
|
||||
|
||||
string str_for_specific_version(uint32_t specific_version) {
|
||||
string ret;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
|
||||
@@ -184,6 +184,10 @@ constexpr bool uses_utf16(Version version) {
|
||||
(version == Version::BB_V4);
|
||||
}
|
||||
|
||||
constexpr uint32_t SPECIFIC_VERSION_SH4_INDETERMINATE = 0x53483400; // SH4_
|
||||
constexpr uint32_t SPECIFIC_VERSION_PPC_INDETERMINATE = 0x50504300; // PPC_
|
||||
constexpr uint32_t SPECIFIC_VERSION_X86_INDETERMINATE = 0x58383600; // X86_
|
||||
|
||||
constexpr uint32_t SPECIFIC_VERSION_DC_NTE = 0x314F4A31; // 1OJ1
|
||||
constexpr uint32_t SPECIFIC_VERSION_DC_11_2000_PROTOTYPE = 0x314F4A32; // 1OJ2
|
||||
constexpr uint32_t SPECIFIC_VERSION_DC_V1_JP = 0x314F4A46; // 1OJF
|
||||
@@ -220,6 +224,7 @@ bool specific_version_is_gc(uint32_t specific_version);
|
||||
bool specific_version_is_xb(uint32_t specific_version);
|
||||
bool specific_version_is_bb(uint32_t specific_version);
|
||||
|
||||
uint32_t specific_version_for_str(const std::string& specific_version);
|
||||
std::string str_for_specific_version(uint32_t specific_version);
|
||||
|
||||
enum class ServerBehavior {
|
||||
|
||||
Reference in New Issue
Block a user