add quest script compiler

This commit is contained in:
Martin Michelsen
2023-12-10 14:24:30 -08:00
parent b53bde9046
commit 16cddd28b2
10 changed files with 945 additions and 21 deletions
+3 -1
View File
@@ -109,6 +109,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
| Compressed Ep3 | .bin or .mnm | Yes (4) | None (1) |
| Uncompressed | .bind and .datd | Yes | compress-prs (2) |
| Uncompressed Ep3 | .bind or .mnmd | Yes (4) | compress-prs (2) |
| Source | .bin.txt and .dat | Yes | None (5) |
| VMS (DCv1) | .bin.vms and .dat.vms | Yes | decode-vms |
| VMS (DCv2) | .bin.vms and .dat.vms | Decode (3) | decode-vms (3) |
| GCI (decrypted) | .bin.gci and .dat.gci | Yes | decode-gci |
@@ -126,6 +127,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
2. *Similar to (1), to compress an uncompressed quest file: `newserv compress-prs FILENAME.bind FILENAME.bin` (and likewise for .datd -> .dat)*
3. *Use the decode action to convert these quests to .bin/.dat format before putting them into the server's quests directory. If you know the encryption seed (serial number), pass it in as a hex string with the `--seed=` option. If you don't know the encryption seed, newserv will find it for you, which will likely take a long time.*
4. *Episode 3 quests don't go in the system/quests directory. See the Episode 3 section below.*
5. *Quest source can be assembled into a .bin or .bind file with `newserv assemble-quest-script FILENAME.txt`. See system/quests/retrieval/q058-gc-e.bin.txt for an annotated example; this is the English GameCube version of Lost HEAT SWORD.*
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst.
@@ -401,7 +403,7 @@ newserv has many CLI options, which can be used to access functionality other th
* Convert quests in .gci, .vms, .dlq, or .qst format to .bin/.dat format (`decode-gci`, `decode-vms`, `decode-dlq`, `decode-qst`)
* Convert quests in .bin/.dat to .qst format (`encode-qst`)
* Convert text archives (e.g. TextEnglish.pr2) to JSON and vice versa (`decode-text-archive`, `encode-text-archive`)
* Disassemble quest scripts (`disassemble-quest-script`)
* Compile or disassemble quest scripts (`assemble-quest-script`, `disassemble-quest-script`)
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`)
* Convert item data to a human-readable description, or vice versa (`describe-item`, `encode-item`)
* Connect to another PSO server and pretend to be a client (`cat-client`)
+17 -2
View File
@@ -1040,8 +1040,7 @@ Action a_disassemble_quest_map(
"disassemble-quest-map", "\
disassemble-quest-map [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Disassemble the input quest map (.dat file) into a text representation of\n\
the data it contains. Specify the quest\'s game version with one of the\n\
--dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc, --xb, or --bb options.\n",
the data it contains.\n",
+[](Arguments& args) {
string data = read_input_data(args);
if (!args.get<bool>("decompressed")) {
@@ -1051,6 +1050,22 @@ Action a_disassemble_quest_map(
write_output_data(args, result.data(), result.size(), "txt");
});
Action a_assemble_quest_script(
"assemble-quest-script", "\
assemble-quest-script [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Assemble the input quest script (.txt file) into a compressed .bin file\n\
usable as an online quest script. If --decompressed is given, produces an\n\
uncompressed .bind file instead.\n",
+[](Arguments& args) {
string text = read_input_data(args);
string result = assemble_quest_script(text);
bool compress = !args.get<bool>("decompressed");
if (compress) {
result = prs_compress_optimal(result);
}
write_output_data(args, result.data(), result.size(), compress ? "bin" : "bind");
});
void a_extract_archive_fn(Arguments& args) {
string output_prefix = args.get<string>(2, false);
if (output_prefix == "-") {
+1 -1
View File
@@ -1028,7 +1028,7 @@ string Map::disassemble_quest_data(const void* data, size_t size) {
}
}
return join(ret, "\n");
return join(ret, "\n") + "\n";
}
SetDataTable::SetDataTable(shared_ptr<const string> data, bool big_endian) {
+7 -1
View File
@@ -319,7 +319,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
this->joinable = header->joinable_in_progress;
this->joinable = header->joinable;
this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version);
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
@@ -501,6 +501,12 @@ QuestIndex::QuestIndex(
} else if (ends_with(filename, ".dlq")) {
file_data = decode_dlq_data(load_file(file_path));
filename.resize(filename.size() - 4);
} else if (ends_with(filename, ".txt")) {
file_data = assemble_quest_script(load_file(file_path));
filename.resize(filename.size() - 4);
if (ends_with(filename, ".bin")) {
filename.push_back('d');
}
} else {
file_data = load_file(file_path);
}
+569 -14
View File
@@ -493,7 +493,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0x00ED, "create_bgmctrl", {}, F_V1_V4},
{0x00EE, "pl_add_meseta2", {INT32}, F_V1_V4 | F_ARGS},
{0x00EF, "sync_register2", {INT32, REG32}, F_V1_V2},
{0x00EF, "sync_register2", {INT32, INT32}, F_V3_V4 | F_ARGS},
{0x00EF, "sync_register2", {REG, INT32}, F_V3_V4 | F_ARGS},
{0x00F0, "send_regwork", {INT32, REG32}, F_V1_V2},
{0x00F1, "leti_fixed_camera", {{REG32_SET_FIXED, 6}}, F_V2},
{0x00F1, "leti_fixed_camera", {{REG_SET_FIXED, 6}}, F_V3_V4},
@@ -672,7 +672,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF8B5, "write4", {REG, REG}, F_V2},
{0xF8B5, "write4", {INT32, INT32}, F_V3_V4 | F_ARGS},
{0xF8B6, "check_for_hacking", {REG}, F_V2}, // Returns a bitmask of 5 different types of detectable hacking. But it only works on DCv2 - it crashes on all other versions.
{0xF8B7, nullptr, {REG}, F_V2_V4}, // TODO (DX) - Challenge mode. Appears to be timing-related; regA is expected to be in [60, 3600]. Encodes the value with encrypt_challenge_time even though it's never sent over the network and is only decrypted locally.
{0xF8B7, "unknown_F8B7", {REG}, F_V2_V4}, // TODO (DX) - Challenge mode. Appears to be timing-related; regA is expected to be in [60, 3600]. Encodes the value with encrypt_challenge_time even though it's never sent over the network and is only decrypted locally.
{0xF8B8, "disable_retry_menu", {}, F_V2_V4},
{0xF8B9, "chl_recovery", {}, F_V2_V4},
{0xF8BA, "load_guild_card_file_creation_time_to_flag_buf", {}, F_V2_V4},
@@ -735,7 +735,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF8EF, "nop_F8EF", {}, F_V3_V4},
{0xF8F0, "turn_off_bgm_p2", {}, F_V3_V4},
{0xF8F1, "turn_on_bgm_p2", {}, F_V3_V4},
{0xF8F2, nullptr, {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::UNKNOWN_F8F2_DATA}}, F_V3_V4 | F_ARGS}, // TODO (DX)
{0xF8F2, "unknown_F8F2", {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::UNKNOWN_F8F2_DATA}}, F_V3_V4 | F_ARGS}, // TODO (DX)
{0xF8F3, "particle2", {{REG_SET_FIXED, 3}, INT32, FLOAT32}, F_V3_V4 | F_ARGS},
{0xF901, "dec2float", {REG, REG}, F_V3_V4},
{0xF902, "float2dec", {REG, REG}, F_V3_V4},
@@ -817,7 +817,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF94B, "particle_effect_nc", {{REG_SET_FIXED, 4}}, F_V3_V4},
{0xF94C, "player_effect_nc", {{REG_SET_FIXED, 4}}, F_V3_V4},
{0xF94D, "give_or_take_card", {{REG_SET_FIXED, 2}}, F_GC_EP3}, // regsA[0] is card_id; card is given if regsA[1] >= 0, otherwise it's taken
{0xF94D, nullptr, {INT32, REG}, F_XB_V3 | F_ARGS}, // Related to voice chat. argA is a client ID; a value is read from that player's TVoiceChatClient object and (!!value) is placed in regB. This value is set by the 6xB3 command; TODO: figure out what that value represents and name this opcode appropriately
{0xF94D, "unknown_F94D", {INT32, REG}, F_XB_V3 | F_ARGS}, // Related to voice chat. argA is a client ID; a value is read from that player's TVoiceChatClient object and (!!value) is placed in regB. This value is set by the 6xB3 command; TODO: figure out what that value represents and name this opcode appropriately
{0xF94D, "nop_F94D", {}, F_V4},
{0xF94E, "nop_F94E", {}, F_V4},
{0xF94F, "nop_F94F", {}, F_V4},
@@ -864,9 +864,36 @@ opcodes_for_version(Version v) {
return index;
}
static const unordered_map<string, const QuestScriptOpcodeDefinition*>&
opcodes_by_name_for_version(Version v) {
static array<
unordered_map<string, const QuestScriptOpcodeDefinition*>,
static_cast<size_t>(Version::BB_V4) + 1>
indexes;
auto& index = indexes.at(static_cast<size_t>(v));
if (index.empty()) {
uint16_t vf = v_flag(v);
for (size_t z = 0; z < sizeof(opcode_defs) / sizeof(opcode_defs[0]); z++) {
const auto& def = opcode_defs[z];
if (!(def.flags & vf)) {
continue;
}
if (!def.name) {
continue;
}
if (!index.emplace(def.name, &def).second) {
throw logic_error(string_printf("duplicate definition for opcode %04hX", def.opcode));
}
}
}
return index;
}
std::string disassemble_quest_script(const void* data, size_t size, Version version, uint8_t language) {
StringReader r(data, size);
deque<string> lines;
lines.emplace_back(string_printf(".version %s", name_for_enum(version)));
bool use_wstrs = false;
size_t code_offset = 0;
@@ -938,8 +965,8 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
lines.emplace_back(string_printf(".quest_num %hu", header.quest_number.load()));
lines.emplace_back(string_printf(".episode %hhu", header.episode));
lines.emplace_back(string_printf(".max_players %hhu", header.episode));
if (header.joinable_in_progress) {
lines.emplace_back(".joinable_in_progress");
if (header.joinable) {
lines.emplace_back(".joinable");
}
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
@@ -1091,7 +1118,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
}
uint8_t num_functions = cmd_r.get_u8();
for (size_t z = 0; z < num_functions; z++) {
dasm_arg += (dasm_arg.empty() ? "(" : ", ");
dasm_arg += (dasm_arg.empty() ? "[" : ", ");
uint32_t label_id = cmd_r.get_u16l();
if (label_id >= function_table.size()) {
dasm_arg += string_printf("function%04" PRIX32 " /* invalid */", label_id);
@@ -1106,9 +1133,9 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
}
}
if (dasm_arg.empty()) {
dasm_arg = "()";
dasm_arg = "[]";
} else {
dasm_arg += ")";
dasm_arg += "]";
}
break;
}
@@ -1126,12 +1153,12 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
}
uint8_t num_regs = cmd_r.get_u8();
for (size_t z = 0; z < num_regs; z++) {
dasm_arg += string_printf("%sr%hhu", (dasm_arg.empty() ? "(" : ", "), cmd_r.get_u8());
dasm_arg += string_printf("%sr%hhu", (dasm_arg.empty() ? "[" : ", "), cmd_r.get_u8());
}
if (dasm_arg.empty()) {
dasm_arg = "()";
dasm_arg = "[]";
} else {
dasm_arg += ")";
dasm_arg += "]";
}
break;
}
@@ -1293,7 +1320,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
case Arg::Type::FLOAT32:
switch (arg_value.type) {
case ArgStackValue::Type::REG:
dasm_arg = string_printf("(float)r%" PRIu32, arg_value.as_int);
dasm_arg = string_printf("f%" PRIu32, arg_value.as_int);
break;
case ArgStackValue::Type::INT:
dasm_arg = string_printf("%g", as_type<float>(arg_value.as_int));
@@ -1598,7 +1625,7 @@ Episode find_quest_episode_from_script(const void* data, size_t size, Version ve
}
if (def == nullptr) {
throw runtime_error("unknown quest opcode");
throw runtime_error(string_printf("unknown quest opcode %04hX", opcode));
}
if (def->flags & F_RET) {
@@ -1697,3 +1724,531 @@ Episode episode_for_quest_episode_number(uint8_t episode_number) {
throw runtime_error(string_printf("invalid episode number %02hhX", episode_number));
}
}
std::string assemble_quest_script(const std::string& text) {
auto lines = split(text, '\n');
// Strip comments and whitespace
for (auto& line : lines) {
size_t comment_start = line.find("/*");
while (comment_start != string::npos) {
size_t comment_end = line.find("*/", comment_start + 2);
if (comment_end == string::npos) {
throw runtime_error("unterminated inline comment");
}
line.erase(comment_start, comment_end + 2 - comment_start);
comment_start = line.find("/*");
}
comment_start = line.find("//");
if (comment_start != string::npos) {
line.resize(comment_start);
}
strip_trailing_whitespace(line);
strip_leading_whitespace(line);
}
// Collect metadata directives
Version quest_version = Version::UNKNOWN;
string quest_name;
string quest_short_desc;
string quest_long_desc;
int64_t quest_num = -1;
uint8_t quest_language = 1;
Episode quest_episode = Episode::EP1;
uint8_t quest_max_players = 4;
bool quest_joinable = false;
for (const auto& line : lines) {
if (line.empty()) {
continue;
}
if (line[0] == '.') {
if (starts_with(line, ".version ")) {
string name = line.substr(9);
quest_version = enum_for_name<Version>(name.c_str());
} else if (starts_with(line, ".name ")) {
quest_name = parse_data_string(line.substr(6));
} else if (starts_with(line, ".short_desc ")) {
quest_short_desc = parse_data_string(line.substr(12));
} else if (starts_with(line, ".long_desc ")) {
quest_long_desc = parse_data_string(line.substr(11));
} else if (starts_with(line, ".quest_num ")) {
quest_num = stoul(line.substr(11), nullptr, 0);
} else if (starts_with(line, ".language ")) {
quest_language = stoul(line.substr(10), nullptr, 0);
} else if (starts_with(line, ".episode ")) {
quest_episode = episode_for_token_name(line.substr(9));
} else if (starts_with(line, ".max_players ")) {
quest_max_players = stoul(line.substr(12), nullptr, 0);
} else if (starts_with(line, ".joinable ")) {
quest_joinable = true;
}
}
}
if (quest_version == Version::PC_PATCH || quest_version == Version::BB_PATCH || quest_version == Version::UNKNOWN) {
throw runtime_error(".version directive is missing or invalid");
}
if (quest_num < 0) {
throw runtime_error(".quest_num directive is missing or invalid");
}
if (quest_name.empty()) {
throw runtime_error(".name directive is missing or invalid");
}
// Find all label names
struct Label {
std::string name;
ssize_t index = -1;
ssize_t offset = -1;
};
unordered_map<string, shared_ptr<Label>> labels_by_name;
map<ssize_t, shared_ptr<Label>> labels_by_index;
for (size_t line_num = 1; line_num <= lines.size(); line_num++) {
const auto& line = lines[line_num - 1];
if (ends_with(line, ":")) {
auto label = make_shared<Label>();
label->name = line.substr(0, line.size() - 1);
size_t at_offset = label->name.find('@');
if (at_offset != string::npos) {
label->index = stoul(label->name.substr(at_offset + 1), nullptr, 0);
label->name.resize(at_offset);
if (label->name == "start" && label->index != 0) {
throw runtime_error("start label cannot have a nonzero label ID");
}
} else if (label->name == "start") {
label->index = 0;
}
if (!labels_by_name.emplace(label->name, label).second) {
throw runtime_error(string_printf("(line %zu) duplicate label name: %s", line_num, label->name.c_str()));
}
if (label->index >= 0) {
auto index_emplace_ret = labels_by_index.emplace(label->index, label);
if (label->index >= 0 && !index_emplace_ret.second) {
throw runtime_error(string_printf("(line %zu) duplicate label index: %zd (0x%zX) from %s and %s", line_num, label->index, label->index, label->name.c_str(), index_emplace_ret.first->second->name.c_str()));
}
}
}
}
if (!labels_by_name.count("start")) {
throw runtime_error("start label is not defined");
}
// Assign indexes to labels without explicit indexes
{
size_t next_index = 0;
for (auto& it : labels_by_name) {
if (it.second->index >= 0) {
continue;
}
while (labels_by_index.count(next_index)) {
next_index++;
}
it.second->index = next_index++;
labels_by_index.emplace(it.second->index, it.second);
}
}
// Assemble code segment
const auto& opcodes = opcodes_by_name_for_version(quest_version);
StringWriter code_w;
for (size_t line_num = 1; line_num <= lines.size(); line_num++) {
try {
const auto& line = lines[line_num - 1];
if (line.empty()) {
continue;
}
if (ends_with(line, ":")) {
size_t at_offset = line.find('@');
string label_name = line.substr(0, (at_offset == string::npos) ? (line.size() - 1) : at_offset);
labels_by_name.at(label_name)->offset = code_w.size();
continue;
}
if (line[0] == '.') {
if (starts_with(line, ".data ")) {
code_w.write(parse_data_string(line.substr(6)));
} else if (starts_with(line, ".zero ")) {
size_t size = stoull(line.substr(6), nullptr, 0);
code_w.extend_by(size, 0x00);
}
continue;
}
auto line_tokens = split(line, ' ', 1);
const auto& opcode_def = opcodes.at(line_tokens.at(0));
if (!(opcode_def->flags & F_ARGS)) {
if ((opcode_def->opcode & 0xFF00) == 0x0000) {
code_w.put_u8(opcode_def->opcode);
} else {
code_w.put_u16b(opcode_def->opcode);
}
}
if (opcode_def->args.empty()) {
if (line_tokens.size() > 1) {
throw runtime_error(string_printf("(line %zu) arguments not allowed for %s", line_num, opcode_def->name));
}
continue;
}
if (line_tokens.size() < 2) {
throw runtime_error(string_printf("(line %zu) arguments required for %s", line_num, opcode_def->name));
}
auto args = split_context(line_tokens[1], ',');
if (args.size() != opcode_def->args.size()) {
throw runtime_error(string_printf("(line %zu) incorrect argument count for %s", line_num, opcode_def->name));
}
for (size_t z = 0; z < args.size(); z++) {
using Type = QuestScriptOpcodeDefinition::Argument::Type;
string& arg = args[z];
const auto& arg_def = opcode_def->args[z];
strip_trailing_whitespace(arg);
strip_leading_whitespace(arg);
try {
auto parse_reg = +[](const string& name) -> uint8_t {
if ((name[0] != 'r') && (name[0] != 'f')) {
throw runtime_error("a register is required");
}
size_t reg_num = stoull(name.substr(1), nullptr, 0);
if (reg_num > 0xFF) {
throw runtime_error("invalid register number");
}
return reg_num;
};
auto add_cstr = [&](const string& text) -> void {
switch (quest_version) {
case Version::DC_NTE:
code_w.write(tt_utf8_to_sjis(text));
code_w.put_u8(0);
break;
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2:
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
case Version::XB_V3:
code_w.write(quest_language ? tt_utf8_to_8859(text) : tt_utf8_to_sjis(text));
code_w.put_u8(0);
break;
case Version::PC_V2:
case Version::BB_V4:
code_w.write(tt_utf8_to_utf16(text));
code_w.put_u16(0);
break;
default:
throw logic_error("invalid game version");
}
};
if (opcode_def->flags & F_ARGS) {
auto label_it = labels_by_name.find(arg);
if (starts_with(line_tokens[1], "...")) {
// Args were specified by preceding arg_push calls; nothing to do here
} else if (arg.empty()) {
throw runtime_error("argument is empty");
} else if (label_it != labels_by_name.end()) {
code_w.put_u8(0x4B); // arg_pushw
code_w.put_u16l(label_it->second->index);
} else if ((arg[0] == 'r') || (arg[0] == 'f')) {
// If the corresponding argument is a REG or REG_SET_FIXED, push
// the register number, not the register's value, since it's an
// out-param
if ((arg_def.type == Type::REG) || (arg_def.type == Type::REG32)) {
code_w.put_u8(0x4A); // arg_pushb
code_w.put_u8(parse_reg(arg));
} else if (
(arg_def.type == Type::REG_SET_FIXED) ||
(arg_def.type == Type::REG32_SET_FIXED)) {
auto tokens = split(arg, '-');
uint8_t start_reg;
if (tokens.size() == 1) {
start_reg = parse_reg(tokens[0]);
} else if (tokens.size() == 2) {
start_reg = parse_reg(tokens[0]);
if ((parse_reg(tokens[1]) - start_reg + 1) != arg_def.count) {
throw runtime_error("incorrect number of registers used");
}
} else {
throw runtime_error("invalid fixed register set syntax");
}
code_w.put_u8(0x4A); // arg_pushb
code_w.put_u8(start_reg);
} else {
code_w.put_u8(0x48); // arg_pushr
code_w.put_u8(parse_reg(arg));
}
} else if ((arg[0] == '@') && ((arg[1] == 'r') || (arg[1] == 'f'))) {
code_w.put_u8(0x4C); // arg_pusha
code_w.put_u8(parse_reg(arg.substr(1)));
} else if ((arg[0] == '@') && labels_by_name.count(arg.substr(1))) {
code_w.put_u8(0x4D); // arg_pusho
code_w.put_u16(labels_by_name.at(arg.substr(1))->index);
} else {
bool write_as_str = false;
try {
size_t end_offset;
uint64_t value = stoll(arg, &end_offset, 0);
if (end_offset != arg.size()) {
write_as_str = true;
} else if (value > 0xFFFF) {
code_w.put_u8(0x49); // arg_pushl
code_w.put_u32l(value);
} else if (value > 0xFF) {
code_w.put_u8(0x4B); // arg_pushw
code_w.put_u16l(value);
} else {
code_w.put_u8(0x4A); // arg_pushb
code_w.put_u8(value);
}
} catch (const exception&) {
write_as_str = true;
}
if (write_as_str) {
if (arg[0] == '\"') {
code_w.put_u8(0x4E); // arg_pushs
add_cstr(parse_data_string(arg));
} else {
throw runtime_error("invalid argument syntax");
}
}
}
} else { // Not F_ARGS
auto add_label = [&](const string& name, bool is32) -> void {
if (!labels_by_name.count(name)) {
throw runtime_error("label not defined: " + name);
}
if (is32) {
code_w.put_u32(labels_by_name.at(name)->index);
} else {
code_w.put_u16(labels_by_name.at(name)->index);
}
};
auto add_reg = [&](const string& name, bool is32) -> void {
if (is32) {
code_w.put_u32l(parse_reg(name));
} else {
code_w.put_u8(parse_reg(name));
}
};
auto split_set = [&](const string& text) -> vector<string> {
if (!starts_with(text, "[") || !ends_with(text, "]")) {
throw runtime_error("incorrect syntax for set-valued argument");
}
auto values = split(text.substr(1, text.size() - 2), ',');
if (values.size() > 0xFF) {
throw runtime_error("too many labels in set-valued argument");
}
return values;
};
switch (arg_def.type) {
case Type::LABEL16:
case Type::LABEL32:
add_label(arg, arg_def.type == Type::LABEL32);
break;
case Type::LABEL16_SET: {
auto label_names = split_set(arg);
code_w.put_u8(label_names.size());
for (auto name : label_names) {
strip_trailing_whitespace(name);
strip_leading_whitespace(name);
add_label(name, false);
}
break;
}
case Type::REG:
case Type::REG32:
add_reg(arg, arg_def.type == Type::REG32);
break;
case Type::REG_SET_FIXED:
case Type::REG32_SET_FIXED: {
auto tokens = split(arg, '-');
if (tokens.size() == 1) {
add_reg(tokens[0], arg_def.type == Type::REG32_SET_FIXED);
} else if (tokens.size() == 2) {
if ((parse_reg(tokens[1]) - parse_reg(tokens[0]) + 1) != arg_def.count) {
throw runtime_error("incorrect number of registers used");
}
add_reg(tokens[0], arg_def.type == Type::REG32_SET_FIXED);
} else {
throw runtime_error("invalid fixed register set syntax");
}
break;
}
case Type::REG_SET: {
auto regs = split_set(arg);
code_w.put_u8(regs.size());
for (auto reg : regs) {
strip_trailing_whitespace(reg);
strip_leading_whitespace(reg);
add_reg(reg, false);
}
break;
}
case Type::INT8:
code_w.put_u8(stol(arg, nullptr, 0));
break;
case Type::INT16:
code_w.put_u16l(stol(arg, nullptr, 0));
break;
case Type::INT32:
code_w.put_u32l(stol(arg, nullptr, 0));
break;
case Type::FLOAT32:
code_w.put_u32l(stof(arg, nullptr));
break;
case Type::CSTRING:
add_cstr(parse_data_string(arg));
break;
default:
throw logic_error("unknown argument type");
}
}
} catch (const exception& e) {
throw runtime_error(string_printf("(arg %zu) %s", z + 1, e.what()));
}
}
if (opcode_def->flags & F_ARGS) {
if ((opcode_def->opcode & 0xFF00) == 0x0000) {
code_w.put_u8(opcode_def->opcode);
} else {
code_w.put_u16b(opcode_def->opcode);
}
}
} catch (const exception& e) {
throw runtime_error(string_printf("(line %zu) %s", line_num, e.what()));
}
}
while (code_w.size() & 3) {
code_w.put_u8(0);
}
// Generate function table
ssize_t function_table_size = labels_by_index.rbegin()->first + 1;
vector<le_uint32_t> function_table;
function_table.reserve(function_table_size);
{
auto it = labels_by_index.begin();
for (ssize_t z = 0; z < function_table_size; z++) {
if (it == labels_by_index.end()) {
throw logic_error("function table size exceeds maximum function ID");
} else if (it->first > z) {
function_table.emplace_back(0xFFFFFFFF);
} else if (it->first == z) {
if (it->second->offset < 0) {
throw runtime_error("label " + it->second->name + " does not have a valid offset");
}
function_table.emplace_back(it->second->offset);
it++;
} else if (it->first < z) {
throw logic_error("missed label " + it->second->name + " when compiling function table");
}
}
}
// Generate header
StringWriter w;
switch (quest_version) {
case Version::DC_NTE: {
PSOQuestHeaderDCNTE header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.name.encode(quest_name, 0);
w.put(header);
break;
}
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2: {
PSOQuestHeaderDC header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.language = quest_language;
header.unknown1 = 0;
header.quest_number = quest_num;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
case Version::PC_V2: {
PSOQuestHeaderPC header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.language = quest_language;
header.unknown1 = 0;
header.quest_number = quest_num;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
case Version::XB_V3: {
PSOQuestHeaderGC header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.language = quest_language;
header.unknown1 = 0;
header.quest_number = quest_num;
header.episode = (quest_episode == Episode::EP2) ? 1 : 0;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
case Version::BB_V4: {
PSOQuestHeaderBB header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.quest_number = quest_num;
header.unused2 = 0;
if (quest_episode == Episode::EP4) {
header.episode = 2;
} else if (quest_episode == Episode::EP2) {
header.episode = 1;
} else {
header.episode = 0;
}
header.max_players = quest_max_players;
header.joinable = quest_joinable ? 1 : 0;
header.unknown = 0;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
default:
throw logic_error("invalid quest version");
}
w.write(code_w.str());
w.write(function_table.data(), function_table.size() * sizeof(function_table[0]));
return std::move(w.str());
}
+2 -1
View File
@@ -72,7 +72,7 @@ struct PSOQuestHeaderBB {
/* 0012 */ le_uint16_t unused2;
/* 0014 */ uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
/* 0015 */ uint8_t max_players;
/* 0016 */ uint8_t joinable_in_progress;
/* 0016 */ uint8_t joinable;
/* 0017 */ uint8_t unknown;
/* 0018 */ pstring<TextEncoding::UTF16, 0x20> name;
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
@@ -83,5 +83,6 @@ struct PSOQuestHeaderBB {
Episode episode_for_quest_episode_number(uint8_t episode_number);
std::string disassemble_quest_script(const void* data, size_t size, Version version, uint8_t language);
std::string assemble_quest_script(const std::string& text);
Episode find_quest_episode_from_script(const void* data, size_t size, Version version);
Binary file not shown.
+345
View File
@@ -0,0 +1,345 @@
// This file documents newserv's quest assembler syntax and format.
// The metadata directives specify the quest's name, description, and other
// information.
// The .version directive specifies which version of the game the quest is for.
// The values are DC_NTE, DC_V1_11_2000_PROTOTYPE, DC_V1, DC_V2, PC_V2, GC_NTE,
// GC_V3, GC_EP3_TRIAL_EDITION, GC_EP3, XB_V3, and BB_V4.
.version GC_V3
// The .quest_num directive specifies the internal number of the quest. This
// has no meaning for online quests, but for download quests, the game
// deduplicates quest files with the same number, so download quests should all
// have unique numbers in this field. On Episodes 1&2, this field must be in
// the range 0-255; onother versions, it can be 0-65535, but generally numbers
// less than 1000 are recommended.
.quest_num 58
// The .language field specifies the internal language of the quest. Values:
// 0 = Japanese
// 1 = English
// 2 = German
// 3 = French
// 4 = Spanish
// 5 = Chinese (simplified)
// 6 = Chinese (traditional)
// 7 = Korean
.language 1
// The .episode directive specifies the quest's episode. The server ignores this
// if a set_episode or set_episode2 opcode is present in the start function.
.episode Episode1
// These directives specify the quest's name, short description, and long
// description. Non-ASCII characters can be used here and in the script below;
// this entire file is encoded as UTF-8 and strings are transcoded to the
// encoding the client expects based on the .version directive.
.name "Lost HEAT SWORD"
.short_desc "Retrieve a\nweapon from\na Dragon!"
.long_desc "Client: Hopkins, hunter\nQuest:\n My weapon was taken\n from me when I was\n fighting a Dragon.\nReward: ??? Meseta\n\n\n"
// The quest script begins here. A quest script is a sequence of opcodes, and
// labels denoting positions within that sequence that can be jumped to or
// called like a function. All labels have names, and some have numbers. (In the
// compiled format, labels have only numbers and no names; during compilation,
// each label that doesn't have a number is assigned a number that isn't in use
// by another label.) To explicitly specify a label number (for example, if an
// object or NPC refers to a label by number), use an @ sign followed by the
// desired number. Note that numbers can be specified in decimal or hexadecimal;
// see on_talk_to_npc1 and on_talk_to_npc2 for examples.
// Every quest must have a start function; this is the main thread that starts
// when the quest begins. The start label implicitly has a number of zero.
start:
gget 0x0091, r252
set_floor_handler 0, floor_handler_pioneer_2
set_floor_handler 1, floor_handler_forest_1
set_floor_handler 2, floor_handler_forest_2
set_floor_handler 11, floor_handler_dragon
set_qt_success on_quest_success
get_difficulty_level_v2 r83
leti r60, 0 // Pioneer 2
leti r61, 0
leti r62, 0
leti r63, 0
map_designate r60-r63
leti r60, 1 // Forest 1
leti r61, 0
leti r62, 0
leti r63, 0
map_designate r60-r63
leti r60, 2 // Forest 2
leti r61, 0
leti r62, 0
leti r63, 0
map_designate r60-r63
leti r60, 11 // Dragon
leti r61, 0
leti r62, 0
leti r63, 0
map_designate r60-r63
ret
return_immediately:
ret
floor_handler_pioneer_2:
switch_jmp r0, [floor_handler_pioneer_2_first_time, floor_handler_pioneer_2_not_first_time]
floor_handler_pioneer_2_first_time:
set r50
set_mainwarp 1
leti r60, 0x000000ED
leti r61, 0x00000000
leti r62, 0x0000014D
leti r63, 0xFFFFFFF1
p_setpos 0, r60-r63
leti r60, 0x000000FF
leti r61, 0x00000000
leti r62, 0x00000152
leti r63, 0xFFFFFFD5
p_setpos 1, r60-r63
leti r60, 0x000000DE
leti r61, 0x00000000
leti r62, 0x00000142
leti r63, 0x00000019
p_setpos 2, r60-r63
leti r60, 0x000000F8
leti r61, 0x00000000
leti r62, 0x00000143
leti r63, 0xFFFFFFEC
p_setpos 3, r60-r63
call on_talk_to_hopkins
ret
floor_handler_pioneer_2_not_first_time:
set r50
thread_stg watch_for_dragon_killed_in_forest
thread_stg show_mission_complete_if_needed
ret
label00CA:
clear r50
set r51
ret
label00CB:
clear r51
set r50
ret
on_quest_success:
jmpi_eq r83, 0, on_quest_success_normal
jmpi_eq r83, 1, on_quest_success_hard
jmpi_eq r83, 2, on_quest_success_very_hard
jmpi_eq r83, 3, on_quest_success_ultimate
on_quest_success_normal:
window_msg "You\'ve been awarded\n100 Meseta."
bgm 1
winend
pl_add_meseta2 100
ret
on_quest_success_hard:
window_msg "You\'ve been awarded\n5000 Meseta."
bgm 1
winend
pl_add_meseta2 5000
ret
on_quest_success_very_hard:
window_msg "You\'ve been awarded\n10000 Meseta."
bgm 1
winend
pl_add_meseta2 10000
ret
on_quest_success_ultimate:
window_msg "You\'ve been awarded\n15000 Meseta."
bgm 1
winend
pl_add_meseta2 15000
ret
floor_handler_forest_1:
thread_stg watch_for_dragon_killed_in_forest
thread_stg show_mission_complete_if_needed
ret
floor_handler_forest_2:
thread_stg watch_for_dragon_killed_in_forest
thread_stg show_mission_complete_if_needed
ret
floor_handler_dragon:
thread_stg watch_for_dragon_zone_clear
thread_stg play_dragon_killed_cutscene_when_ready
thread_stg show_mission_complete_if_needed
ret
watch_for_dragon_zone_clear:
jmpi_eq r15, 1, return_immediately
sync
leti r240, 11
leti r241, 1
if_zone_clear r242, r240-r241
jmpi_eq r242, 0, watch_for_dragon_zone_clear
sync_register2 r15, 1
ret
watch_for_dragon_killed_in_forest:
jmpi_eq r18, 1, return_immediately
jmpi_eq r19, 1, return_immediately
watch_for_dragon_killed_in_forest_check_again:
sync
jmpi_eq r15, 0, watch_for_dragon_killed_in_forest_check_again
call wait_30_frames
p_action_disable
call on_dragon_killed
set r18
p_action_enable
ret
play_dragon_killed_cutscene_when_ready:
jmpi_eq r18, 1, return_immediately
jmpi_eq r19, 1, return_immediately
play_dragon_killed_cutscene_when_ready_check_again:
sync
jmpi_eq r15, 0, play_dragon_killed_cutscene_when_ready_check_again
call wait_30_frames
call start_cutscene
set r19
set_quest_board_handler 0, quest_board_item_handler, "HEAT SWORD"
set r74
set r254
call on_dragon_killed
call end_cutscene
ret
on_dragon_killed:
jmpi_eq r83, 3, on_dragon_killed_ultimate
window_msg "Dragon killed!"
add_msg "Hopkins\'s HEAT SWORD found\nin Dragon\'s mouth!"
se 1
winend
ret
on_dragon_killed_ultimate:
window_msg "Sil Dragon killed!"
add_msg "Hopkins\'s HEAT SWORD found\nin Sil Dragon\'s mouth!"
se 1
winend
ret
quest_board_item_handler:
disp_msg_qb "My dad gave HEAT SWORD\nto me."
close_msg_qb
ret
show_mission_complete_if_needed:
jmpi_eq r20, 1, return_immediately
show_mission_complete_if_needed_check_again:
sync
jmpi_eq r16, 0, show_mission_complete_if_needed_check_again
jmpi_eq r17, 1, return_immediately
p_action_disable
window_msg "Mission complete!"
bgm 0
add_msg "You will be taken to the lobby\nafter you receive your reward."
set r20
winend
clear_quest_board_handler 0
clear r74
p_action_enable
playbgm_epi 1
ret
on_talk_to_hopkins@310:
jmpi_eq r255, 1, on_talk_to_hopkins_complete_again
jmpi_eq r254, 1, on_talk_to_hopkins_complete
jmpi_eq r0, 1, on_talk_to_hopkins_incomplete_again
call start_cutscene
call wait_30_frames
message 100, "Ca... can you help\nme? Please?"
add_msg "I almost killed a\nDragon."
add_msg "But... but..."
add_msg "My HEAT SWORD...\nMy dad gave HEAT SWORD\nto me..."
add_msg "It\'s really important to\nme. I don\'t know how it\nwas taken from me."
add_msg "I cannot do my job\nwithout it! Please\nget it back for me."
set r0
mesend
bgm 1
call end_cutscene
ret
on_talk_to_hopkins_incomplete_again:
message 100, "Please get my\nHEAT SWORD\nback to me."
add_msg "Perhaps it\'s in the\nDragon\'s stomach!"
mesend
ret
on_talk_to_hopkins_complete:
call start_cutscene
message 100, "My item!\nThis is great! Wow!"
add_msg "Thank you very much!\nYou\'re great!"
add_msg "Please get your reward\nat the counter!"
add_msg "This is all I can do to\nshow my appreciation."
mesend
set r17
sync_register2 r16, 1
sync_register2 r255, 1
clear_quest_board_handler 0
clear r74
bgm 0
playbgm_epi 1
call end_cutscene
ret
on_talk_to_hopkins_complete_again:
message 100, "Please go get your\nreward at the counter!\nThank you!"
mesend
ret
on_talk_to_npc1@320:
jmpi_eq r254, 1, on_talk_to_npc1_complete
message 104, "Did Hopkins do it again?"
add_msg "Nothing. Forget about\nit."
mesend
ret
on_talk_to_npc1_complete:
message 104, "Well, Hopkins often\nloses his..."
mesend
ret
on_talk_to_npc2@0x14A:
jmpi_eq r254, 1, on_talk_to_npc2_complete
message 103, "It\'s kind of his\n\"talent,\" I think."
mesend
ret
on_talk_to_npc2_complete:
message 103, "Thanks for taking care\nof Hopkins."
mesend
ret
start_cutscene:
p_action_disable
disable_movement1 0
disable_movement1 1
disable_movement1 2
disable_movement1 3
hud_hide
cine_enable
cam_zmin
ret
end_cutscene:
cam_zmout
cine_disable
hud_show
enable_movement1 0
enable_movement1 1
enable_movement1 2
enable_movement1 3
p_action_enable
ret
wait_30_frames:
leti r72, 0
wait_30_frames_next_frame:
sync
addi r72, 1
jmpi_le r72, 30, wait_30_frames_next_frame
ret
-1
View File
@@ -1 +0,0 @@
q058-gc-e.bin
+1
View File
@@ -0,0 +1 @@
q058-gc-e.bin.txt