add quest script disassembler

This commit is contained in:
Martin Michelsen
2023-06-25 17:29:58 -07:00
parent fcc43e24c5
commit e1b4bd32c9
9 changed files with 1212 additions and 106 deletions
+1
View File
@@ -88,6 +88,7 @@ add_executable(newserv
src/PSOGCObjectGraph.cc
src/PSOProtocol.cc
src/Quest.cc
src/QuestScript.cc
src/RareItemSet.cc
src/ReceiveCommands.cc
src/ReceiveSubcommands.cc
+1
View File
@@ -394,6 +394,7 @@ newserv has many CLI options, which can be used to access functionality other th
* Run a brute-force search for a decryption seed (`find-decryption-seed`)
* Decode Shift-JIS text to UTF-16 (`decode-sjis`)
* Convert quests in .gci, .vms, .dlq, or .qst format to .bin/.dat format (`decode-gci`, `decode-vms`, `decode-dlq`, `decode-qst`)
* Disassemble quest scripts (`disassemble-bin`)
* Connect to another PSO server and pretend to be a client (`cat-client`)
* Format Episode 3 game data in a human-readable manner (`show-ep3-data`)
* Render a human-readable description of item data (`describe-item`)
+26 -18
View File
@@ -1956,33 +1956,30 @@ struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry<char16_t, 0x7A> {
// probably all other private servers) ignores it.
// Curiously, PSO GC sends uninitialized data in header.flag.
// AA (C->S): Update quest statistics (V3/BB)
// This command is used in Maximum Attack 2, but its format is unlikely to be
// specific to that quest. The structure here represents the only instance I've
// seen so far.
// AA (C->S): Send quest statistic (V3/BB)
// This command is generated when an opcode F92E is executed in a quest.
// The server should respond with an AB command.
// This command is likely never sent by PSO GC Episodes 1&2 Trial Edition,
// because the following command (AB) is definitely not valid on that version.
struct C_UpdateQuestStatistics_V3_BB_AA {
le_uint16_t quest_internal_id = 0;
struct C_SendQuestStatistic_V3_BB_AA {
le_uint16_t stat_id1 = 0;
le_uint16_t unused = 0;
le_uint16_t request_token = 0;
le_uint16_t unknown_a1 = 0;
le_uint32_t unknown_a2 = 0;
le_uint32_t kill_count = 0;
le_uint32_t time_taken = 0; // in seconds
parray<le_uint32_t, 5> unknown_a3;
le_uint16_t function_id1 = 0;
le_uint16_t function_id2 = 0;
parray<le_uint32_t, 8> params;
} __packed__;
// AB (S->C): Confirm update quest statistics (V3/BB)
// AB (S->C): Confirm quest statistic (V3/BB)
// This command is not valid on PSO GC Episodes 1&2 Trial Edition.
// Upon receipt, the client starts a quest thread running the given function.
// Probably this is supposed to be one of the function IDs previously sent in
// the AA command, but the client does not check for this. The server can
// presumably use this command to call any function at any time during a quest.
struct S_ConfirmUpdateQuestStatistics_V3_BB_AB {
le_uint16_t unknown_a1 = 0; // 0
be_uint16_t unknown_a2 = 0; // Probably actually unused
le_uint16_t request_token = 0; // Should match token sent in AA command
le_uint16_t unknown_a3 = 0; // Schtserv always sends 0xBFFF here
struct S_ConfirmQuestStatistic_V3_BB_AB {
le_uint16_t function_id;
parray<uint8_t, 2> unused;
} __packed__;
// AC: Quest barrier (V3/BB)
@@ -4538,6 +4535,7 @@ struct G_EnemyKilled_6x76 {
} __packed__;
// 6x77: Sync quest data
// This is sent by the client when an opcode D9 is executed within a quest.
struct G_SyncQuestData_6x77 {
G_UnusedHeader header;
@@ -5426,6 +5424,7 @@ struct G_Unknown_BB_6xD4 {
} __packed__;
// 6xD5: Exchange item in quest (BB; handled by server)
// The client sends this when it executes an F953 quest opcode.
struct G_ExchangeItemInQuest_BB_6xD5 {
G_ClientIDHeader header;
@@ -5445,6 +5444,7 @@ struct G_WrapItem_BB_6xD6 {
} __packed__;
// 6xD7: Paganini Photon Drop exchange (BB; handled by server)
// The client sends this when it executes an F955 quest opcode.
struct G_PaganiniPhotonDropExchange_BB_6xD7 {
G_ClientIDHeader header;
@@ -5454,6 +5454,7 @@ struct G_PaganiniPhotonDropExchange_BB_6xD7 {
} __packed__;
// 6xD8: Add S-rank weapon special (BB; handled by server)
// The client sends this when it executes an F956 quest opcode.
struct G_AddSRankWeaponSpecial_BB_6xD8 {
G_ClientIDHeader header;
@@ -5465,6 +5466,7 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 {
} __packed__;
// 6xD9: Momoka item exchange (BB; handled by server)
// The client sends this when it executes an F95B quest opcode.
struct G_MomokaItemExchange_BB_6xD9 {
G_ClientIDHeader header;
@@ -5477,6 +5479,7 @@ struct G_MomokaItemExchange_BB_6xD9 {
} __packed__;
// 6xDA: Upgrade weapon attribute (BB; handled by server)
// The client sends this when it executes an F957 or F957 quest opcode.
struct G_UpgradeWeaponAttribute_BB_6xDA {
G_ClientIDHeader header;
@@ -5515,6 +5518,7 @@ struct G_SetEXPMultiplier_BB_6xDD {
} __packed__;
// 6xDE: Good Luck quest (BB; handled by server)
// The client sends this when it executes an F95C quest opcode.
struct G_GoodLuckQuestActions_BB_6xDE {
G_ClientIDHeader header;
@@ -5524,12 +5528,14 @@ struct G_GoodLuckQuestActions_BB_6xDE {
} __packed__;
// 6xDF: Black Paper's Deal Photon Drop exchange (BB; handled by server)
// The client sends this when it executes an F95D quest opcode.
struct G_BlackPaperDealPhotonDropExchange_BB_6xE0 {
G_ClientIDHeader header;
} __packed__;
// 6xE0: Black Paper's Deal rewards (BB; handled by server)
// The client sends this when it executes an F95E quest opcode.
struct G_BlackPaperDealRewards_BB_6xE0 {
G_ClientIDHeader header;
@@ -5537,6 +5543,7 @@ struct G_BlackPaperDealRewards_BB_6xE0 {
} __packed__;
// 6xE1: Gallon's Plan quest (BB; handled by server)
// The client sends this when it executes an F95F quest opcode.
struct G_GallonsPlanQuestActions_BB_6xE1 {
G_ClientIDHeader header;
@@ -5549,6 +5556,7 @@ struct G_GallonsPlanQuestActions_BB_6xE1 {
} __packed__;
// 6xE2: Coren actions (BB)
// The client sends this when it executes an F960 quest opcode.
struct G_CorenActions_BB_6xE2 {
G_ClientIDHeader header;
+51 -21
View File
@@ -24,6 +24,8 @@
#include "PSOGCObjectGraph.hh"
#include "Product.hh"
#include "ProxyServer.hh"
#include "Quest.hh"
#include "QuestScript.hh"
#include "ReplaySession.hh"
#include "SaveFileFormats.hh"
#include "SendCommands.hh"
@@ -163,6 +165,9 @@ The actions are:\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\
disassemble-bin [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Disassemble the input quest script (.bin file) into a text representation\n\
of the commands and metadata it contains.\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\
@@ -219,6 +224,7 @@ enum class Behavior {
FIND_DECRYPTION_SEED,
SALVAGE_GCI,
DECODE_QUEST_FILE,
DISASSEMBLE_QUEST_SCRIPT,
DECODE_SJIS,
EXTRACT_GSL,
EXTRACT_BML,
@@ -251,6 +257,7 @@ static bool behavior_takes_input_filename(Behavior b) {
(b == Behavior::SALVAGE_GCI) ||
(b == Behavior::ENCRYPT_GCI_SAVE) ||
(b == Behavior::DECODE_QUEST_FILE) ||
(b == Behavior::DISASSEMBLE_QUEST_SCRIPT) ||
(b == Behavior::DECODE_SJIS) ||
(b == Behavior::FORMAT_ITEMRT_REL) ||
(b == Behavior::EXTRACT_GSL) ||
@@ -274,22 +281,17 @@ static bool behavior_takes_output_filename(Behavior b) {
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
(b == Behavior::DECRYPT_GCI_SAVE) ||
(b == Behavior::ENCRYPT_GCI_SAVE) ||
(b == Behavior::DISASSEMBLE_QUEST_SCRIPT) ||
(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;
bool is_dcv1 = false;
Quest::FileFormat quest_file_type = Quest::FileFormat::BIN_DAT_GCI;
string seed;
string key_file_name;
const char* config_filename = "system/config.json";
@@ -325,16 +327,25 @@ int main(int argc, char** argv) {
num_threads = strtoull(&argv[x][10], nullptr, 0);
} else if (!strcmp(argv[x], "--patch")) {
cli_version = GameVersion::PATCH;
is_dcv1 = false;
} else if (!strcmp(argv[x], "--dc")) {
cli_version = GameVersion::DC;
is_dcv1 = false;
} else if (!strcmp(argv[x], "--dcv1")) {
cli_version = GameVersion::DC;
is_dcv1 = true;
} else if (!strcmp(argv[x], "--pc")) {
cli_version = GameVersion::PC;
is_dcv1 = false;
} else if (!strcmp(argv[x], "--gc")) {
cli_version = GameVersion::GC;
is_dcv1 = false;
} else if (!strcmp(argv[x], "--xb")) {
cli_version = GameVersion::XB;
is_dcv1 = false;
} else if (!strcmp(argv[x], "--bb")) {
cli_version = GameVersion::BB;
is_dcv1 = false;
} else if (!strncmp(argv[x], "--compression-level=", 20)) {
compression_level = strtoll(&argv[x][20], nullptr, 0);
} else if (!strcmp(argv[x], "--optimal")) {
@@ -422,16 +433,18 @@ int main(int argc, char** argv) {
behavior = Behavior::DECODE_SJIS;
} else if (!strcmp(argv[x], "decode-gci")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::GCI;
quest_file_type = Quest::FileFormat::BIN_DAT_GCI;
} else if (!strcmp(argv[x], "decode-vms")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::VMS;
quest_file_type = Quest::FileFormat::BIN_DAT_VMS;
} else if (!strcmp(argv[x], "decode-dlq")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::DLQ;
quest_file_type = Quest::FileFormat::BIN_DAT_DLQ;
} else if (!strcmp(argv[x], "decode-qst")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::QST;
quest_file_type = Quest::FileFormat::QST;
} else if (!strcmp(argv[x], "disassemble-bin")) {
behavior = Behavior::DISASSEMBLE_QUEST_SCRIPT;
} else if (!strcmp(argv[x], "cat-client")) {
behavior = Behavior::CAT_CLIENT;
} else if (!strcmp(argv[x], "format-itemrt-rel")) {
@@ -488,13 +501,14 @@ int main(int argc, char** argv) {
};
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, "-")) {
// If the output is to a specified file, write it there
save_file(output_filename, data, size);
} else if (!output_filename && input_filename && strcmp(input_filename, "-")) {
// 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") ||
@@ -524,16 +538,21 @@ int main(int argc, char** argv) {
} else {
filename += ".dec";
}
} else if (behavior == Behavior::DISASSEMBLE_QUEST_SCRIPT) {
filename += ".txt";
} 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))) {
} else if (isatty(fileno(stdout)) && (behavior != Behavior::DISASSEMBLE_QUEST_SCRIPT)) {
// If stdout is a terminal and the data is not known to be text, use
// print_data to write the result
print_data(stdout, data, size);
fflush(stdout);
// If stdout is not a terminal, write the data as-is
} else {
// If stdout is not a terminal, write the data as-is
fwritex(stdout, data, size);
fflush(stdout);
}
@@ -945,18 +964,18 @@ int main(int argc, char** argv) {
}
string output_filename_base = input_filename;
if (quest_file_type == QuestFileFormat::GCI) {
if (quest_file_type == Quest::FileFormat::BIN_DAT_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) {
} else if (quest_file_type == Quest::FileFormat::BIN_DAT_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) {
} else if (quest_file_type == Quest::FileFormat::BIN_DAT_DLQ) {
auto decoded = Quest::decode_dlq_file(input_filename);
save_file(output_filename_base + ".dec", decoded);
} else if (quest_file_type == QuestFileFormat::QST) {
} else if (quest_file_type == Quest::FileFormat::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);
@@ -966,6 +985,17 @@ int main(int argc, char** argv) {
break;
}
case Behavior::DISASSEMBLE_QUEST_SCRIPT: {
if (!input_filename || !strcmp(input_filename, "-")) {
throw invalid_argument("an input filename is required");
}
auto data = prs_decompress(read_input_data());
string result = disassemble_quest_script(data.data(), data.size(), cli_version, is_dcv1);
write_output_data(result.data(), result.size());
break;
}
case Behavior::DECODE_SJIS: {
string data = read_input_data();
auto decoded = decode_sjis(data);
+1 -58
View File
@@ -15,6 +15,7 @@
#include "Compression.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "QuestScript.hh"
#include "SaveFileFormats.hh"
#include "Text.hh"
@@ -230,64 +231,6 @@ struct PSODownloadQuestHeader {
le_uint32_t encryption_seed;
} __attribute__((packed));
struct PSOQuestHeaderDC { // Same format for DC v1 and v2, thankfully
uint32_t start_offset;
uint32_t unknown_offset1;
uint32_t size;
uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
uint16_t quest_number; // 0xFFFF for challenge quests
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
} __attribute__((packed));
struct PSOQuestHeaderPC {
uint32_t start_offset;
uint32_t unknown_offset1;
uint32_t size;
uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
uint16_t quest_number; // 0xFFFF for challenge quests
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
// TODO: Is the XB quest header format the same as on GC? If not, make a
// separate struct; if so, rename this struct to V3.
struct PSOQuestHeaderGC {
uint32_t start_offset;
uint32_t unknown_offset1;
uint32_t size;
uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
uint8_t quest_number;
uint8_t episode; // 1 = Ep2. Apparently some quests have 0xFF here, which means ep1 (?)
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
} __attribute__((packed));
struct PSOQuestHeaderBB {
uint32_t start_offset;
uint32_t unknown_offset1;
uint32_t size;
uint32_t unused;
uint16_t quest_number; // 0xFFFF for challenge quests
uint16_t unused2;
uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
uint8_t max_players;
uint8_t joinable_in_progress;
uint8_t unknown;
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> category_index)
: internal_id(-1),
menu_item_id(0),
+1058
View File
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <stdint.h>
#include <phosg/Encoding.hh>
#include "Text.hh"
#include "Version.hh"
struct PSOQuestHeaderDC { // Same format for DC v1 and v2
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
le_uint16_t quest_number; // 0xFFFF for challenge quests
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
} __attribute__((packed));
struct PSOQuestHeaderPC {
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
le_uint16_t quest_number; // 0xFFFF for challenge quests
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
// TODO: Is the XB quest header format the same as on GC? If not, make a
// separate struct; if so, rename this struct to V3.
struct PSOQuestHeaderGC {
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
uint8_t quest_number;
uint8_t episode; // 1 = Ep2. Apparently some quests have 0xFF here, which means ep1 (?)
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
} __attribute__((packed));
struct PSOQuestHeaderBB {
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
le_uint16_t quest_number; // 0xFFFF for challenge quests
le_uint16_t unused2;
uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
uint8_t max_players;
uint8_t joinable_in_progress;
uint8_t unknown;
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
std::string disassemble_quest_script(const void* data, size_t size, GameVersion version, bool is_dcv1);
+5 -8
View File
@@ -2368,23 +2368,20 @@ static void on_AC_V3_BB(shared_ptr<ServerState> s, shared_ptr<Client> c,
static void on_AA(shared_ptr<ServerState> s,
shared_ptr<Client> c, uint16_t, uint32_t, const string& data) {
const auto& cmd = check_size_t<C_UpdateQuestStatistics_V3_BB_AA>(data);
const auto& cmd = check_size_t<C_SendQuestStatistic_V3_BB_AA>(data);
if (c->flags & Client::Flag::IS_TRIAL_EDITION) {
throw runtime_error("trial edition client sent update quest stats command");
}
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game() || !l->quest.get() ||
(l->quest->internal_id != cmd.quest_internal_id)) {
if (!l || !l->is_game() || !l->quest.get()) {
return;
}
S_ConfirmUpdateQuestStatistics_V3_BB_AB response;
response.unknown_a1 = 0x0000;
response.unknown_a2 = 0x0000;
response.request_token = cmd.request_token;
response.unknown_a3 = 0xBFFF;
// TODO: Send the right value here. (When should we send function_id2?)
S_ConfirmQuestStatistic_V3_BB_AB response;
response.function_id = cmd.function_id1;
send_command_t(c, 0xAB, 0x00, response);
}
+1 -1
View File
@@ -78,7 +78,7 @@ struct PSOGCSystemFile {
/* 0008 */ be_uint32_t unknown_a3; // Default 1728000 (== 60 * 60 * 24 * 20)
/* 000C */ be_uint16_t udp_behavior; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled;
/* 0010 */ parray<uint8_t, 0x100> unknown_a6;
/* 0010 */ parray<uint8_t, 0x100> event_flags; // Can be set by quest opcode D8 or E8
/* 0110 */ parray<uint8_t, 8> unknown_a7;
// This timestamp is the number of seconds since 12:00AM on 1 January 2000.
// This field is also used as the round1 seed for encrypting the character and