add quest script compiler
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
Executable
+345
@@ -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 +0,0 @@
|
||||
q058-gc-e.bin
|
||||
@@ -0,0 +1 @@
|
||||
q058-gc-e.bin.txt
|
||||
Reference in New Issue
Block a user