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
+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());
}