add quest script compiler
This commit is contained in:
+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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user