add quest script disassembler
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user