add named registers in quest assembler

This commit is contained in:
Martin Michelsen
2024-06-04 21:14:54 -07:00
parent 3ac421cf55
commit d178d062a8
4 changed files with 395 additions and 105 deletions
+298 -46
View File
@@ -1775,6 +1775,186 @@ Episode episode_for_quest_episode_number(uint8_t episode_number) {
}
}
struct RegisterAssigner {
struct Register {
string name;
int16_t number = -1; // -1 = unassigned (any number)
shared_ptr<Register> prev;
shared_ptr<Register> next;
unordered_set<size_t> offsets;
};
~RegisterAssigner() {
for (auto& it : this->named_regs) {
it.second->prev.reset();
it.second->next.reset();
}
for (auto& reg : this->numbered_regs) {
if (reg) {
reg->prev.reset();
reg->next.reset();
}
}
}
shared_ptr<Register> get_or_create(const string& name, int16_t number) {
shared_ptr<Register> reg;
try {
if (!name.empty()) {
reg = this->named_regs.at(name);
} else if (number >= 0 && number < 0x100) {
reg = this->numbered_regs.at(number);
} else {
throw runtime_error("invalid register name or number");
}
} catch (const out_of_range&) {
}
if (!reg) {
reg = make_shared<Register>();
}
if (number >= 0) {
if (reg->number < 0) {
reg->number = number;
if (this->numbered_regs.at(reg->number)) {
throw runtime_error(string_printf("number %hd is already assigned to a different register", reg->number));
}
this->numbered_regs.at(reg->number) = reg;
} else if (reg->number != number) {
throw runtime_error(string_printf("register %s is assigned multiple numbers", reg->name.c_str()));
}
}
if (!name.empty()) {
if (reg->name.empty()) {
reg->name = name;
if (!this->named_regs.emplace(reg->name, reg).second) {
throw runtime_error(string_printf("name %s is already assigned to a different register", reg->name.c_str()));
}
} else if (reg->name != name) {
throw runtime_error(string_printf("register %hd is assigned multiple names", reg->number));
}
}
return reg;
}
void assign_number(shared_ptr<Register> reg, uint8_t number) {
if (reg->number < 0) {
reg->number = number;
if (this->numbered_regs.at(reg->number)) {
throw logic_error(string_printf("register number %hd assigned multiple times", reg->number));
}
this->numbered_regs.at(reg->number) = reg;
} else if (reg->number != static_cast<int16_t>(number)) {
throw runtime_error(string_printf("assigning different register number %hhu over existing register number %hd", number, reg->number));
}
}
void constrain(shared_ptr<Register> first_reg, shared_ptr<Register> second_reg) {
if (!first_reg->next) {
first_reg->next = second_reg;
} else if (first_reg->next != second_reg) {
throw runtime_error(string_printf("register %s must come after %s, but is already constrained to another register", second_reg->name.c_str(), first_reg->name.c_str()));
}
if (!second_reg->prev) {
second_reg->prev = first_reg;
} else if (second_reg->prev != first_reg) {
throw runtime_error(string_printf("register %s must come before %s, but is already constrained to another register", first_reg->name.c_str(), second_reg->name.c_str()));
}
if ((first_reg->number >= 0) && (second_reg->number >= 0) && (first_reg->number != ((second_reg->number - 1) & 0xFF))) {
throw runtime_error(string_printf("register %s must come before %s, but both registers already have non-consecutive numbers", first_reg->name.c_str(), second_reg->name.c_str()));
}
}
void assign_all() {
// TODO: Technically, we should assign the biggest blocks first to minimize
// fragmentation. I am lazy and haven't implemented this yet.
unordered_set<shared_ptr<Register>> unassigned;
for (auto it : this->named_regs) {
if (it.second->number < 0) {
unassigned.emplace(it.second);
}
}
while (!unassigned.empty()) {
auto reg_it = unassigned.begin();
auto reg = *reg_it;
unassigned.erase(reg_it);
// If this register is already assigned, skip it
if (reg->number >= 0) {
continue;
}
// If any next register is assigned, assign this register
size_t next_delta = 1;
for (auto next_reg = reg->next; next_reg; next_reg = next_reg->next, next_delta++) {
if (next_reg->number >= 0) {
this->assign_number(reg, (next_reg->number - next_delta) & 0xFF);
break;
}
}
if (reg->number >= 0) {
continue;
}
// If any prev register is assigned, assign this register
size_t prev_delta = 1;
for (auto prev_reg = reg->prev; prev_reg; prev_reg = prev_reg->prev, prev_delta++) {
if (prev_reg->number >= 0) {
this->assign_number(reg, (prev_reg->number + prev_delta) & 0xFF);
break;
}
}
if (reg->number >= 0) {
continue;
}
// No prev or next register is assigned; find an interval in the register
// number space that fits this block of registers. The total number of
// register numbers needed is (prev_delta - 1) + (next_delta - 1) + 1.
size_t num_regs = prev_delta + next_delta - 1;
this->assign_number(reg, (this->find_register_number_space(num_regs) + (prev_delta - 1)) & 0xFF);
// We don't need to assign the prev and next registers; they should also
// be in the unassigned set and will be assigned by the above logic
}
// At this point, all registers should be assigned
for (const auto& it : this->named_regs) {
if (it.second->number < 0) {
throw logic_error(string_printf("register %s was not assigned", it.second->name.c_str()));
}
}
for (size_t z = 0; z < 0x100; z++) {
auto reg = this->numbered_regs[z];
if (reg && (reg->number != static_cast<int16_t>(z))) {
throw logic_error(string_printf("register %zu has incorrect number %hd", z, reg->number));
}
}
}
uint8_t find_register_number_space(size_t num_regs) const {
for (size_t candidate = 0; candidate < 0x100; candidate++) {
size_t z;
for (z = 0; z < num_regs; z++) {
if (this->numbered_regs[candidate + z]) {
break;
}
}
if (z == num_regs) {
return candidate;
}
}
throw runtime_error("not enough space to assign registers");
}
unordered_map<string, shared_ptr<Register>> named_regs;
array<shared_ptr<Register>, 0x100> numbered_regs;
};
std::string assemble_quest_script(const std::string& text) {
auto lines = split(text, '\n');
@@ -1901,6 +2081,88 @@ std::string assemble_quest_script(const std::string& text) {
}
}
// Prepare to collect named registers
RegisterAssigner reg_assigner;
auto parse_reg = [&reg_assigner](const string& arg, bool allow_unnumbered = true) -> shared_ptr<RegisterAssigner::Register> {
if (arg.size() < 2) {
throw runtime_error("register argument is too short");
}
if ((arg[0] != 'r') && (arg[0] != 'f')) {
throw runtime_error("a register is required");
}
string name;
ssize_t number = -1;
if (arg[1] == ':') {
auto tokens = split(arg.substr(2), '@');
if (tokens.size() == 1) {
name = std::move(tokens[0]);
} else if (tokens.size() == 2) {
name = std::move(tokens[0]);
number = stoull(tokens[1], nullptr, 0);
} else {
throw runtime_error("invalid register specification");
}
} else {
number = stoull(arg.substr(1), nullptr, 0);
}
if (!allow_unnumbered && (number < 0)) {
throw runtime_error("a numbered register is required");
}
if (number > 0xFF) {
throw runtime_error("invalid register number");
}
return reg_assigner.get_or_create(name, number);
};
auto parse_reg_set_fixed = [&reg_assigner, &parse_reg](const string& name, size_t expected_count) -> vector<shared_ptr<RegisterAssigner::Register>> {
if (expected_count == 0) {
throw logic_error("REG_SET_FIXED argument expects no registers");
}
if (name.empty()) {
throw runtime_error("no register specified for REG_SET_FIXED argument");
}
vector<shared_ptr<RegisterAssigner::Register>> regs;
if ((name[0] == '(') && (name.back() == ')')) {
auto tokens = split(name.substr(1, name.size() - 2), ',');
if (tokens.size() != expected_count) {
throw runtime_error("incorrect number of registers in REG_SET_FIXED");
}
for (auto& token : tokens) {
strip_trailing_whitespace(token);
strip_leading_whitespace(token);
regs.emplace_back(parse_reg(token));
if (regs.size() > 1) {
reg_assigner.constrain(regs.at(regs.size() - 2), regs.back());
}
}
} else {
auto tokens = split(name, '-');
if (tokens.size() == 1) {
regs.emplace_back(parse_reg(tokens[0], false));
while (regs.size() < expected_count) {
regs.emplace_back(parse_reg("", (regs.back()->number + 1) & 0xFF));
reg_assigner.constrain(regs.at(regs.size() - 2), regs.back());
}
} else if (tokens.size() == 2) {
regs.emplace_back(parse_reg(tokens[0], false));
while (regs.size() < expected_count - 1) {
regs.emplace_back(reg_assigner.get_or_create("", (regs.back()->number + 1) & 0xFF));
reg_assigner.constrain(regs.at(regs.size() - 2), regs.back());
}
regs.emplace_back(parse_reg(tokens[1], false));
if (static_cast<size_t>(regs.back()->number - regs.front()->number + 1) != expected_count) {
throw runtime_error("incorrect number of registers used");
}
reg_assigner.constrain(regs.at(regs.size() - 2), regs.back());
} else {
throw runtime_error("invalid fixed register set syntax");
}
}
if (regs.empty() || regs.size() != expected_count) {
throw logic_error("incorrect register count in REG_SET_FIXED after parsing");
}
return regs;
};
// Assemble code segment
bool version_has_args = F_HAS_ARGS & v_flag(quest_version);
const auto& opcodes = opcodes_by_name_for_version(quest_version);
@@ -1974,16 +2236,6 @@ std::string assemble_quest_script(const std::string& text) {
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:
@@ -2019,37 +2271,33 @@ std::string assemble_quest_script(const std::string& text) {
} 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')) {
} else if ((arg[0] == 'r') || (arg[0] == 'f') || ((arg[0] == '(') && (arg.back() == ')'))) {
// 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));
auto reg = parse_reg(arg);
reg->offsets.emplace(code_w.size());
code_w.put_u8(reg->number);
} 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 (static_cast<size_t>(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");
}
auto regs = parse_reg_set_fixed(arg, arg_def.count);
code_w.put_u8(0x4A); // arg_pushb
code_w.put_u8(start_reg);
regs[0]->offsets.emplace(code_w.size());
code_w.put_u8(regs[0]->number);
} else {
code_w.put_u8(0x48); // arg_pushr
code_w.put_u8(parse_reg(arg));
auto reg = parse_reg(arg);
reg->offsets.emplace(code_w.size());
code_w.put_u8(reg->number);
}
} 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)));
auto reg = parse_reg(arg.substr(1));
reg->offsets.emplace(code_w.size());
code_w.put_u8(reg->number);
} 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);
@@ -2094,11 +2342,12 @@ std::string assemble_quest_script(const std::string& text) {
code_w.put_u16(labels_by_name.at(name)->index);
}
};
auto add_reg = [&](const string& name, bool is32) -> void {
auto add_reg = [&](shared_ptr<RegisterAssigner::Register> reg, bool is32) -> void {
reg->offsets.emplace(code_w.size());
if (is32) {
code_w.put_u32l(parse_reg(name));
code_w.put_u32l(reg->number & 0xFF);
} else {
code_w.put_u8(parse_reg(name));
code_w.put_u8(reg->number);
}
};
@@ -2130,30 +2379,21 @@ std::string assemble_quest_script(const std::string& text) {
}
case Type::REG:
case Type::REG32:
add_reg(arg, arg_def.type == Type::REG32);
add_reg(parse_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 (static_cast<size_t>(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");
}
auto regs = parse_reg_set_fixed(arg, arg_def.count);
add_reg(regs[0], arg_def.type == Type::REG32_SET_FIXED);
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);
for (auto reg_arg : regs) {
strip_trailing_whitespace(reg_arg);
strip_leading_whitespace(reg_arg);
add_reg(parse_reg(reg_arg), false);
}
break;
}
@@ -2198,6 +2438,18 @@ std::string assemble_quest_script(const std::string& text) {
code_w.put_u8(0);
}
// Assign all register numbers and patch the code section if needed
reg_assigner.assign_all();
for (size_t z = 0; z < 0x100; z++) {
auto reg = reg_assigner.numbered_regs[z];
if (!reg) {
continue;
}
for (size_t offset : reg->offsets) {
code_w.pput_u8(offset, reg->number);
}
}
// Generate function table
ssize_t function_table_size = labels_by_index.rbegin()->first + 1;
vector<le_uint32_t> function_table;
+82 -59
View File
@@ -59,15 +59,38 @@
// capability.
// .joinable
// 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.
// The quest script begins after the header directives. 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.
// Registers may be named as well as labels. (In the compiled script, registers
// do not have names, so disassembling a quest script always produces only
// numbered registers.) When compiling, all of the following are valid:
// r83 (explicitly numbered register)
// r:difficulty_level (the compiler will assign an unused register number)
// r:difficulty_level@83 (named and explicitly numbered)
// You don't always have to use the same form for each register; for example,
// if you use r:difficulty_level@83 anywhere in the quest script, you can also
// use r:difficulty_level and r83 in other places and they will all refer to the
// same register. (However, if you don't use r:difficulty_level@83 anywhere, but
// you do use r83 and r:difficulty_level, the compiler will assign these to two
// different registers since there is nothing linking the name to the number.)
// Using opcodes that take a consecutive sequence of registers, such as
// map_designate which takes 4, introduces constraints on which registers may be
// assigned to which numbers. For example, before one of the map_designate
// opcodes after the start label, we explicitly assign one register's number,
// but leave the nearby registers' numbers unassigned. The compiler assigns
// those four registers to r60-r63, because they are used in a map_designate
// call. If we didn't explicitly number any of those registers, the compiler
// would instead choose a consecutive sequence of register numbers that aren't
// used anywhere else in the script.
// This quest does not contain any examples of non-script data, but such data
// can be included in the quest script using the .data directive, like this:
@@ -80,63 +103,63 @@
// Every quest must have a start label; this is the main thread that starts when
// the quest begins. The start label is always assigned number 0.
start:
gget 0x0091, r252
gget 0x0091, r:flag_0091_value@252
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
get_difficulty_level_v2 r:difficulty_level@83
leti r:op_arg1, 0 // Pioneer 2
leti r:op_arg2, 0
leti r:op_arg3@62, 0 // See comment above about register assignment
leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
leti r:op_arg1, 1 // Forest 1
leti r:op_arg2, 0
leti r:op_arg3, 0
leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
leti r:op_arg1, 2 // Forest 2
leti r:op_arg2, 0
leti r:op_arg3, 0
leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
leti r:op_arg1, 11 // Dragon
leti r:op_arg2, 0
leti r:op_arg3, 0
leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
ret
return_immediately:
ret
floor_handler_pioneer_2:
switch_jmp r0, [floor_handler_pioneer_2_first_time, floor_handler_pioneer_2_not_first_time]
switch_jmp r:has_talked_to_hopkins, [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
leti r:op_arg1, 0x000000ED
leti r:op_arg2, 0x00000000
leti r:op_arg3, 0x0000014D
leti r:op_arg4, 0xFFFFFFF1
p_setpos 0, (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
leti r:op_arg1, 0x000000FF
leti r:op_arg2, 0x00000000
leti r:op_arg3, 0x00000152
leti r:op_arg4, 0xFFFFFFD5
p_setpos 1, (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
leti r:op_arg1, 0x000000DE
leti r:op_arg2, 0x00000000
leti r:op_arg3, 0x00000142
leti r:op_arg4, 0x00000019
p_setpos 2, (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
leti r:op_arg1, 0x000000F8
leti r:op_arg2, 0x00000000
leti r:op_arg3, 0x00000143
leti r:op_arg4, 0xFFFFFFEC
p_setpos 3, (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
call on_talk_to_hopkins
ret
floor_handler_pioneer_2_not_first_time:
@@ -156,10 +179,10 @@ label00CB@0x00CB:
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
jmpi_eq r:difficulty_level, 0, on_quest_success_normal
jmpi_eq r:difficulty_level, 1, on_quest_success_hard
jmpi_eq r:difficulty_level, 2, on_quest_success_very_hard
jmpi_eq r:difficulty_level, 3, on_quest_success_ultimate
on_quest_success_normal:
window_msg "You\'ve been awarded\n100 Meseta."
bgm 1
@@ -241,7 +264,7 @@ play_dragon_killed_cutscene_when_ready_check_again:
ret
on_dragon_killed:
jmpi_eq r83, 3, on_dragon_killed_ultimate
jmpi_eq r:difficulty_level, 3, on_dragon_killed_ultimate
window_msg "Dragon killed!"
add_msg "Hopkins\'s HEAT SWORD found\nin Dragon\'s mouth!"
se 1
@@ -281,7 +304,7 @@ show_mission_complete_if_needed_check_again:
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
jmpi_eq r:has_talked_to_hopkins, 1, on_talk_to_hopkins_incomplete_again
call start_cutscene
call wait_30_frames
message 100, "Ca... can you help\nme? Please?"
@@ -290,7 +313,7 @@ on_talk_to_hopkins@310:
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
set r:has_talked_to_hopkins
mesend
bgm 1
call end_cutscene
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -e
EXECUTABLE="$1"
if [ "$EXECUTABLE" == "" ]; then
EXECUTABLE="./newserv"
fi
echo "... assemble system/quests/retrieval/q058-gc-e.bin.txt"
$EXECUTABLE assemble-quest-script system/quests/retrieval/q058-gc-e.bin.txt tests/q058-gc-e-test.bin
diff tests/q058-gc-e-test.bin tests/q058-gc-e.bin
echo "... clean up"
rm tests/q058-gc-e-test.bin
BIN
View File
Binary file not shown.