1340 lines
54 KiB
C++
1340 lines
54 KiB
C++
#include <event2/event.h>
|
|
#include <pwd.h>
|
|
#include <signal.h>
|
|
#include <string.h>
|
|
|
|
#include <phosg/Filesystem.hh>
|
|
#include <phosg/JSON.hh>
|
|
#include <phosg/Math.hh>
|
|
#include <phosg/Network.hh>
|
|
#include <phosg/Strings.hh>
|
|
#include <phosg/Tools.hh>
|
|
#include <set>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
|
|
#include "BMLArchive.hh"
|
|
#include "CatSession.hh"
|
|
#include "Compression.hh"
|
|
#include "DNSServer.hh"
|
|
#include "GSLArchive.hh"
|
|
#include "IPStackSimulator.hh"
|
|
#include "Loggers.hh"
|
|
#include "NetworkAddresses.hh"
|
|
#include "PSOGCObjectGraph.hh"
|
|
#include "Product.hh"
|
|
#include "ProxyServer.hh"
|
|
#include "ReplaySession.hh"
|
|
#include "SaveFileFormats.hh"
|
|
#include "SendCommands.hh"
|
|
#include "Server.hh"
|
|
#include "ServerShell.hh"
|
|
#include "ServerState.hh"
|
|
#include "StaticGameData.hh"
|
|
#include "Text.hh"
|
|
|
|
using namespace std;
|
|
|
|
bool use_terminal_colors = false;
|
|
|
|
template <typename T>
|
|
vector<T> parse_int_vector(shared_ptr<const JSONObject> o) {
|
|
vector<T> ret;
|
|
for (const auto& x : o->as_list()) {
|
|
ret.emplace_back(x->as_int());
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void drop_privileges(const string& username) {
|
|
if ((getuid() != 0) || (getgid() != 0)) {
|
|
throw runtime_error(string_printf(
|
|
"newserv was not started as root; can\'t switch to user %s",
|
|
username.c_str()));
|
|
}
|
|
|
|
struct passwd* pw = getpwnam(username.c_str());
|
|
if (!pw) {
|
|
string error = string_for_error(errno);
|
|
throw runtime_error(string_printf("user %s not found (%s)",
|
|
username.c_str(), error.c_str()));
|
|
}
|
|
|
|
if (setgid(pw->pw_gid) != 0) {
|
|
string error = string_for_error(errno);
|
|
throw runtime_error(string_printf("can\'t switch to group %d (%s)",
|
|
pw->pw_gid, error.c_str()));
|
|
}
|
|
if (setuid(pw->pw_uid) != 0) {
|
|
string error = string_for_error(errno);
|
|
throw runtime_error(string_printf("can\'t switch to user %d (%s)",
|
|
pw->pw_uid, error.c_str()));
|
|
}
|
|
config_log.info("Switched to user %s (%d:%d)", username.c_str(), pw->pw_uid, pw->pw_gid);
|
|
}
|
|
|
|
void print_usage() {
|
|
fputs("\
|
|
Usage:\n\
|
|
newserv [ACTION [OPTIONS...]]\n\
|
|
\n\
|
|
If ACTION is not specified, newserv runs in server mode. PSO clients can\n\
|
|
connect normally, join lobbies, play games, and use the proxy server. See\n\
|
|
README.md and system/config.json for more information.\n\
|
|
\n\
|
|
When ACTION is given, newserv will do things other than running the server.\n\
|
|
\n\
|
|
Some actions accept input and/or output filenames; see the descriptions below\n\
|
|
for details. If INPUT-FILENAME is missing or is '-', newserv reads from stdin.\n\
|
|
If OUTPUT-FILENAME is missing and the input is not from stdin, newserv writes\n\
|
|
the output to INPUT-FILENAME.dec; if OUTPUT-FILENAME is '-', newserv writes the\n\
|
|
output to stdout. If stdout is a terminal, data written there is formatted in a\n\
|
|
hex/ASCII view; otherwise, raw (binary) data is written there.\n\
|
|
\n\
|
|
The actions are:\n\
|
|
help\n\
|
|
You\'re reading it now.\n\
|
|
compress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
|
decompress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
|
compress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
|
decompress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
|
Compress or decompress data using the PRS or BC0 algorithms. When\n\
|
|
compressing with PRS, the --compression-level=N option (default 1)\n\
|
|
specifies how aggressive the compressor should be in searching for literal\n\
|
|
sequences. A higher value generally means slower compression and a smaller\n\
|
|
output size. If 0 is given, the data is PRS-encoded but not actually\n\
|
|
compressed, resulting in valid PRS data which is larger than the input.\n\
|
|
prs-size [INPUT-FILENAME]\n\
|
|
Compute the decompressed size of the PRS-compressed input data, but don\'t\n\
|
|
write the decompressed data anywhere.\n\
|
|
encrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\
|
|
decrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\
|
|
Encrypt or decrypt data using PSO\'s standard network protocol encryption.\n\
|
|
By default, PSO V3 (GameCube/XBOX) encryption is used, but this can be\n\
|
|
overridden with the --pc or --bb options. The --seed=SEED option specifies\n\
|
|
the encryption seed (4 hex bytes for PC or GC, or 48 hex bytes for BB). For\n\
|
|
BB, the --key=KEY-NAME option is required as well, and refers to a .nsk\n\
|
|
file in system/blueburst/keys (without the directory or .nsk extension).\n\
|
|
For non-BB ciphers, the --big-endian option applies the cipher masks as\n\
|
|
big-endian instead of little-endian, which is necessary for some GameCube\n\
|
|
file formats.\n\
|
|
encrypt-trivial-data [--seed=BASIS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
|
decrypt-trivial-data [--seed=BASIS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
|
Encrypt or decrypt data using the Episode 3 trivial algorithm. If BASIS is\n\
|
|
given, it should be specified as one hex byte. If BASIS is not given,\n\
|
|
newserv will try all possible values and return the one that results in the\n\
|
|
greatest number of zero bytes in the output.\n\
|
|
encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
|
|
decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
|
|
Encrypt or decrypt a character or Guild Card file. If encrypting, the\n\
|
|
checksum is also recomputed and stored in the encrypted file. CRYPT-OPTION\n\
|
|
is required; it can be either --sys=SYSTEM-FILENAME or --seed=ROUND1-SEED\n\
|
|
(specified in hex).\n\
|
|
salvage-gci INPUT-FILENAME [--round2] [CRYPT-OPTION] [--bytes=SIZE]\n\
|
|
Attempt to find either the round-1 or round-2 decryption seed for a\n\
|
|
corrupted GCI file. If --round2 is given, then CRYPT-OPTION must be given\n\
|
|
(and should specify either a valid system file or the round1 seed).\n\
|
|
find-decryption-seed <OPTIONS...>\n\
|
|
Perform a brute-force search for a decryption seed of the given data. The\n\
|
|
ciphertext is specified with the --encrypted=DATA option and the expected\n\
|
|
plaintext is specified with the --decrypted=DATA option. The plaintext may\n\
|
|
include unmatched bytes (specified with the Phosg parse_data_string ?\n\
|
|
operator), but overall it must be the same length as the ciphertext. By\n\
|
|
default, this option uses PSO V3 encryption, but this can be overridden\n\
|
|
with --pc. (BB encryption seeds are too long to be searched for with this\n\
|
|
function.) By default, the number of worker threads is equal the the number\n\
|
|
of CPU cores in the system, but this can be overridden with the\n\
|
|
--threads=NUM-THREADS option.\n\
|
|
decode-sjis [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
|
Apply newserv\'s text decoding algorithm to the input data, producing\n\
|
|
little-endian UTF-16 output data.\n\
|
|
decode-gci INPUT-FILENAME [OPTIONS...]\n\
|
|
decode-vms INPUT-FILENAME [OPTIONS...]\n\
|
|
decode-dlq INPUT-FILENAME\n\
|
|
decode-qst INPUT-FILENAME\n\
|
|
Decode the input quest file into a compressed, unencrypted .bin or .dat\n\
|
|
file (or in the case of decode-qst, both a .bin and a .dat file).\n\
|
|
INPUT-FILENAME must be specified, but there is no OUTPUT-FILENAME; the\n\
|
|
output is written to INPUT-FILENAME.dec (or .bin, or .dat). If the output\n\
|
|
is a .dec file, you can rename it to .bin or .dat manually. DLQ and QST\n\
|
|
decoding are relatively simple operations, but GCI and VMS decoding can be\n\
|
|
computationally expensive if the file is encrypted and doesn\'t contain an\n\
|
|
embedded seed. If you know the player\'s serial number who generated the\n\
|
|
GCI or VMS file, use the --seed=SEED option and give the serial number (as\n\
|
|
a hex-encoded 32-bit integer). If you don\'t know the serial number,\n\
|
|
newserv will find it via a brute-force search, which will take a long time.\n\
|
|
cat-client ADDR:PORT\n\
|
|
Connect to the given server and simulate a PSO client. newserv will then\n\
|
|
print all the received commands to stdout, and forward any commands typed\n\
|
|
into stdin to the remote server. It is assumed that the input and output\n\
|
|
are terminals, so all commands are hex-encoded. The --patch, --dc, --pc,\n\
|
|
--gc, and --bb options can be used to select the command format and\n\
|
|
encryption. If --bb is used, the --key=KEY-NAME option is also required (as\n\
|
|
in decrypt-data above).\n\
|
|
show-ep3-data\n\
|
|
Print the Episode 3 maps and card definitions from the system/ep3 directory\n\
|
|
in a (sort of) human-readable format.\n\
|
|
describe-item DATA\n\
|
|
Print the name of the item given by DATA (in hex). DATA must not contain\n\
|
|
spaces. If DATA is 20 bytes, newserv assumes it contains an unused item ID\n\
|
|
field; if it is fewer bytes, up to 16 bytes are used.\n\
|
|
replay-log [INPUT-FILENAME] [OPTIONS...]\n\
|
|
Replay a terminal log as if it were a client session. input-filename may be\n\
|
|
specified for this option. This is used for regression testing, to make\n\
|
|
sure client sessions are repeatable and code changes don\'t affect existing\n\
|
|
(working) functionality.\n\
|
|
extract-gsl [INPUT-FILENAME] [--big-endian]\n\
|
|
extract-bml [INPUT-FILENAME] [--big-endian]\n\
|
|
Extract all files from a GSL or BML archive into the current directory.\n\
|
|
input-filename may be specified. If output-filename is specified, then it\n\
|
|
is treated as a prefix which is prepended to the filename of each file\n\
|
|
contained in the archive. If --big-endian is given, the archive header is\n\
|
|
read in GameCube format; otherwise it is read in PC/BB format.\n\
|
|
\n\
|
|
A few options apply to multiple modes described above:\n\
|
|
--parse-data\n\
|
|
For modes that take input (from a file or from stdin), parse the input as\n\
|
|
a hex string before encrypting/decoding/etc.\n\
|
|
--config=FILENAME\n\
|
|
Use this file instead of system/config.json.\n\
|
|
",
|
|
stderr);
|
|
}
|
|
|
|
enum class Behavior {
|
|
RUN_SERVER = 0,
|
|
COMPRESS_PRS,
|
|
DECOMPRESS_PRS,
|
|
COMPRESS_BC0,
|
|
DECOMPRESS_BC0,
|
|
PRS_SIZE,
|
|
PRS_DISASSEMBLE,
|
|
ENCRYPT_DATA,
|
|
DECRYPT_DATA,
|
|
ENCRYPT_TRIVIAL_DATA,
|
|
DECRYPT_TRIVIAL_DATA,
|
|
ENCRYPT_GCI_SAVE,
|
|
DECRYPT_GCI_SAVE,
|
|
FIND_DECRYPTION_SEED,
|
|
SALVAGE_GCI,
|
|
DECODE_QUEST_FILE,
|
|
DECODE_SJIS,
|
|
EXTRACT_GSL,
|
|
EXTRACT_BML,
|
|
FORMAT_ITEMRT_REL,
|
|
SHOW_EP3_DATA,
|
|
DESCRIBE_ITEM,
|
|
ENCODE_ITEM,
|
|
PARSE_OBJECT_GRAPH,
|
|
REPLAY_LOG,
|
|
CAT_CLIENT,
|
|
GENERATE_PRODUCT,
|
|
GENERATE_ALL_PRODUCTS,
|
|
INSPECT_PRODUCT,
|
|
PRODUCT_SPEED_TEST,
|
|
};
|
|
|
|
static bool behavior_takes_input_filename(Behavior b) {
|
|
return (b == Behavior::COMPRESS_PRS) ||
|
|
(b == Behavior::DECOMPRESS_PRS) ||
|
|
(b == Behavior::COMPRESS_BC0) ||
|
|
(b == Behavior::DECOMPRESS_BC0) ||
|
|
(b == Behavior::PRS_SIZE) ||
|
|
(b == Behavior::PRS_DISASSEMBLE) ||
|
|
(b == Behavior::ENCRYPT_DATA) ||
|
|
(b == Behavior::DECRYPT_DATA) ||
|
|
(b == Behavior::ENCRYPT_TRIVIAL_DATA) ||
|
|
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
|
|
(b == Behavior::DECRYPT_GCI_SAVE) ||
|
|
(b == Behavior::SALVAGE_GCI) ||
|
|
(b == Behavior::ENCRYPT_GCI_SAVE) ||
|
|
(b == Behavior::DECODE_QUEST_FILE) ||
|
|
(b == Behavior::DECODE_SJIS) ||
|
|
(b == Behavior::FORMAT_ITEMRT_REL) ||
|
|
(b == Behavior::EXTRACT_GSL) ||
|
|
(b == Behavior::EXTRACT_BML) ||
|
|
(b == Behavior::DESCRIBE_ITEM) ||
|
|
(b == Behavior::ENCODE_ITEM) ||
|
|
(b == Behavior::PARSE_OBJECT_GRAPH) ||
|
|
(b == Behavior::REPLAY_LOG) ||
|
|
(b == Behavior::CAT_CLIENT) ||
|
|
(b == Behavior::INSPECT_PRODUCT);
|
|
}
|
|
|
|
static bool behavior_takes_output_filename(Behavior b) {
|
|
return (b == Behavior::COMPRESS_PRS) ||
|
|
(b == Behavior::DECOMPRESS_PRS) ||
|
|
(b == Behavior::COMPRESS_BC0) ||
|
|
(b == Behavior::DECOMPRESS_BC0) ||
|
|
(b == Behavior::ENCRYPT_DATA) ||
|
|
(b == Behavior::DECRYPT_DATA) ||
|
|
(b == Behavior::ENCRYPT_TRIVIAL_DATA) ||
|
|
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
|
|
(b == Behavior::DECRYPT_GCI_SAVE) ||
|
|
(b == Behavior::ENCRYPT_GCI_SAVE) ||
|
|
(b == Behavior::DECODE_SJIS) ||
|
|
(b == Behavior::EXTRACT_GSL) ||
|
|
(b == Behavior::EXTRACT_BML);
|
|
}
|
|
|
|
enum class QuestFileFormat {
|
|
GCI = 0,
|
|
VMS,
|
|
DLQ,
|
|
QST,
|
|
};
|
|
|
|
int main(int argc, char** argv) {
|
|
Behavior behavior = Behavior::RUN_SERVER;
|
|
GameVersion cli_version = GameVersion::GC;
|
|
QuestFileFormat quest_file_type = QuestFileFormat::GCI;
|
|
string seed;
|
|
string key_file_name;
|
|
const char* config_filename = "system/config.json";
|
|
bool parse_data = false;
|
|
bool big_endian = false;
|
|
bool skip_little_endian = false;
|
|
bool skip_big_endian = false;
|
|
bool round2 = false;
|
|
bool skip_checksum = false;
|
|
uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF;
|
|
size_t offset = 0;
|
|
size_t stride = 1;
|
|
size_t num_threads = 0;
|
|
size_t bytes = 0;
|
|
size_t prs_compression_level = 1;
|
|
const char* find_decryption_seed_ciphertext = nullptr;
|
|
vector<const char*> find_decryption_seed_plaintexts;
|
|
const char* input_filename = nullptr;
|
|
const char* output_filename = nullptr;
|
|
const char* system_filename = nullptr;
|
|
const char* replay_required_access_key = "";
|
|
const char* replay_required_password = "";
|
|
uint32_t root_object_address = 0;
|
|
uint16_t ep3_card_id = 0xFFFF;
|
|
uint8_t domain = 1;
|
|
uint8_t subdomain = 0xFF;
|
|
for (int x = 1; x < argc; x++) {
|
|
if (!strcmp(argv[x], "--help")) {
|
|
print_usage();
|
|
return 0;
|
|
} else if (!strncmp(argv[x], "--threads=", 10)) {
|
|
num_threads = strtoull(&argv[x][10], nullptr, 0);
|
|
} else if (!strcmp(argv[x], "--patch")) {
|
|
cli_version = GameVersion::PATCH;
|
|
} else if (!strcmp(argv[x], "--dc")) {
|
|
cli_version = GameVersion::DC;
|
|
} else if (!strcmp(argv[x], "--pc")) {
|
|
cli_version = GameVersion::PC;
|
|
} else if (!strcmp(argv[x], "--gc")) {
|
|
cli_version = GameVersion::GC;
|
|
} else if (!strcmp(argv[x], "--xb")) {
|
|
cli_version = GameVersion::XB;
|
|
} else if (!strcmp(argv[x], "--bb")) {
|
|
cli_version = GameVersion::BB;
|
|
} else if (!strncmp(argv[x], "--compression-level=", 20)) {
|
|
prs_compression_level = strtoull(&argv[x][20], nullptr, 0);
|
|
} else if (!strcmp(argv[x], "--round2")) {
|
|
round2 = true;
|
|
} else if (!strncmp(argv[x], "--bytes=", 8)) {
|
|
bytes = strtoull(&argv[x][8], nullptr, 0);
|
|
} else if (!strncmp(argv[x], "--offset=", 9)) {
|
|
offset = strtoull(&argv[x][9], nullptr, 0);
|
|
} else if (!strncmp(argv[x], "--stride=", 9)) {
|
|
stride = strtoull(&argv[x][9], nullptr, 0);
|
|
} else if (!strcmp(argv[x], "--skip-checksum")) {
|
|
skip_checksum = true;
|
|
} else if (!strncmp(argv[x], "--seed=", 7)) {
|
|
seed = &argv[x][7];
|
|
} else if (!strncmp(argv[x], "--round2-seed=", 14)) {
|
|
override_round2_seed = strtoull(&argv[x][14], nullptr, 16);
|
|
} else if (!strncmp(argv[x], "--key=", 6)) {
|
|
key_file_name = &argv[x][6];
|
|
} else if (!strncmp(argv[x], "--sys=", 6)) {
|
|
system_filename = &argv[x][6];
|
|
} else if (!strncmp(argv[x], "--domain=", 9)) {
|
|
domain = atoi(&argv[x][9]);
|
|
} else if (!strncmp(argv[x], "--subdomain=", 12)) {
|
|
subdomain = atoi(&argv[x][12]);
|
|
} else if (!strncmp(argv[x], "--encrypted=", 12)) {
|
|
find_decryption_seed_ciphertext = &argv[x][12];
|
|
} else if (!strncmp(argv[x], "--decrypted=", 12)) {
|
|
find_decryption_seed_plaintexts.emplace_back(&argv[x][12]);
|
|
} else if (!strcmp(argv[x], "--parse-data")) {
|
|
parse_data = true;
|
|
} else if (!strcmp(argv[x], "--big-endian")) {
|
|
big_endian = true;
|
|
} else if (!strcmp(argv[x], "--skip-little-endian")) {
|
|
skip_little_endian = true;
|
|
} else if (!strcmp(argv[x], "--skip-big-endian")) {
|
|
skip_big_endian = true;
|
|
} else if (!strncmp(argv[x], "--require-password=", 19)) {
|
|
replay_required_password = &argv[x][19];
|
|
} else if (!strncmp(argv[x], "--require-access-key=", 21)) {
|
|
replay_required_access_key = &argv[x][21];
|
|
} else if (!strncmp(argv[x], "--root-addr=", 12)) {
|
|
root_object_address = strtoul(&argv[x][12], nullptr, 16);
|
|
} else if (!strncmp(argv[x], "--config=", 9)) {
|
|
config_filename = &argv[x][9];
|
|
|
|
} else if (!strcmp(argv[x], "-") || argv[x][0] != '-') {
|
|
if (behavior == Behavior::RUN_SERVER) {
|
|
if (!strcmp(argv[x], "help")) {
|
|
print_usage();
|
|
return 0;
|
|
}
|
|
if (!strcmp(argv[x], "compress-prs")) {
|
|
behavior = Behavior::COMPRESS_PRS;
|
|
} else if (!strcmp(argv[x], "decompress-prs")) {
|
|
behavior = Behavior::DECOMPRESS_PRS;
|
|
} else if (!strcmp(argv[x], "compress-bc0")) {
|
|
behavior = Behavior::COMPRESS_BC0;
|
|
} else if (!strcmp(argv[x], "decompress-bc0")) {
|
|
behavior = Behavior::DECOMPRESS_BC0;
|
|
} else if (!strcmp(argv[x], "prs-size")) {
|
|
behavior = Behavior::PRS_SIZE;
|
|
} else if (!strcmp(argv[x], "disassemble-prs")) {
|
|
behavior = Behavior::PRS_DISASSEMBLE;
|
|
} else if (!strcmp(argv[x], "encrypt-data")) {
|
|
behavior = Behavior::ENCRYPT_DATA;
|
|
} else if (!strcmp(argv[x], "decrypt-data")) {
|
|
behavior = Behavior::DECRYPT_DATA;
|
|
} else if (!strcmp(argv[x], "encrypt-trivial-data")) {
|
|
behavior = Behavior::ENCRYPT_TRIVIAL_DATA;
|
|
} else if (!strcmp(argv[x], "decrypt-trivial-data")) {
|
|
behavior = Behavior::DECRYPT_TRIVIAL_DATA;
|
|
} else if (!strcmp(argv[x], "decrypt-gci-save")) {
|
|
behavior = Behavior::DECRYPT_GCI_SAVE;
|
|
} else if (!strcmp(argv[x], "encrypt-gci-save")) {
|
|
behavior = Behavior::ENCRYPT_GCI_SAVE;
|
|
} else if (!strcmp(argv[x], "find-decryption-seed")) {
|
|
behavior = Behavior::FIND_DECRYPTION_SEED;
|
|
} else if (!strcmp(argv[x], "salvage-gci")) {
|
|
behavior = Behavior::SALVAGE_GCI;
|
|
} else if (!strcmp(argv[x], "decode-sjis")) {
|
|
behavior = Behavior::DECODE_SJIS;
|
|
} else if (!strcmp(argv[x], "decode-gci")) {
|
|
behavior = Behavior::DECODE_QUEST_FILE;
|
|
quest_file_type = QuestFileFormat::GCI;
|
|
} else if (!strcmp(argv[x], "decode-vms")) {
|
|
behavior = Behavior::DECODE_QUEST_FILE;
|
|
quest_file_type = QuestFileFormat::VMS;
|
|
} else if (!strcmp(argv[x], "decode-dlq")) {
|
|
behavior = Behavior::DECODE_QUEST_FILE;
|
|
quest_file_type = QuestFileFormat::DLQ;
|
|
} else if (!strcmp(argv[x], "decode-qst")) {
|
|
behavior = Behavior::DECODE_QUEST_FILE;
|
|
quest_file_type = QuestFileFormat::QST;
|
|
} else if (!strcmp(argv[x], "cat-client")) {
|
|
behavior = Behavior::CAT_CLIENT;
|
|
} else if (!strcmp(argv[x], "format-itemrt-rel")) {
|
|
behavior = Behavior::FORMAT_ITEMRT_REL;
|
|
} else if (!strcmp(argv[x], "show-ep3-data")) {
|
|
behavior = Behavior::SHOW_EP3_DATA;
|
|
} else if (!strcmp(argv[x], "describe-item")) {
|
|
behavior = Behavior::DESCRIBE_ITEM;
|
|
} else if (!strcmp(argv[x], "encode-item")) {
|
|
behavior = Behavior::ENCODE_ITEM;
|
|
} else if (!strcmp(argv[x], "parse-object-graph")) {
|
|
behavior = Behavior::PARSE_OBJECT_GRAPH;
|
|
} else if (!strcmp(argv[x], "replay-log")) {
|
|
behavior = Behavior::REPLAY_LOG;
|
|
} else if (!strcmp(argv[x], "extract-gsl")) {
|
|
behavior = Behavior::EXTRACT_GSL;
|
|
} else if (!strcmp(argv[x], "extract-bml")) {
|
|
behavior = Behavior::EXTRACT_BML;
|
|
} else if (!strcmp(argv[x], "generate-product")) {
|
|
behavior = Behavior::GENERATE_PRODUCT;
|
|
} else if (!strcmp(argv[x], "generate-all-products")) {
|
|
behavior = Behavior::GENERATE_ALL_PRODUCTS;
|
|
} else if (!strcmp(argv[x], "inspect-product")) {
|
|
behavior = Behavior::INSPECT_PRODUCT;
|
|
} else if (!strcmp(argv[x], "product-speed-test")) {
|
|
behavior = Behavior::PRODUCT_SPEED_TEST;
|
|
} else {
|
|
throw invalid_argument(string_printf("unknown command: %s (try --help)", argv[x]));
|
|
}
|
|
} else if (!input_filename && behavior_takes_input_filename(behavior)) {
|
|
input_filename = argv[x];
|
|
} else if (!output_filename && behavior_takes_output_filename(behavior)) {
|
|
output_filename = argv[x];
|
|
} else {
|
|
throw invalid_argument(string_printf("unknown option: %s (try --help)", argv[x]));
|
|
}
|
|
|
|
} else {
|
|
throw invalid_argument(string_printf("unknown option: %s (try --help)", argv[x]));
|
|
}
|
|
}
|
|
|
|
auto read_input_data = [&]() -> string {
|
|
string data;
|
|
if (input_filename && strcmp(input_filename, "-")) {
|
|
data = load_file(input_filename);
|
|
} else {
|
|
data = read_all(stdin);
|
|
}
|
|
if (parse_data) {
|
|
data = parse_data_string(data, nullptr, ParseDataFlags::ALLOW_FILES);
|
|
}
|
|
return data;
|
|
};
|
|
|
|
auto write_output_data = [&](const void* data, size_t size) {
|
|
// If the output is to a specified file, write it there
|
|
if (output_filename && strcmp(output_filename, "-")) {
|
|
save_file(output_filename, data, size);
|
|
// If no output filename is given and an input filename is given, write to
|
|
// <input-filename>.dec (or an appropriate extension, if it can be
|
|
// autodetected)
|
|
} else if (!output_filename && input_filename && strcmp(input_filename, "-")) {
|
|
string filename = input_filename;
|
|
if (behavior == Behavior::COMPRESS_PRS) {
|
|
if (ends_with(filename, ".bind") ||
|
|
ends_with(filename, ".datd") ||
|
|
ends_with(filename, ".mnmd")) {
|
|
filename.resize(filename.size() - 1);
|
|
} else {
|
|
filename += ".prs";
|
|
}
|
|
} else if (behavior == Behavior::DECOMPRESS_PRS) {
|
|
if (ends_with(filename, ".bin") ||
|
|
ends_with(filename, ".dat") ||
|
|
ends_with(filename, ".mnm")) {
|
|
filename += "d";
|
|
} else {
|
|
filename += ".dec";
|
|
}
|
|
} else if (behavior == Behavior::ENCRYPT_GCI_SAVE) {
|
|
if (ends_with(filename, ".gcid")) {
|
|
filename.resize(filename.size() - 1);
|
|
} else {
|
|
filename += ".gci";
|
|
}
|
|
} else if (behavior == Behavior::DECRYPT_GCI_SAVE) {
|
|
if (ends_with(filename, ".gci")) {
|
|
filename += "d";
|
|
} else {
|
|
filename += ".dec";
|
|
}
|
|
} else {
|
|
filename += ".dec";
|
|
}
|
|
save_file(filename, data, size);
|
|
// If stdout is a terminal, use print_data to write the result
|
|
} else if (isatty(fileno(stdout))) {
|
|
print_data(stdout, data, size);
|
|
fflush(stdout);
|
|
// If stdout is not a terminal, write the data as-is
|
|
} else {
|
|
fwritex(stdout, data, size);
|
|
fflush(stdout);
|
|
}
|
|
};
|
|
|
|
switch (behavior) {
|
|
case Behavior::COMPRESS_PRS:
|
|
case Behavior::DECOMPRESS_PRS:
|
|
case Behavior::COMPRESS_BC0:
|
|
case Behavior::DECOMPRESS_BC0: {
|
|
string data = read_input_data();
|
|
size_t input_bytes = data.size();
|
|
auto progress_fn = [&](size_t input_progress, size_t output_progress) -> void {
|
|
float progress = static_cast<float>(input_progress * 100) / input_bytes;
|
|
float size_ratio = static_cast<float>(output_progress * 100) / input_progress;
|
|
fprintf(stderr, "... %zu/%zu (%g%%) => %zu (%g%%) \r",
|
|
input_progress, input_bytes, progress, output_progress, size_ratio);
|
|
};
|
|
|
|
uint64_t start = now();
|
|
if (behavior == Behavior::COMPRESS_PRS) {
|
|
data = prs_compress(data, prs_compression_level, progress_fn);
|
|
} else if (behavior == Behavior::DECOMPRESS_PRS) {
|
|
data = prs_decompress(data);
|
|
} else if (behavior == Behavior::COMPRESS_BC0) {
|
|
data = bc0_compress(data, progress_fn);
|
|
} else if (behavior == Behavior::DECOMPRESS_BC0) {
|
|
data = bc0_decompress(data);
|
|
} else {
|
|
throw logic_error("invalid behavior");
|
|
}
|
|
uint64_t end = now();
|
|
string time_str = format_duration(end - start);
|
|
|
|
float size_ratio = static_cast<float>(data.size() * 100) / input_bytes;
|
|
double bytes_per_sec = input_bytes / (static_cast<double>(end - start) / 1000000.0);
|
|
string bytes_per_sec_str = format_size(bytes_per_sec);
|
|
log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output (%g%%) in %s (%s / sec)",
|
|
input_bytes, input_bytes, data.size(), data.size(), size_ratio, time_str.c_str(), bytes_per_sec_str.c_str());
|
|
|
|
write_output_data(data.data(), data.size());
|
|
break;
|
|
}
|
|
|
|
case Behavior::PRS_SIZE: {
|
|
string data = read_input_data();
|
|
size_t input_bytes = data.size();
|
|
size_t output_bytes = prs_decompress_size(data);
|
|
log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output",
|
|
input_bytes, input_bytes, output_bytes, output_bytes);
|
|
break;
|
|
}
|
|
|
|
case Behavior::PRS_DISASSEMBLE: {
|
|
prs_disassemble(stdout, read_input_data());
|
|
break;
|
|
}
|
|
|
|
case Behavior::DECRYPT_DATA:
|
|
case Behavior::ENCRYPT_DATA: {
|
|
shared_ptr<PSOEncryption> crypt;
|
|
switch (cli_version) {
|
|
case GameVersion::PATCH:
|
|
case GameVersion::DC:
|
|
case GameVersion::PC:
|
|
crypt.reset(new PSOV2Encryption(stoul(seed, nullptr, 16)));
|
|
break;
|
|
case GameVersion::GC:
|
|
case GameVersion::XB:
|
|
crypt.reset(new PSOV3Encryption(stoul(seed, nullptr, 16)));
|
|
break;
|
|
case GameVersion::BB: {
|
|
seed = parse_data_string(seed, nullptr, ParseDataFlags::ALLOW_FILES);
|
|
auto key = load_object_file<PSOBBEncryption::KeyFile>(
|
|
"system/blueburst/keys/" + key_file_name + ".nsk");
|
|
crypt.reset(new PSOBBEncryption(key, seed.data(), seed.size()));
|
|
break;
|
|
}
|
|
default:
|
|
throw logic_error("invalid game version");
|
|
}
|
|
|
|
string data = read_input_data();
|
|
|
|
size_t original_size = data.size();
|
|
data.resize((data.size() + 7) & (~7), '\0');
|
|
|
|
if (big_endian) {
|
|
uint32_t* dwords = reinterpret_cast<uint32_t*>(data.data());
|
|
for (size_t x = 0; x < (data.size() >> 2); x++) {
|
|
dwords[x] = bswap32(dwords[x]);
|
|
}
|
|
}
|
|
|
|
if (behavior == Behavior::DECRYPT_DATA) {
|
|
crypt->decrypt(data.data(), data.size());
|
|
} else if (behavior == Behavior::ENCRYPT_DATA) {
|
|
crypt->encrypt(data.data(), data.size());
|
|
} else {
|
|
throw logic_error("invalid behavior");
|
|
}
|
|
|
|
if (big_endian) {
|
|
uint32_t* dwords = reinterpret_cast<uint32_t*>(data.data());
|
|
for (size_t x = 0; x < (data.size() >> 2); x++) {
|
|
dwords[x] = bswap32(dwords[x]);
|
|
}
|
|
}
|
|
|
|
data.resize(original_size);
|
|
|
|
write_output_data(data.data(), data.size());
|
|
|
|
break;
|
|
}
|
|
|
|
case Behavior::ENCRYPT_TRIVIAL_DATA:
|
|
case Behavior::DECRYPT_TRIVIAL_DATA: {
|
|
string data = read_input_data();
|
|
uint8_t basis;
|
|
if (seed.empty()) {
|
|
uint8_t best_seed = 0x00;
|
|
size_t best_seed_score = 0;
|
|
for (size_t z = 0; z < 0x100; z++) {
|
|
string decrypted = data;
|
|
decrypt_trivial_gci_data(decrypted.data(), decrypted.size(), z);
|
|
size_t score = 0;
|
|
for (size_t x = 0; x < decrypted.size(); x++) {
|
|
if (decrypted[x] == '\0') {
|
|
score++;
|
|
}
|
|
}
|
|
if (score > best_seed_score) {
|
|
best_seed = z;
|
|
best_seed_score = score;
|
|
}
|
|
}
|
|
fprintf(stderr, "Basis appears to be %02hhX (%zu zero bytes in output)\n",
|
|
best_seed, best_seed_score);
|
|
basis = best_seed;
|
|
} else {
|
|
basis = stoul(seed, nullptr, 16);
|
|
}
|
|
decrypt_trivial_gci_data(data.data(), data.size(), basis);
|
|
write_output_data(data.data(), data.size());
|
|
break;
|
|
}
|
|
|
|
case Behavior::ENCRYPT_GCI_SAVE:
|
|
case Behavior::DECRYPT_GCI_SAVE: {
|
|
uint32_t round1_seed;
|
|
if (system_filename) {
|
|
string system_data = load_file(system_filename);
|
|
StringReader r(system_data);
|
|
const auto& header = r.get<PSOGCIFileHeader>();
|
|
header.check();
|
|
const auto& system = r.get<PSOGCSystemFile>();
|
|
round1_seed = system.creation_timestamp;
|
|
} else if (!seed.empty()) {
|
|
round1_seed = stoul(seed, nullptr, 16);
|
|
} else {
|
|
// TODO: We can support brute-forcing character file encryption, but I'm
|
|
// lazy and this would probably not be useful for anyone.
|
|
throw runtime_error("either --sys or --seed must be given");
|
|
}
|
|
|
|
bool is_decrypt = (behavior == Behavior::DECRYPT_GCI_SAVE);
|
|
|
|
auto data = read_input_data();
|
|
StringReader r(data);
|
|
const auto& header = r.get<PSOGCIFileHeader>();
|
|
header.check();
|
|
|
|
size_t data_start_offset = r.where();
|
|
|
|
auto process_file = [&]<typename StructT>() {
|
|
if (is_decrypt) {
|
|
const void* data_section = r.getv(header.data_size);
|
|
auto decrypted = decrypt_gci_fixed_size_file_data_section<StructT>(
|
|
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
|
|
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
|
|
} else {
|
|
const auto& s = r.get<StructT>();
|
|
auto encrypted = encrypt_gci_fixed_size_file_data_section<StructT>(
|
|
s, round1_seed);
|
|
if (data_start_offset + encrypted.size() > data.size()) {
|
|
throw runtime_error("encrypted result exceeds file size");
|
|
}
|
|
memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size());
|
|
}
|
|
};
|
|
|
|
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
|
|
process_file.template operator()<PSOGCGuildCardFile>();
|
|
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
|
|
process_file.template operator()<PSOGCCharacterFile>();
|
|
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
|
|
auto* charfile = reinterpret_cast<PSOGCEp3CharacterFile*>(data.data() + data_start_offset);
|
|
if (!is_decrypt) {
|
|
for (size_t z = 0; z < charfile->characters.size(); z++) {
|
|
charfile->characters[z].ep3_config.encrypt(random_object<uint8_t>());
|
|
}
|
|
}
|
|
process_file.template operator()<PSOGCEp3CharacterFile>();
|
|
if (is_decrypt) {
|
|
for (size_t z = 0; z < charfile->characters.size(); z++) {
|
|
charfile->characters[z].ep3_config.decrypt();
|
|
}
|
|
}
|
|
} else {
|
|
throw runtime_error("unrecognized save type");
|
|
}
|
|
|
|
write_output_data(data.data(), data.size());
|
|
|
|
break;
|
|
}
|
|
|
|
case Behavior::SALVAGE_GCI: {
|
|
uint64_t likely_round1_seed = 0xFFFFFFFFFFFFFFFF;
|
|
if (system_filename) {
|
|
try {
|
|
string system_data = load_file(system_filename);
|
|
StringReader r(system_data);
|
|
const auto& header = r.get<PSOGCIFileHeader>();
|
|
header.check();
|
|
const auto& system = r.get<PSOGCSystemFile>();
|
|
likely_round1_seed = system.creation_timestamp;
|
|
log_info("System file appears to be in order; round1 seed is %08" PRIX64, likely_round1_seed);
|
|
} catch (const exception& e) {
|
|
log_warning("Cannot parse system file (%s); ignoring it", e.what());
|
|
}
|
|
} else if (!seed.empty()) {
|
|
likely_round1_seed = stoul(seed, nullptr, 16);
|
|
log_info("Specified round1 seed is %08" PRIX64, likely_round1_seed);
|
|
}
|
|
|
|
if (round2 && likely_round1_seed > 0x100000000) {
|
|
throw invalid_argument("cannot find round2 seed without known round1 seed");
|
|
}
|
|
|
|
auto data = read_input_data();
|
|
StringReader r(data);
|
|
const auto& header = r.get<PSOGCIFileHeader>();
|
|
header.check();
|
|
|
|
const void* data_section = r.getv(header.data_size);
|
|
|
|
auto process_file = [&]<typename StructT>() {
|
|
vector<multimap<size_t, uint32_t>> top_seeds_by_thread(
|
|
num_threads ? num_threads : thread::hardware_concurrency());
|
|
parallel_range<uint64_t>(
|
|
[&](uint64_t seed, size_t thread_num) -> bool {
|
|
size_t zero_count;
|
|
if (round2) {
|
|
string decrypted = decrypt_gci_fixed_size_file_data_section_for_salvage(
|
|
data_section, header.data_size, likely_round1_seed, seed, bytes);
|
|
zero_count = count_zeroes(
|
|
decrypted.data() + offset,
|
|
decrypted.size() - offset,
|
|
stride);
|
|
} else {
|
|
auto decrypted = decrypt_gci_fixed_size_file_data_section<StructT>(
|
|
data_section,
|
|
header.data_size,
|
|
seed,
|
|
true);
|
|
zero_count = count_zeroes(
|
|
reinterpret_cast<const uint8_t*>(&decrypted) + offset,
|
|
sizeof(decrypted) - offset,
|
|
stride);
|
|
}
|
|
auto& top_seeds = top_seeds_by_thread[thread_num];
|
|
if (top_seeds.size() < 10 || (zero_count >= top_seeds.begin()->second)) {
|
|
top_seeds.emplace(zero_count, seed);
|
|
if (top_seeds.size() > 10) {
|
|
top_seeds.erase(top_seeds.begin());
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
0,
|
|
0x100000000,
|
|
num_threads);
|
|
|
|
multimap<size_t, uint32_t> top_seeds;
|
|
for (const auto& thread_top_seeds : top_seeds_by_thread) {
|
|
for (const auto& it : thread_top_seeds) {
|
|
top_seeds.emplace(it.first, it.second);
|
|
}
|
|
}
|
|
for (const auto& it : top_seeds) {
|
|
const char* sys_seed_str = (!round2 && (it.second == likely_round1_seed))
|
|
? " (this is the seed from the system file)"
|
|
: "";
|
|
log_info("Round %c seed %08" PRIX32 " resulted in %zu zero bytes%s",
|
|
round2 ? '2' : '1', it.second, it.first, sys_seed_str);
|
|
}
|
|
};
|
|
|
|
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
|
|
process_file.template operator()<PSOGCGuildCardFile>();
|
|
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
|
|
process_file.template operator()<PSOGCCharacterFile>();
|
|
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
|
|
process_file.template operator()<PSOGCEp3CharacterFile>();
|
|
} else {
|
|
throw runtime_error("unrecognized save type");
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case Behavior::FIND_DECRYPTION_SEED: {
|
|
if (find_decryption_seed_plaintexts.empty() || !find_decryption_seed_ciphertext) {
|
|
throw runtime_error("both --encrypted and --decrypted must be specified");
|
|
}
|
|
if (cli_version == GameVersion::BB) {
|
|
throw runtime_error("--find-decryption-seed cannot be used for BB ciphers");
|
|
}
|
|
|
|
size_t max_plaintext_size = 0;
|
|
vector<pair<string, string>> plaintexts;
|
|
for (const auto& plaintext_ascii : find_decryption_seed_plaintexts) {
|
|
string mask;
|
|
string data = parse_data_string(plaintext_ascii, &mask, ParseDataFlags::ALLOW_FILES);
|
|
if (data.size() != mask.size()) {
|
|
throw logic_error("plaintext and mask are not the same size");
|
|
}
|
|
max_plaintext_size = max<size_t>(max_plaintext_size, data.size());
|
|
plaintexts.emplace_back(std::move(data), std::move(mask));
|
|
}
|
|
string ciphertext = parse_data_string(find_decryption_seed_ciphertext, nullptr, ParseDataFlags::ALLOW_FILES);
|
|
|
|
auto mask_match = +[](const void* a, const void* b, const void* m, size_t size) -> bool {
|
|
const uint8_t* a8 = reinterpret_cast<const uint8_t*>(a);
|
|
const uint8_t* b8 = reinterpret_cast<const uint8_t*>(b);
|
|
const uint8_t* m8 = reinterpret_cast<const uint8_t*>(m);
|
|
for (size_t z = 0; z < size; z++) {
|
|
if ((a8[z] & m8[z]) != (b8[z] & m8[z])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
bool is_v3 = ((cli_version == GameVersion::GC) || (cli_version == GameVersion::XB));
|
|
uint64_t seed = parallel_range<uint64_t>([&](uint64_t seed, size_t) -> bool {
|
|
string be_decrypt_buf = ciphertext.substr(0, max_plaintext_size);
|
|
string le_decrypt_buf = ciphertext.substr(0, max_plaintext_size);
|
|
if (is_v3) {
|
|
PSOV3Encryption(seed).encrypt_both_endian(
|
|
le_decrypt_buf.data(),
|
|
be_decrypt_buf.data(),
|
|
be_decrypt_buf.size());
|
|
} else {
|
|
PSOV2Encryption(seed).encrypt_both_endian(
|
|
le_decrypt_buf.data(),
|
|
be_decrypt_buf.data(),
|
|
be_decrypt_buf.size());
|
|
}
|
|
|
|
for (const auto& plaintext : plaintexts) {
|
|
if (!skip_little_endian) {
|
|
if (mask_match(le_decrypt_buf.data(), plaintext.first.data(), plaintext.second.data(), plaintext.second.size())) {
|
|
return true;
|
|
}
|
|
}
|
|
if (!skip_big_endian) {
|
|
if (mask_match(be_decrypt_buf.data(), plaintext.first.data(), plaintext.second.data(), plaintext.second.size())) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
0, 0x100000000, num_threads);
|
|
|
|
if (seed < 0x100000000) {
|
|
log_info("Found seed %08" PRIX64, seed);
|
|
} else {
|
|
log_error("No seed found");
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Behavior::DECODE_QUEST_FILE: {
|
|
if (!input_filename || !strcmp(input_filename, "-")) {
|
|
throw invalid_argument("an input filename is required");
|
|
}
|
|
|
|
string output_filename_base = input_filename;
|
|
if (quest_file_type == QuestFileFormat::GCI) {
|
|
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
|
|
auto decoded = Quest::decode_gci_file(input_filename, num_threads, dec_seed);
|
|
save_file(output_filename_base + ".dec", decoded);
|
|
} else if (quest_file_type == QuestFileFormat::VMS) {
|
|
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
|
|
auto decoded = Quest::decode_vms_file(input_filename, num_threads, dec_seed);
|
|
save_file(output_filename_base + ".dec", decoded);
|
|
} else if (quest_file_type == QuestFileFormat::DLQ) {
|
|
auto decoded = Quest::decode_dlq_file(input_filename);
|
|
save_file(output_filename_base + ".dec", decoded);
|
|
} else if (quest_file_type == QuestFileFormat::QST) {
|
|
auto data = Quest::decode_qst_file(input_filename);
|
|
save_file(output_filename_base + ".bin", data.first);
|
|
save_file(output_filename_base + ".dat", data.second);
|
|
} else {
|
|
throw logic_error("invalid quest file format");
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Behavior::DECODE_SJIS: {
|
|
string data = read_input_data();
|
|
auto decoded = decode_sjis(data);
|
|
write_output_data(decoded.data(), decoded.size() * sizeof(decoded[0]));
|
|
break;
|
|
}
|
|
|
|
case Behavior::EXTRACT_GSL:
|
|
case Behavior::EXTRACT_BML: {
|
|
if (!output_filename) {
|
|
output_filename = "";
|
|
} else if (!strcmp(output_filename, "-")) {
|
|
throw invalid_argument("output prefix cannot be stdout");
|
|
}
|
|
|
|
string data = read_input_data();
|
|
shared_ptr<string> data_shared(new string(std::move(data)));
|
|
|
|
if (behavior == Behavior::EXTRACT_GSL) {
|
|
GSLArchive arch(data_shared, big_endian);
|
|
for (const auto& entry_it : arch.all_entries()) {
|
|
auto e = arch.get(entry_it.first);
|
|
string out_file = output_filename + entry_it.first;
|
|
save_file(out_file.c_str(), e.first, e.second);
|
|
fprintf(stderr, "... %s\n", out_file.c_str());
|
|
}
|
|
} else {
|
|
BMLArchive arch(data_shared, big_endian);
|
|
for (const auto& entry_it : arch.all_entries()) {
|
|
{
|
|
auto e = arch.get(entry_it.first);
|
|
string data = prs_decompress(e.first, e.second);
|
|
string out_file = output_filename + entry_it.first;
|
|
save_file(out_file, data);
|
|
fprintf(stderr, "... %s\n", out_file.c_str());
|
|
}
|
|
|
|
auto gvm_e = arch.get_gvm(entry_it.first);
|
|
if (gvm_e.second) {
|
|
string data = prs_decompress(gvm_e.first, gvm_e.second);
|
|
string out_file = output_filename + entry_it.first + ".gvm";
|
|
save_file(out_file, data);
|
|
fprintf(stderr, "... %s\n", out_file.c_str());
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Behavior::CAT_CLIENT: {
|
|
shared_ptr<PSOBBEncryption::KeyFile> key;
|
|
if (cli_version == GameVersion::BB) {
|
|
if (key_file_name.empty()) {
|
|
throw runtime_error("a key filename is required for BB client emulation");
|
|
}
|
|
key.reset(new PSOBBEncryption::KeyFile(
|
|
load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + key_file_name + ".nsk")));
|
|
}
|
|
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
|
|
auto cat_client_remote = make_sockaddr_storage(parse_netloc(input_filename)).first;
|
|
CatSession session(base, cat_client_remote, cli_version, key);
|
|
event_base_dispatch(base.get());
|
|
break;
|
|
}
|
|
|
|
case Behavior::FORMAT_ITEMRT_REL: {
|
|
shared_ptr<string> data(new string(read_input_data()));
|
|
RELRareItemSet rs(data);
|
|
|
|
auto format_drop = +[](const RareItemSet::ExpandedDrop& r) -> string {
|
|
ItemData item;
|
|
item.data1[0] = r.item_code[0];
|
|
item.data1[1] = r.item_code[1];
|
|
item.data1[2] = r.item_code[2];
|
|
string name = item.name(false);
|
|
|
|
auto frac = reduce_fraction<uint64_t>(r.probability, 0x100000000);
|
|
return string_printf(
|
|
"(%08" PRIX32 " => %" PRIu64 "/%" PRIu64 ") %02hhX%02hhX%02hhX (%s)",
|
|
r.probability, frac.first, frac.second, r.item_code[0], r.item_code[1], r.item_code[2], name.c_str());
|
|
};
|
|
|
|
auto print_collection = [&](GameMode mode, Episode episode, uint8_t difficulty, uint8_t section_id) -> void {
|
|
string secid_name = name_for_section_id(section_id);
|
|
fprintf(stdout, "%s %s %s %s\n",
|
|
name_for_mode(mode),
|
|
name_for_episode(episode),
|
|
name_for_difficulty(difficulty),
|
|
secid_name.c_str());
|
|
|
|
fprintf(stdout, " Monster rares:\n");
|
|
for (size_t z = 0; z < 0x65; z++) {
|
|
for (const auto& spec : rs.get_enemy_specs(mode, episode, difficulty, section_id, z)) {
|
|
string s = format_drop(spec);
|
|
fprintf(stdout, " %02zX: %s\n", z, s.c_str());
|
|
}
|
|
}
|
|
|
|
fprintf(stdout, " Box rares:\n");
|
|
for (size_t area = 0; area < 0x12; area++) {
|
|
for (const auto& spec : rs.get_box_specs(mode, episode, difficulty, section_id, area)) {
|
|
string s = format_drop(spec);
|
|
fprintf(stdout, " (area %02zX) %s\n", area, s.c_str());
|
|
}
|
|
}
|
|
};
|
|
|
|
static const vector<Episode> episodes = {
|
|
Episode::EP1,
|
|
Episode::EP2,
|
|
Episode::EP4,
|
|
};
|
|
for (Episode episode : episodes) {
|
|
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
|
print_collection(GameMode::NORMAL, episode, difficulty, section_id);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Behavior::DESCRIBE_ITEM: {
|
|
string data = parse_data_string(input_filename);
|
|
|
|
ItemData item;
|
|
if (data.size() == sizeof(ItemData)) {
|
|
item = *reinterpret_cast<const ItemData*>(data.data());
|
|
} else {
|
|
memcpy(&item.data1[0], data.data(), min<size_t>(sizeof(item.data1), data.size()));
|
|
if (data.size() > sizeof(item.data1)) {
|
|
memcpy(&item.data2[0], data.data() + sizeof(item.data1), min<size_t>(sizeof(item.data2), data.size() - sizeof(item.data1)));
|
|
}
|
|
}
|
|
|
|
string desc = item.name(false);
|
|
log_info("Item: %s", desc.c_str());
|
|
break;
|
|
}
|
|
|
|
case Behavior::ENCODE_ITEM: {
|
|
ItemData item(input_filename);
|
|
string desc = item.name(false);
|
|
log_info("Data: %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX",
|
|
item.data1[0], item.data1[1], item.data1[2], item.data1[3],
|
|
item.data1[4], item.data1[5], item.data1[6], item.data1[7],
|
|
item.data1[8], item.data1[9], item.data1[10], item.data1[11],
|
|
item.data2[0], item.data2[1], item.data2[2], item.data2[3]);
|
|
log_info("Description: %s", desc.c_str());
|
|
break;
|
|
}
|
|
|
|
case Behavior::SHOW_EP3_DATA: {
|
|
config_log.info("Collecting Episode 3 data");
|
|
Episode3::DataIndex index("system/ep3", Episode3::BehaviorFlag::LOAD_CARD_TEXT);
|
|
|
|
if (ep3_card_id == 0xFFFF) {
|
|
auto map_ids = index.all_map_ids();
|
|
log_info("%zu maps", map_ids.size());
|
|
for (uint32_t map_id : map_ids) {
|
|
auto map = index.definition_for_map_number(map_id);
|
|
string s = map->map.str(&index);
|
|
fprintf(stdout, "%s\n", s.c_str());
|
|
}
|
|
|
|
auto card_ids = index.all_card_ids();
|
|
log_info("%zu card definitions", card_ids.size());
|
|
for (uint32_t card_id : card_ids) {
|
|
auto entry = index.definition_for_card_id(card_id);
|
|
string s = entry->def.str();
|
|
string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", ");
|
|
string text = entry->text.empty() ? "(No text available)" : entry->text;
|
|
fprintf(stdout, "%s\nTags: %s\n%s\n\n", s.c_str(), tags.c_str(), text.c_str());
|
|
}
|
|
|
|
} else {
|
|
auto entry = index.definition_for_card_id(ep3_card_id);
|
|
string s = entry->def.str();
|
|
string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", ");
|
|
string text = entry->text.empty() ? "(No text available)" : entry->text;
|
|
fprintf(stdout, "%s\nTags: %s\n%s\n", s.c_str(), tags.c_str(), text.c_str());
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case Behavior::PARSE_OBJECT_GRAPH: {
|
|
string data = read_input_data();
|
|
PSOGCObjectGraph g(data, root_object_address);
|
|
g.print(stdout);
|
|
break;
|
|
}
|
|
|
|
case Behavior::GENERATE_PRODUCT: {
|
|
auto product = generate_product(domain, subdomain);
|
|
fprintf(stdout, "%s\n", product.c_str());
|
|
break;
|
|
}
|
|
|
|
case Behavior::GENERATE_ALL_PRODUCTS: {
|
|
auto products = generate_all_products();
|
|
fprintf(stdout, "%zu (0x%zX) products found\n", products.size(), products.size());
|
|
for (const auto& it : products) {
|
|
fprintf(stdout, "Valid product: %08" PRIX32, it.first);
|
|
for (uint8_t where : it.second) {
|
|
fprintf(stdout, " (domain=%hhu, subdomain=%hhu)",
|
|
static_cast<uint8_t>((where >> 2) & 3),
|
|
static_cast<uint8_t>(where & 3));
|
|
}
|
|
fputc('\n', stdout);
|
|
}
|
|
|
|
atomic<uint64_t> num_valid_products = 0;
|
|
mutex output_lock;
|
|
auto thread_fn = [&](uint64_t product, size_t) -> bool {
|
|
for (uint8_t domain = 0; domain < 3; domain++) {
|
|
for (uint8_t subdomain = 0; subdomain < 3; subdomain++) {
|
|
if (product_is_valid_fast(product, domain, subdomain)) {
|
|
num_valid_products++;
|
|
lock_guard g(output_lock);
|
|
fprintf(stdout, "Valid product: %08" PRIX64 " (domain=%hhu, subdomain=%hhu)\n", product, domain, subdomain);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
auto progress_fn = [&](uint64_t, uint64_t, uint64_t current_value, uint64_t) -> void {
|
|
uint64_t num_found = num_valid_products.load();
|
|
fprintf(stderr, "... %08" PRIX64 " %" PRId64 " (0x%" PRIX64 ") found\r",
|
|
current_value, num_found, num_found);
|
|
};
|
|
parallel_range<uint64_t>(thread_fn, 0, 0x100000000, num_threads, progress_fn);
|
|
break;
|
|
}
|
|
|
|
case Behavior::INSPECT_PRODUCT: {
|
|
if (!input_filename) {
|
|
throw invalid_argument("no product given");
|
|
}
|
|
size_t num_valid_subdomains = 0;
|
|
for (uint8_t domain = 0; domain < 3; domain++) {
|
|
for (uint8_t subdomain = 0; subdomain < 3; subdomain++) {
|
|
if (product_is_valid_fast(input_filename, domain, subdomain)) {
|
|
fprintf(stdout, "%s is valid in domain %hhu subdomain %hhu\n", input_filename, domain, subdomain);
|
|
num_valid_subdomains++;
|
|
}
|
|
}
|
|
}
|
|
if (num_valid_subdomains == 0) {
|
|
fprintf(stdout, "%s is not valid in any domain\n", input_filename);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Behavior::PRODUCT_SPEED_TEST:
|
|
if (seed.empty()) {
|
|
product_speed_test();
|
|
} else {
|
|
product_speed_test(stoul(seed, nullptr, 16));
|
|
}
|
|
break;
|
|
|
|
case Behavior::REPLAY_LOG:
|
|
case Behavior::RUN_SERVER: {
|
|
bool is_replay = behavior == Behavior::REPLAY_LOG;
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
if (isatty(fileno(stderr))) {
|
|
use_terminal_colors = true;
|
|
}
|
|
|
|
if (is_replay) {
|
|
set_function_compiler_available(false);
|
|
}
|
|
|
|
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
|
|
shared_ptr<ServerState> state(new ServerState(config_filename, is_replay));
|
|
|
|
shared_ptr<DNSServer> dns_server;
|
|
if (state->dns_server_port && (behavior != Behavior::REPLAY_LOG)) {
|
|
config_log.info("Starting DNS server");
|
|
dns_server.reset(new DNSServer(base, state->local_address,
|
|
state->external_address));
|
|
dns_server->listen("", state->dns_server_port);
|
|
} else {
|
|
config_log.info("DNS server is disabled");
|
|
}
|
|
|
|
shared_ptr<Shell> shell;
|
|
shared_ptr<ReplaySession> replay_session;
|
|
shared_ptr<IPStackSimulator> ip_stack_simulator;
|
|
if (behavior == Behavior::REPLAY_LOG) {
|
|
config_log.info("Starting proxy server");
|
|
state->proxy_server.reset(new ProxyServer(base, state));
|
|
config_log.info("Starting game server");
|
|
state->game_server.reset(new Server(base, state));
|
|
|
|
shared_ptr<FILE> log_f(
|
|
stdin, +[](FILE*) {});
|
|
if (input_filename && strcmp(input_filename, "-")) {
|
|
log_f = fopen_shared(input_filename, "rt");
|
|
}
|
|
|
|
replay_session.reset(new ReplaySession(
|
|
base, log_f.get(), state, replay_required_access_key, replay_required_password));
|
|
replay_session->start();
|
|
|
|
} else if (behavior == Behavior::RUN_SERVER) {
|
|
config_log.info("Opening sockets");
|
|
for (const auto& it : state->name_to_port_config) {
|
|
const auto& pc = it.second;
|
|
if (pc->behavior == ServerBehavior::PROXY_SERVER) {
|
|
if (!state->proxy_server.get()) {
|
|
config_log.info("Starting proxy server");
|
|
state->proxy_server.reset(new ProxyServer(base, state));
|
|
}
|
|
if (state->proxy_server.get()) {
|
|
// For PC and GC, proxy sessions are dynamically created when a client
|
|
// picks a destination from the menu. For patch and BB clients, there's
|
|
// no way to ask the client which destination they want, so only one
|
|
// destination is supported, and we have to manually specify the
|
|
// destination netloc here.
|
|
if (pc->version == GameVersion::PATCH) {
|
|
auto [ss, size] = make_sockaddr_storage(
|
|
state->proxy_destination_patch.first,
|
|
state->proxy_destination_patch.second);
|
|
state->proxy_server->listen(pc->port, pc->version, &ss);
|
|
} else if (pc->version == GameVersion::BB) {
|
|
auto [ss, size] = make_sockaddr_storage(
|
|
state->proxy_destination_bb.first,
|
|
state->proxy_destination_bb.second);
|
|
state->proxy_server->listen(pc->port, pc->version, &ss);
|
|
} else {
|
|
state->proxy_server->listen(pc->port, pc->version);
|
|
}
|
|
}
|
|
} else {
|
|
if (!state->game_server.get()) {
|
|
config_log.info("Starting game server");
|
|
state->game_server.reset(new Server(base, state));
|
|
}
|
|
string spec = string_printf("T-%hu-%s-%s-%s",
|
|
pc->port, name_for_version(pc->version), pc->name.c_str(),
|
|
name_for_server_behavior(pc->behavior));
|
|
state->game_server->listen(spec, "", pc->port, pc->version, pc->behavior);
|
|
}
|
|
}
|
|
|
|
#ifndef PHOSG_WINDOWS
|
|
if (!state->ip_stack_addresses.empty()) {
|
|
config_log.info("Starting IP stack simulator");
|
|
ip_stack_simulator.reset(new IPStackSimulator(base, state));
|
|
for (const auto& it : state->ip_stack_addresses) {
|
|
auto netloc = parse_netloc(it);
|
|
ip_stack_simulator->listen(netloc.first, netloc.second);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
} else {
|
|
throw logic_error("invalid behavior");
|
|
}
|
|
|
|
if (!state->username.empty()) {
|
|
config_log.info("Switching to user %s", state->username.c_str());
|
|
drop_privileges(state->username);
|
|
}
|
|
|
|
bool should_run_shell;
|
|
if (state->run_shell_behavior == ServerState::RunShellBehavior::DEFAULT) {
|
|
should_run_shell = isatty(fileno(stdin));
|
|
} else if (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS) {
|
|
should_run_shell = true;
|
|
} else {
|
|
should_run_shell = false;
|
|
}
|
|
if (should_run_shell) {
|
|
should_run_shell = !replay_session.get();
|
|
}
|
|
if (should_run_shell) {
|
|
shell.reset(new ServerShell(base, state));
|
|
}
|
|
|
|
config_log.info("Ready");
|
|
event_base_dispatch(base.get());
|
|
|
|
config_log.info("Normal shutdown");
|
|
state->proxy_server.reset(); // Break reference cycle
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw logic_error("invalid behavior");
|
|
}
|
|
|
|
return 0;
|
|
}
|