extend persistence to enemy, set, and switch flags

This commit is contained in:
Martin Michelsen
2024-03-12 19:37:34 -07:00
parent 84bb946e05
commit 69f40f9157
14 changed files with 572 additions and 109 deletions
+18 -2
View File
@@ -1471,18 +1471,34 @@ static void server_command_warp(shared_ptr<Client> c, const std::string& args, b
auto s = c->require_server_state();
auto l = c->require_lobby();
check_is_game(l, true);
check_cheats_enabled(l, c);
uint32_t floor = stoul(args, nullptr, 0);
if (c->floor == floor) {
return;
}
// Special case: $warp[me] 0 is allowed in boss arenas if the boss is already
// defeated, even if cheats are disabled. This is because if a player returns
// to a boss arena after a persistence gap in the game, the exit warp won't
// exist, so they need a way to get out.
bool should_check_cheats = is_warpall || (floor != 0) || !floor_is_boss_arena(l->episode, c->floor);
if (!should_check_cheats) {
for (const auto* event : l->map->get_events(c->floor)) {
if (!(event->flags & 0x18)) {
should_check_cheats = true;
break;
}
}
}
if (should_check_cheats) {
check_cheats_enabled(l, c);
}
size_t limit = floor_limit_for_episode(l->episode);
if (limit == 0) {
return;
} else if (floor > limit) {
send_text_message_printf(c, "$C6Area numbers must\nbe %zu or less.", limit);
send_text_message_printf(c, "$C6Area numbers must\nbe %zu or less", limit);
return;
}
+24 -16
View File
@@ -3828,21 +3828,28 @@ struct G_ExtendedHeader {
struct G_Unknown_6x04 {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unused = 0;
le_uint16_t unknown_a2 = 0;
} __packed__;
// 6x05: Switch state changed
// Some things that don't look like switches are implemented as switches using
// this subcommand. For example, when all enemies in a room are defeated, this
// subcommand is used to unlock the doors.
// Note: In the client, this is a subclass of 6x04, similar to how 6xA2 is a
// subclass of 6x60.
struct G_SwitchStateChanged_6x05 {
// Note: header.object_id is 0xFFFF for room clear when all enemies defeated
G_ObjectIDHeader header;
parray<uint8_t, 2> unknown_a1;
// TODO: Some of these might be big-endian on GC; it only byteswaps
// unknown_a3. Are the others actually uint16, or are they uint8[2]?
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
parray<uint8_t, 2> unknown_a3;
uint8_t floor = 0;
le_uint16_t switch_flag_num = 0;
uint8_t switch_flag_floor = 0;
// Only two bits in flags have meanings:
// 01 - set unlock flag (if not set, the flag is cleared instead)
// 02 - play room unlock sound if floor matches client's floor
uint8_t flags = 0; // Bit field, with 2 lowest bits having meaning
} __packed__;
@@ -4635,7 +4642,7 @@ struct G_TriggerSetEvent_6x67 {
G_UnusedHeader header;
le_uint32_t floor = 0;
le_uint32_t event_id = 0; // NOT event index
le_uint32_t unused = 0;
le_uint32_t client_id = 0;
} __packed__;
// 6x68: Create telepipe / cast Ryuker
@@ -4754,11 +4761,11 @@ struct G_SyncSetFlagState_6x6E_Decompressed {
le_uint16_t total_size = 0; // == sum of the following 3 fields
le_uint16_t entity_set_flags_size = 0;
le_uint16_t event_set_flags_size = 0;
le_uint16_t unused_size = 0;
le_uint16_t switch_flags_size = 0;
// Variable-length fields follow here:
// EntitySetFlags entity_set_flags; // Total size is set_flags_size
// le_uint16_t event_set_flags[event_set_flags_size / 2]; // Same order as in map files (NOT sorted by event_id)
// uint8_t unused[is_v1 ? 0x200 : 0x240]; // Possibly an early implementation of 6x6F; unused even in DC NTE
// SwitchFlags switch_flags; // 0x200 bytes on v1 abd earlier; 0x240 bytes on v2 and later
struct EntitySetFlags {
le_uint32_t object_set_flags_offset = 0;
@@ -4993,11 +5000,11 @@ struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 {
le_uint16_t unused = 0;
} __packed__;
// 6x76: Set entity flags
// This command can only be used to set flags, since the game performs a bitwise
// OR operation instead of a simple assignment.
// 6x76: Set entity set flags
// This command can only be used to set set flags, since the game performs a
// bitwise OR operation instead of a simple assignment.
struct G_SetEntityFlags_6x76 {
struct G_SetEntitySetFlags_6x76 {
G_EnemyIDHeader header; // 1000-3FFF = enemy, 4000-FFFF = object
le_uint16_t floor = 0;
le_uint16_t flags = 0;
@@ -5235,8 +5242,9 @@ struct G_Unknown_6x91 {
le_uint32_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
le_uint16_t unknown_a4 = 0;
le_uint16_t unknown_a5 = 0;
parray<uint8_t, 2> unknown_a6;
le_uint16_t switch_flag_num = 0;
uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared
uint8_t switch_flag_floor = 0;
} __packed__;
// 6x92: Unknown (not valid on Episode 3)
@@ -5251,9 +5259,9 @@ struct G_Unknown_6x92 {
struct G_ActivateTimedSwitch_6x93 {
G_UnusedHeader header;
le_uint16_t floor = 0;
le_uint16_t switch_id = 0;
uint8_t unknown_a1 = 0; // Logic is different if this is 1 vs. any other value
le_uint16_t switch_flag_floor = 0;
le_uint16_t switch_flag_num = 0;
uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared
parray<uint8_t, 3> unused;
} __packed__;
+1
View File
@@ -101,6 +101,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
parray<le_uint32_t, 0x20> variations;
std::unique_ptr<QuestFlags> quest_flags_known; // If null, ALL quest flags are known
std::unique_ptr<QuestFlags> quest_flag_values;
std::unique_ptr<SwitchFlags> switch_flags;
// Game config
Version base_version;
+37
View File
@@ -1115,6 +1115,43 @@ Action a_disassemble_quest_map(
data = prs_decompress(data);
}
string result = Map::disassemble_quest_data(data.data(), data.size());
write_output_data(args, result.data(), result.size(), "txt");
});
Action a_disassemble_free_map(
"disassemble-free-map", "\
disassemble-free-map INPUT-FILENAME [OUTPUT-FILENAME]\n\
Disassemble the input free-roam map (.dat or .evt file) into a text\n\
representation of the data it contains. Unlike othe disassembly actions,\n\
this action expects its input to be already decompressed. If the input is\n\
compressed, use the --compressed option. Also unlike other options, the\n\
input must be from a file (that is, INPUT-FILENAME is required and cannot\n\
be \"-\").\n",
+[](Arguments& args) {
const string& input_filename = args.get<string>(1, true);
bool is_events = ends_with(input_filename, ".evt");
bool is_enemies = ends_with(input_filename, "e.dat") || ends_with(input_filename, "e_s.dat") || ends_with(input_filename, "e_c1.dat") || ends_with(input_filename, "e_d.dat");
bool is_objects = ends_with(input_filename, "o.dat") || ends_with(input_filename, "o_s.dat") || ends_with(input_filename, "o_c1.dat") || ends_with(input_filename, "o_d.dat");
if (!is_objects && !is_enemies && !is_events) {
throw runtime_error("cannot determine input file type");
}
string data = read_input_data(args);
if (args.get<bool>("compressed")) {
data = prs_decompress(data);
}
string result;
if (is_objects) {
result = Map::disassemble_objects_data(data.data(), data.size());
} else if (is_enemies) {
result = Map::disassemble_enemies_data(data.data(), data.size());
} else if (is_events) {
result = Map::disassemble_wave_events_data(data.data(), data.size());
} else {
throw logic_error("unhandled input type");
}
result.push_back('\n');
write_output_data(args, result.data(), result.size(), "txt");
});
Action a_disassemble_set_data_table(
+187 -36
View File
@@ -14,6 +14,10 @@ using namespace std;
static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f;
static uint64_t section_index_key(uint8_t floor, uint16_t section, uint16_t wave_number) {
return (static_cast<uint64_t>(floor) << 32) | (static_cast<uint64_t>(section) << 16) | static_cast<uint64_t>(wave_number);
}
const char* Map::name_for_object_type(uint16_t type) {
switch (type) {
case 0x0000:
@@ -655,23 +659,34 @@ string Map::EnemyEntry::str() const {
this->unused.load());
}
Map::Enemy::Enemy(uint16_t enemy_id, size_t source_index, size_t set_index, uint8_t floor, EnemyType type)
Map::Enemy::Enemy(
uint16_t enemy_id,
size_t source_index,
size_t set_index,
uint8_t floor,
uint16_t section,
uint16_t wave_number,
EnemyType type)
: source_index(source_index),
set_index(set_index),
enemy_id(enemy_id),
total_damage(0),
game_flags(0),
section(section),
wave_number(wave_number),
type(type),
floor(floor),
state_flags(0) {}
string Map::Enemy::str() const {
return string_printf("[Map::Enemy E-%hX source %zX %s%s floor=%02hhX flags=%02hhX]",
return string_printf("[Map::Enemy E-%hX source %zX %s%s floor=%02hhX section=%04hX wave_number=%04hX flags=%02hhX]",
this->enemy_id,
this->source_index,
name_for_enum(this->type),
enemy_type_is_rare(this->type) ? " RARE" : "",
this->floor,
this->section,
this->wave_number,
this->state_flags);
}
@@ -725,6 +740,7 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size
.floor = floor,
.base_type = objects[z].base_type,
.section = objects[z].section,
.group = objects[z].group,
.param1 = objects[z].param1,
.param3 = objects[z].param3,
.param4 = objects[z].param4,
@@ -734,6 +750,8 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size
.set_flags = 0,
.item_drop_checked = false,
});
uint64_t k = section_index_key(floor, objects[z].section, objects[z].group);
this->floor_section_and_group_to_object_index.emplace(k, object_id);
}
}
@@ -782,7 +800,9 @@ void Map::add_enemy(
auto add = [&](EnemyType type) -> void {
uint16_t enemy_id = this->enemies.size();
this->enemies.emplace_back(enemy_id, source_index, set_index, floor, type);
this->enemies.emplace_back(enemy_id, source_index, set_index, floor, e.section, e.wave_number, type);
uint64_t k = section_index_key(floor, e.section, e.wave_number);
this->floor_section_and_wave_number_to_enemy_index.emplace(k, enemy_id);
};
EnemyType child_type = EnemyType::UNKNOWN;
@@ -1434,7 +1454,7 @@ void Map::add_random_enemies_from_map_data(
}
if (remaining_waves) {
/* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay);
this->add_event(wave_next_event_id, entry.flags, floor, this->event_action_stream.size());
this->add_event(wave_next_event_id, entry.flags, floor, entry.section, wave_number, this->event_action_stream.size());
this->event_action_stream.push_back(0x0C);
wave_next_event_id = entry.event_id + wave_number + 10000;
this->event_action_stream.append(reinterpret_cast<const char*>(&wave_next_event_id), sizeof(wave_next_event_id));
@@ -1444,15 +1464,17 @@ void Map::add_random_enemies_from_map_data(
}
/* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay);
this->add_event(wave_next_event_id, entry.flags, floor, action_stream_base_offset + entry.action_stream_offset);
this->add_event(wave_next_event_id, entry.flags, floor, entry.section, wave_number, action_stream_base_offset + entry.action_stream_offset);
wave_number++;
}
}
void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset) {
void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint16_t section, uint16_t wave_number, uint32_t action_stream_offset) {
size_t index = this->events.size();
auto& ev = this->events.emplace_back();
ev.event_id = event_id;
ev.section = section;
ev.wave_number = wave_number;
ev.flags = flags;
ev.floor = floor;
ev.action_stream_offset = action_stream_offset;
@@ -1460,6 +1482,9 @@ void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t a
if (!this->floor_and_event_id_to_index.emplace(k, index).second) {
this->log.warning("Duplicate event ID: W-%02hhX-%" PRIX32, floor, event_id);
}
k = section_index_key(floor, section, wave_number);
this->floor_section_and_wave_number_to_event_index.emplace(k, index);
}
Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) {
@@ -1486,7 +1511,7 @@ void Map::add_events_from_map_data(uint8_t floor, const void* data, size_t size)
auto events_r = r.sub(header.entries_offset, sizeof(Event1Entry) * header.entry_count);
while (!events_r.eof()) {
const auto& entry = events_r.get<Event1Entry>();
this->add_event(entry.event_id, entry.flags, floor, entry.action_stream_offset + action_stream_base_offset);
this->add_event(entry.event_id, entry.flags, floor, entry.section, entry.wave_number, entry.action_stream_offset + action_stream_base_offset);
}
}
@@ -1638,6 +1663,154 @@ Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) {
throw out_of_range("enemy not found");
}
std::vector<Map::Object*> Map::get_objects(uint8_t floor, uint16_t section, uint16_t group) {
uint64_t k = section_index_key(floor, section, group);
vector<Object*> ret;
for (auto its = this->floor_section_and_group_to_object_index.equal_range(k); its.first != its.second; its.first++) {
ret.emplace_back(&this->objects.at(its.first->second));
}
return ret;
}
std::vector<Map::Enemy*> Map::get_enemies(uint8_t floor, uint16_t section, uint16_t wave_number) {
uint64_t k = section_index_key(floor, section, wave_number);
vector<Enemy*> ret;
for (auto its = this->floor_section_and_wave_number_to_enemy_index.equal_range(k); its.first != its.second; its.first++) {
ret.emplace_back(&this->enemies.at(its.first->second));
}
return ret;
}
std::vector<Map::Event*> Map::get_events(uint8_t floor, uint16_t section, uint16_t wave_number) {
uint64_t k = section_index_key(floor, section, wave_number);
vector<Event*> ret;
for (auto its = this->floor_section_and_wave_number_to_event_index.equal_range(k); its.first != its.second; its.first++) {
ret.emplace_back(&this->events.at(its.first->second));
}
return ret;
}
std::vector<Map::Event*> Map::get_events(uint8_t floor) {
uint64_t k_start = (static_cast<uint64_t>(floor) << 32);
uint64_t k_end = (static_cast<uint64_t>(floor + 1) << 32);
vector<Event*> ret;
for (auto it = this->floor_and_event_id_to_index.lower_bound(k_start);
(it != this->floor_and_event_id_to_index.end()) && (it->first < k_end);
it++) {
ret.emplace_back(&this->events.at(it->second));
}
return ret;
}
template <typename EntryT>
static string disassemble_vector_file_t(const void* data, size_t size, size_t* entry_number, char type_ch) {
deque<string> ret;
StringReader r(data, size);
size_t local_entry_number = 0;
if (!entry_number) {
entry_number = &local_entry_number;
}
while (r.remaining() >= sizeof(EntryT)) {
string o_str = r.get<EntryT>().str();
ret.emplace_back(string_printf("/* %c-%zX */ %s", type_ch, (*entry_number)++, o_str.c_str()));
}
if (r.remaining()) {
ret.emplace_back("// Warning: section size is not a multiple of entry size");
size_t size = r.remaining();
ret.emplace_back(format_data(r.getv(size), size));
}
return join(ret, "\n");
}
string Map::disassemble_objects_data(const void* data, size_t size, size_t* object_number) {
return disassemble_vector_file_t<ObjectEntry>(data, size, object_number, 'K');
}
string Map::disassemble_enemies_data(const void* data, size_t size, size_t* enemy_number) {
return disassemble_vector_file_t<EnemyEntry>(data, size, enemy_number, 'S');
}
string Map::disassemble_wave_events_data(const void* data, size_t size, uint8_t floor) {
deque<string> ret;
StringReader r(data, size);
const auto& evt_header = r.get<EventsSectionHeader>();
if (evt_header.format == 0x65767432) { // 'evt2'
ret.emplace_back(".evt2_format"); // TODO
size_t size = r.remaining();
ret.emplace_back(format_data(r.getv(size), size));
} else {
auto action_stream_r = r.sub(evt_header.action_stream_offset);
for (size_t z = 0; z < evt_header.entry_count; z++) {
const auto& entry = r.get<Event1Entry>();
ret.emplace_back(string_printf("/* W-%02hhX-%" PRIX32 " */ [Event1Entry flags=%04hX type=%04hX section=%04hX wave_number=%04hX delay=%" PRIu32 "]",
floor,
entry.event_id.load(),
entry.flags.load(),
entry.event_type.load(),
entry.section.load(),
entry.wave_number.load(),
entry.delay.load()));
auto ev_actions_r = action_stream_r.sub(entry.action_stream_offset);
bool should_continue = true;
while (!ev_actions_r.eof() && should_continue) {
uint8_t opcode = ev_actions_r.get_u8();
switch (opcode) {
case 0x00:
ret.emplace_back(string_printf(" 00 nop"));
break;
case 0x01:
ret.emplace_back(string_printf(" 01 stop"));
should_continue = false;
break;
case 0x08: {
uint16_t section = ev_actions_r.get_u16l();
uint16_t group = ev_actions_r.get_u16l();
ret.emplace_back(string_printf(" 08 %04hX %04hX construct_objects section=%04hX group=%04hX",
section, group, section, group));
break;
}
case 0x09: {
uint16_t section = ev_actions_r.get_u16l();
uint16_t wave_number = ev_actions_r.get_u16l();
ret.emplace_back(string_printf(" 09 %04hX %04hX construct_enemies section=%04hX wave_number=%04hX",
section, wave_number, section, wave_number));
break;
}
case 0x0A: {
uint16_t id = ev_actions_r.get_u16l();
ret.emplace_back(string_printf(" 0A %04hX enable_switch_flag id=%04hX", id, id));
break;
}
case 0x0B: {
uint16_t id = ev_actions_r.get_u16l();
ret.emplace_back(string_printf(" 0B %04hX disable_switch_flag id=%04hX", id, id));
break;
}
case 0x0C: {
uint32_t event_id = ev_actions_r.get_u32l();
ret.emplace_back(string_printf(" 0C %04hX trigger_event event_id=%08" PRIX32, event_id, event_id));
break;
}
case 0x0D: {
uint16_t section = ev_actions_r.get_u16l();
uint16_t wave_number = ev_actions_r.get_u16l();
ret.emplace_back(string_printf(" 0D %04hX %04hX construct_enemies_stop section=%04hX wave_number=%04hX",
section, wave_number, section, wave_number));
break;
}
default:
ret.emplace_back(string_printf(" %02hhX .invalid", opcode));
}
}
}
}
return join(ret, "\n");
}
string Map::disassemble_quest_data(const void* data, size_t size) {
auto all_floor_sections = Map::collect_quest_map_data_sections(data, size);
@@ -1651,56 +1824,34 @@ string Map::disassemble_quest_data(const void* data, size_t size) {
if (floor_sections.objects != 0xFFFFFFFF) {
ret.emplace_back(string_printf(".objects %zu", floor));
const auto& header = r.pget<SectionHeader>(floor_sections.objects);
auto sub_r = r.sub(floor_sections.objects + sizeof(SectionHeader), header.data_size);
while (sub_r.remaining() >= sizeof(ObjectEntry)) {
string o_str = sub_r.get<ObjectEntry>().str();
ret.emplace_back(string_printf("/* K-%zX */ %s", object_number++, o_str.c_str()));
}
if (sub_r.remaining()) {
ret.emplace_back("// Warning: object section size is not a multiple of object entry size");
size_t offset = floor_sections.objects + sizeof(SectionHeader) + r.where();
size_t bytes = r.remaining();
ret.emplace_back(format_data(r.getv(r.remaining()), bytes, offset));
}
size_t offset = floor_sections.objects + sizeof(SectionHeader);
ret.emplace_back(Map::disassemble_objects_data(r.pgetv(offset, header.data_size), header.data_size, &object_number));
}
if (floor_sections.enemies != 0xFFFFFFFF) {
ret.emplace_back(string_printf(".enemies %zu", floor));
const auto& header = r.pget<SectionHeader>(floor_sections.enemies);
auto sub_r = r.sub(floor_sections.enemies + sizeof(SectionHeader), header.data_size);
while (sub_r.remaining() >= sizeof(EnemyEntry)) {
string e_str = sub_r.get<EnemyEntry>().str();
ret.emplace_back(string_printf("/* entry %zX */ %s", enemy_number++, e_str.c_str()));
}
if (sub_r.remaining()) {
ret.emplace_back("// Warning: enemy section size is not a multiple of enemy entry size");
size_t offset = floor_sections.objects + sizeof(SectionHeader) + r.where();
size_t bytes = r.remaining();
ret.emplace_back(format_data(r.getv(r.remaining()), bytes, offset));
}
size_t offset = floor_sections.enemies + sizeof(SectionHeader);
ret.emplace_back(Map::disassemble_enemies_data(r.pgetv(offset, header.data_size), header.data_size, &enemy_number));
}
// TODO: Add disassembly for these section types
if (floor_sections.wave_events != 0xFFFFFFFF) {
ret.emplace_back(string_printf(".wave_events %zu", floor));
const auto& header = r.pget<SectionHeader>(floor_sections.wave_events);
size_t offset = floor_sections.wave_events + sizeof(SectionHeader);
auto sub_r = r.sub(offset, header.data_size);
ret.emplace_back(format_data(r.getv(r.remaining()), header.data_size, offset));
ret.emplace_back(Map::disassemble_wave_events_data(r.pgetv(offset, header.data_size), header.data_size, floor));
}
if (floor_sections.random_enemy_locations != 0xFFFFFFFF) {
ret.emplace_back(string_printf(".random_enemy_locations %zu", floor));
const auto& header = r.pget<SectionHeader>(floor_sections.random_enemy_locations);
size_t offset = floor_sections.random_enemy_locations + sizeof(SectionHeader);
auto sub_r = r.sub(offset, header.data_size);
ret.emplace_back(format_data(r.getv(r.remaining()), header.data_size, offset));
ret.emplace_back(format_data(sub_r.getv(sub_r.remaining()), header.data_size, offset));
}
if (floor_sections.random_enemy_definitions != 0xFFFFFFFF) {
ret.emplace_back(string_printf(".random_enemy_definitions %zu", floor));
const auto& header = r.pget<SectionHeader>(floor_sections.random_enemy_definitions);
size_t offset = floor_sections.random_enemy_definitions + sizeof(SectionHeader);
auto sub_r = r.sub(offset, header.data_size);
ret.emplace_back(format_data(r.getv(r.remaining()), header.data_size, offset));
ret.emplace_back(format_data(sub_r.getv(sub_r.remaining()), header.data_size, offset));
}
}
+39 -6
View File
@@ -103,6 +103,10 @@ struct Map {
struct Event1Entry { // Section type 3 (WAVE_EVENTS) if format == 0
/* 00 */ le_uint32_t event_id;
// Bits in flags:
// 0004 = is active
// 0008 = post-wave actions have been run
// 0010 = all enemies killed
/* 04 */ le_uint16_t flags;
/* 06 */ le_uint16_t event_type;
/* 08 */ le_uint16_t section;
@@ -208,10 +212,11 @@ struct Map {
// TODO: Add more fields in here if we ever care about them. Currently we
// only care about boxes with fixed item drops.
size_t source_index;
uint16_t object_id;
uint8_t floor;
uint16_t object_id;
uint16_t base_type;
uint16_t section;
uint16_t group;
float param1; // If <= 0, this is a specialized box, and the specialization is in param4/5/6
float param3; // If == 0, the item should be varied by difficulty and area
uint32_t param4;
@@ -239,23 +244,35 @@ struct Map {
uint16_t enemy_id;
uint16_t total_damage;
uint32_t game_flags; // From 6x0A
uint16_t section;
uint16_t wave_number;
EnemyType type;
uint8_t floor;
uint8_t state_flags;
Enemy(uint16_t enemy_id, size_t source_index, size_t set_index, uint8_t floor, EnemyType type);
Enemy(
uint16_t enemy_id,
size_t source_index,
size_t set_index,
uint8_t floor,
uint16_t section,
uint16_t wave_number,
EnemyType type);
std::string str() const;
} __attribute__((packed));
};
struct Event {
uint32_t event_id;
uint16_t flags;
uint16_t section;
uint16_t wave_number;
uint8_t floor;
uint32_t action_stream_offset;
std::vector<size_t> enemy_indexes;
std::string str() const;
} __attribute__((packed));
};
struct DATParserRandomState {
PSOV2Encryption random;
@@ -306,7 +323,13 @@ struct Map {
std::shared_ptr<DATParserRandomState> random_state,
std::shared_ptr<const RareEnemyRates> rare_rates = DEFAULT_RARE_ENEMIES);
void add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset);
void add_event(
uint32_t event_id,
uint16_t flags,
uint8_t floor,
uint16_t section,
uint16_t wave_number,
uint32_t action_stream_offset);
Event& get_event(uint8_t floor, uint32_t event_id);
const Event& get_event(uint8_t floor, uint32_t event_id) const;
void add_events_from_map_data(uint8_t floor, const void* data, size_t size);
@@ -330,7 +353,14 @@ struct Map {
const Enemy& find_enemy(uint8_t floor, EnemyType type) const;
Enemy& find_enemy(uint8_t floor, EnemyType type);
std::vector<Object*> get_objects(uint8_t floor, uint16_t section, uint16_t wave_number);
std::vector<Enemy*> get_enemies(uint8_t floor, uint16_t section, uint16_t wave_number);
std::vector<Event*> get_events(uint8_t floor, uint16_t section, uint16_t wave_number);
std::vector<Event*> get_events(uint8_t floor);
static std::string disassemble_objects_data(const void* data, size_t size, size_t* object_number = nullptr);
static std::string disassemble_enemies_data(const void* data, size_t size, size_t* enemy_number = nullptr);
static std::string disassemble_wave_events_data(const void* data, size_t size, uint8_t floor = 0xFF);
static std::string disassemble_quest_data(const void* data, size_t size);
PrefixedLogger log;
@@ -343,7 +373,10 @@ struct Map {
std::vector<size_t> rare_enemy_indexes;
std::vector<Event> events;
std::string event_action_stream;
std::unordered_map<uint64_t, size_t> floor_and_event_id_to_index;
std::map<uint64_t, size_t> floor_and_event_id_to_index;
std::unordered_multimap<uint64_t, size_t> floor_section_and_group_to_object_index;
std::unordered_multimap<uint64_t, size_t> floor_section_and_wave_number_to_enemy_index;
std::unordered_multimap<uint64_t, size_t> floor_section_and_wave_number_to_event_index;
};
class SetDataTableBase {
+14
View File
@@ -565,6 +565,20 @@ struct QuestFlagsV1 {
operator QuestFlags() const;
} __attribute__((packed));
struct SwitchFlags {
parray<parray<uint8_t, 0x20>, 0x12> data;
inline bool get(uint8_t floor, uint16_t flag_num) const {
return this->data[floor][flag_num >> 3] & (0x80 >> (flag_num & 7));
}
inline void set(uint8_t floor, uint16_t flag_num) {
this->data[floor][flag_num >> 3] |= (0x80 >> (flag_num & 7));
}
inline void clear(uint8_t floor, uint16_t flag_num) {
this->data[floor][flag_num >> 3] &= ~(0x80 >> (flag_num & 7));
}
} __attribute__((packed));
struct BattleRules {
enum class TechDiskMode : uint8_t {
ALLOW = 0,
+2 -11
View File
@@ -2515,17 +2515,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
// leader)
if (game->count_clients() == 1) {
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
// TODO: Eventually, we want to send the enemy and set states too,
// but currently this doesn't work well. Instead, we reset their
// flags so it's as if they were never defeated.
// c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE);
if (game->map) {
for (auto& enemy : game->map->enemies) {
enemy.game_flags = 0;
enemy.total_damage = 0;
enemy.state_flags = 0;
}
}
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE);
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE);
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
}
@@ -4329,6 +4319,7 @@ shared_ptr<Lobby> create_game_generic(
game->quest_flag_values = make_unique<QuestFlags>();
game->quest_flags_known = make_unique<QuestFlags>();
}
game->switch_flags = make_unique<SwitchFlags>();
return game;
}
+208 -34
View File
@@ -485,11 +485,10 @@ static void on_sync_joining_player_compressed_state(shared_ptr<Client> c, uint8_
send_game_item_state(target);
break;
}
case 0x6E: {
StringReader r(decompressed);
const auto& dec_header = r.get<G_SyncSetFlagState_6x6E_Decompressed>();
if (dec_header.total_size != dec_header.entity_set_flags_size + dec_header.event_set_flags_size + dec_header.unused_size) {
if (dec_header.total_size != dec_header.entity_set_flags_size + dec_header.event_set_flags_size + dec_header.switch_flags_size) {
throw runtime_error("incorrect size fields in 6x6E header");
}
@@ -518,8 +517,9 @@ static void on_sync_joining_player_compressed_state(shared_ptr<Client> c, uint8_
for (size_t z = 0; z < min<size_t>(set_flags_header.num_object_sets, l->map->objects.size()); z++) {
uint16_t flags = set_flags_r.get_u16l();
if (flags != l->map->objects[z].set_flags) {
l->log.warning("(K-%zX) Set flags from client (%04hX) do not match flags from map (%04hX)",
l->log.warning("(K-%zX) Set flags from client (%04hX) do not match set flags from map (%04hX)",
z, flags, l->map->objects[z].set_flags);
l->map->objects[z].set_flags = flags;
}
}
@@ -533,8 +533,9 @@ static void on_sync_joining_player_compressed_state(shared_ptr<Client> c, uint8_
for (size_t z = 0; z < min<size_t>(set_flags_header.num_enemy_sets, l->map->enemy_set_flags.size()); z++) {
uint16_t flags = set_flags_r.get_u16l();
if (flags != l->map->enemy_set_flags[z]) {
l->log.warning("(S-%zX) Set flags from client (%04hX) do not match flags from map (%04hX)",
l->log.warning("(S-%zX) Set flags from client (%04hX) do not match set flags from map (%04hX)",
z, flags, l->map->enemy_set_flags[z]);
l->map->enemy_set_flags[z] = flags;
}
}
@@ -552,32 +553,60 @@ static void on_sync_joining_player_compressed_state(shared_ptr<Client> c, uint8_
}
for (size_t z = 0; z < min<size_t>(num_event_flags, l->map->events.size()); z++) {
uint16_t flags = event_set_flags_r.get_u16l();
const auto& event = l->map->events[z];
auto& event = l->map->events[z];
if (flags != event.flags) {
l->log.warning("(W-%02hhX-%" PRIX32 ") Event flags from client (%04hX) do not match flags from map (%04hX)",
event.floor, event.event_id, flags, event.flags);
event.flags = flags;
}
}
}
size_t expected_unused_size = is_v1(c->version()) ? 0x200 : 0x240;
size_t target_unused_size = is_v1(target->version()) ? 0x200 : 0x240;
if (dec_header.unused_size != expected_unused_size) {
l->log.warning("Unused data size (0x%" PRIX32 ") does not match expected size (0x%zX)",
dec_header.unused_size.load(), expected_unused_size);
size_t expected_switch_flag_num_floors = is_v1(c->version()) ? 0x10 : 0x12;
size_t expected_switch_flags_size = expected_switch_flag_num_floors * 0x20;
if (dec_header.switch_flags_size != expected_switch_flags_size) {
l->log.warning("Switch flags size (0x%" PRIX32 ") does not match expected size (0x%zX)",
dec_header.switch_flags_size.load(), expected_switch_flags_size);
} else {
l->log.info("Switch flags size matches expected size (0x%zX)", expected_switch_flags_size);
}
if (dec_header.unused_size != target_unused_size) {
l->log.info("Resizing unused data from 0x%" PRIX32 " bytes to 0x%zX bytes",
dec_header.unused_size.load(), target_unused_size);
if (dec_header.unused_size >= decompressed.size()) {
throw runtime_error("unused size is too large");
if (l->switch_flags) {
StringReader switch_flags_r = r.sub(r.where() + dec_header.entity_set_flags_size + dec_header.event_set_flags_size);
for (size_t floor = 0; floor < expected_switch_flag_num_floors; floor++) {
// There is a bug in most (perhaps all) versions of the game, which
// causes this array to be too small. It looks like Sega forgot to
// account for the header (G_SyncSetFlagState_6x6E_Decompressed)
// before compressing the buffer, so the game cuts off the last 8
// bytes of the switch flags. Since this only affects the last floor,
// which rarely has any switches on it (or is even accessible by the
// player), it's not surprising that no one noticed this. But it does
// mean we have to check switch_flags_r.eof() here.
for (size_t z = 0; (z < 0x20) && !switch_flags_r.eof(); z++) {
uint8_t& l_flags = l->switch_flags->data[floor][z];
uint8_t r_flags = switch_flags_r.get_u8();
if (l_flags != r_flags) {
l->log.warning("Switch flags do not match at %02zX[%02zX] (expected %02hhX, received %02hhX)",
floor, z, l_flags, r_flags);
l_flags = r_flags;
}
}
}
decompressed.resize(decompressed.size() - dec_header.unused_size.load() + target_unused_size, '\0');
auto* wdec_header = reinterpret_cast<G_SyncSetFlagState_6x6E_Decompressed*>(decompressed.data());
wdec_header->unused_size = target_unused_size;
wdec_header->total_size = wdec_header->entity_set_flags_size + wdec_header->event_set_flags_size + wdec_header->unused_size;
}
// size_t target_switch_flag_num_floors = is_v1(target->version()) ? 0x10 : 0x12;
// size_t target_switch_flags_size = target_switch_flag_num_floors * 0x20;
// if (dec_header.switch_flags_size != target_switch_flags_size) {
// l->log.info("Resizing switch flags from 0x%" PRIX32 " bytes to 0x%zX bytes",
// dec_header.switch_flags_size.load(), target_switch_flags_size);
// if (dec_header.switch_flags_size >= decompressed.size()) {
// throw runtime_error("switch flags size is too large");
// }
// decompressed.resize(decompressed.size() - dec_header.switch_flags_size.load() + target_switch_flags_size, '\0');
// auto* wdec_header = reinterpret_cast<G_SyncSetFlagState_6x6E_Decompressed*>(decompressed.data());
// wdec_header->switch_flags_size = target_switch_flags_size;
// wdec_header->total_size = wdec_header->entity_set_flags_size + wdec_header->event_set_flags_size + wdec_header->switch_flags_size;
// }
send_game_join_sync_command_compressed(
target,
compressed_data,
@@ -1514,6 +1543,14 @@ static void on_switch_state_changed(shared_ptr<Client> c, uint8_t command, uint8
forward_subcommand(c, command, flag, data, size);
if (l->switch_flags) {
if (cmd.flags & 1) {
l->switch_flags->set(cmd.switch_flag_floor, cmd.switch_flag_num);
} else {
l->switch_flags->clear(cmd.switch_flag_floor, cmd.switch_flag_num);
}
}
if (cmd.flags && cmd.header.object_id != 0xFFFF) {
if (!l->quest &&
c->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) &&
@@ -2718,25 +2755,130 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
}
}
static void on_set_entity_flag(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
static void on_set_entity_set_flag(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
auto l = c->require_lobby();
if (!l->is_game()) {
return;
}
const auto& cmd = check_size_t<G_SetEntityFlags_6x76>(data, size);
const auto& cmd = check_size_t<G_SetEntitySetFlags_6x76>(data, size);
if (l->map) {
if (cmd.header.enemy_id >= 0x1000 && cmd.header.enemy_id < 0x4000) {
if (cmd.header.enemy_id >= 0x4000) {
uint16_t object_index = cmd.header.enemy_id - 0x4000;
try {
l->map->enemies.at(cmd.header.enemy_id - 0x1000).game_flags |= cmd.flags;
uint16_t& set_flags = l->map->objects.at(object_index).set_flags;
set_flags |= cmd.flags;
l->log.info("Client set set flags %04hX on K-%hX (flags are now %04hX)",
cmd.flags.load(), object_index, cmd.flags.load());
} catch (const out_of_range&) {
l->log.warning("Flag update refers to missing object");
}
} else if (cmd.header.enemy_id >= 0x1000) {
int32_t section = -1;
int32_t wave_number = -1;
uint16_t enemy_index = cmd.header.enemy_id - 0x1000;
try {
const auto& enemy = l->map->enemies.at(enemy_index);
uint16_t& set_flags = l->map->enemy_set_flags.at(enemy.set_index);
set_flags |= cmd.flags;
section = enemy.section;
wave_number = enemy.wave_number;
l->log.info("Client set set flags %04hX on E-%hX (flags are now %04hX)",
cmd.flags.load(), enemy_index, cmd.flags.load());
} catch (const out_of_range&) {
l->log.warning("Flag update refers to missing enemy");
}
} else if (cmd.header.enemy_id >= 0x4000) {
try {
l->map->objects.at(cmd.header.enemy_id - 0x4000).game_flags |= cmd.flags;
} catch (const out_of_range&) {
l->log.warning("Flag update refers to missing object");
if ((section >= 0) && (wave_number >= 0)) {
// When all enemies in a wave event have (set_flags & 8), which means
// they are defeated, set event_flags = (event_flags | 0x18) & (~4),
// which means it is done and should not trigger
bool all_enemies_defeated = true;
l->log.info("Checking for defeated enemies with section=%04" PRIX32 " wave_number=%04" PRIX32,
section, wave_number);
for (const Map::Enemy* enemy : l->map->get_enemies(cmd.floor, section, wave_number)) {
if (!(l->map->enemy_set_flags.at(enemy->set_index) & 8)) {
l->log.info("E-%hX is not defeated; cannot advance event to finished state", enemy->enemy_id);
all_enemies_defeated = false;
break;
} else {
l->log.info("E-%hX is defeated", enemy->enemy_id);
}
}
if (all_enemies_defeated) {
l->log.info("All enemies defeated; setting events with section=%04" PRIX32 " wave_number=%04" PRIX32 " to finished state",
section, wave_number);
for (Map::Event* event : l->map->get_events(cmd.floor, section, wave_number)) {
event->flags = (event->flags | 0x18) & (~4);
l->log.info("Set flags on W-%02hhX-%" PRIX32 " to %04hX", event->floor, event->event_id, event->flags);
StringReader actions_r(l->map->event_action_stream);
actions_r.go(event->action_stream_offset);
while (!actions_r.eof()) {
uint8_t opcode = actions_r.get_u8();
switch (opcode) {
case 0x00: // nop
l->log.info("(W-%02hhX-%" PRIX32 " script) nop", event->floor, event->event_id);
break;
case 0x01: // stop
l->log.info("(W-%02hhX-%" PRIX32 " script) stop", event->floor, event->event_id);
actions_r.go(actions_r.size());
break;
case 0x08: { // construct_objects
uint16_t section = actions_r.get_u16l();
uint16_t group = actions_r.get_u16l();
l->log.info("(W-%02hhX-%" PRIX32 " script) construct_objects %04hX %04hX", event->floor, event->event_id, section, group);
for (auto* obj : l->map->get_objects(event->floor, section, group)) {
if (!(obj->set_flags & 0x0A)) {
l->log.info("(W-%02hhX-%" PRIX32 " script) Setting flags 0012 on object K-%hX", event->floor, event->event_id, obj->object_id);
obj->set_flags |= 0x12;
}
}
break;
}
case 0x09: // construct_enemies
case 0x0D: { // construct_enemies_stop
uint16_t section = actions_r.get_u16l();
uint16_t wave_number = actions_r.get_u16l();
l->log.info("(W-%02hhX-%" PRIX32 " script) construct_enemies %04hX %04hX", event->floor, event->event_id, section, wave_number);
for (auto* enemy : l->map->get_enemies(event->floor, section, wave_number)) {
uint16_t& set_flags = l->map->enemy_set_flags.at(enemy->set_index);
if (!(set_flags & 0x0A)) {
l->log.info("(W-%02hhX-%" PRIX32 " script) Setting flags 0002 on enemy set S-%zX (from E-%hX)", event->floor, event->event_id, enemy->set_index, enemy->enemy_id);
set_flags |= 0x02;
}
}
if (opcode == 0x0D) {
actions_r.go(actions_r.size());
}
break;
}
case 0x0A: // enable_switch_flag
case 0x0B: { // disable_switch_flag
// These opcodes cause the client to send 6x05 commands, so
// we don't have to do anything here.
uint16_t switch_flag_num = actions_r.get_u16l();
l->log.info("(W-%02hhX-%" PRIX32 " script) %sable_switch_flag %04hX",
event->floor, event->event_id, (opcode & 1) ? "dis" : "en", switch_flag_num);
break;
}
case 0x0C: { // trigger_event
// This opcode causes the client to send a 6x67 command, so
// we don't have to do anything here.
uint32_t event_id = actions_r.get_u32l();
l->log.info("(W-%02hhX-%" PRIX32 " script) trigger_event W-%02hhX-%" PRIX32,
event->floor, event->event_id, event->floor, event_id);
break;
}
default:
l->log.warning("(W-%02hhX-%" PRIX32 ") Invalid opcode %02hhX at offset %zX in event action stream",
event->floor, event->event_id, opcode, actions_r.where() - 1);
actions_r.go(actions_r.size());
}
}
}
}
}
}
}
@@ -2752,18 +2894,50 @@ static void on_trigger_set_event(shared_ptr<Client> c, uint8_t command, uint8_t
const auto& cmd = check_size_t<G_TriggerSetEvent_6x67>(data, size);
if (l->map) {
// TODO: The game's logic is significantly more complex than this. Do we
// need to do anything fancy here?
try {
l->map->get_event(cmd.floor, cmd.event_id).flags |= 0x04;
l->log.info("Client triggered set event W-%02" PRIX32 "-%" PRIX32, cmd.floor.load(), cmd.event_id.load());
} catch (const out_of_range&) {
l->log.warning("Client triggered missing event W-%02" PRIX32 "-%" PRIX32, cmd.floor.load(), cmd.event_id.load());
l->log.warning("Client triggered missing set event W-%02" PRIX32 "-%" PRIX32, cmd.floor.load(), cmd.event_id.load());
}
}
forward_subcommand(c, command, flag, data, size);
}
static void on_unknown_6x91(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
const auto& cmd = check_size_t<G_Unknown_6x91>(data, size);
auto l = c->require_lobby();
if (!l->is_game()) {
return;
}
if (l->switch_flags &&
(cmd.should_set == 1) &&
(cmd.switch_flag_num < 0x100) &&
(cmd.switch_flag_floor < 0x12) &&
(cmd.header.object_id >= 0x4000) &&
(cmd.header.object_id != 0xFFFF)) {
l->switch_flags->set(cmd.switch_flag_floor, cmd.switch_flag_num);
}
forward_subcommand(c, command, flag, data, size);
}
static void on_activate_timed_switch(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
const auto& cmd = check_size_t<G_ActivateTimedSwitch_6x93>(data, size);
auto l = c->require_lobby();
if (!l->is_game()) {
return;
}
if (l->switch_flags) {
if (cmd.should_set == 1) {
l->switch_flags->set(cmd.switch_flag_floor, cmd.switch_flag_num);
} else {
l->switch_flags->clear(cmd.switch_flag_floor, cmd.switch_flag_num);
}
}
forward_subcommand(c, command, flag, data, size);
}
static void on_battle_scores(shared_ptr<Client> c, uint8_t command, uint8_t, void* data, size_t size) {
const auto& cmd = check_size_t<G_BattleScores_6x7F<false>>(data, size);
@@ -4220,7 +4394,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
/* 6x73 */ {0x00, 0x00, 0x73, on_forward_check_game_quest},
/* 6x74 */ {0x62, 0x69, 0x74, on_word_select, SDF::ALWAYS_FORWARD_TO_WATCHERS},
/* 6x75 */ {0x00, 0x00, 0x75, on_set_quest_flag},
/* 6x76 */ {0x00, 0x00, 0x76, on_set_entity_flag},
/* 6x76 */ {0x00, 0x00, 0x76, on_set_entity_set_flag},
/* 6x77 */ {0x00, 0x00, 0x77, on_forward_check_game},
/* 6x78 */ {0x00, 0x00, 0x78, forward_subcommand_m},
/* 6x79 */ {0x00, 0x00, 0x79, on_forward_check_lobby},
@@ -4247,9 +4421,9 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
/* 6x8E */ {0x00, 0x00, 0x8E, on_forward_check_game},
/* 6x8F */ {0x00, 0x00, 0x8F, on_forward_check_game},
/* 6x90 */ {0x00, 0x00, 0x90, on_forward_check_game},
/* 6x91 */ {0x00, 0x00, 0x91, on_forward_check_game},
/* 6x91 */ {0x00, 0x00, 0x91, on_unknown_6x91},
/* 6x92 */ {0x00, 0x00, 0x92, on_forward_check_game},
/* 6x93 */ {0x00, 0x00, 0x93, on_forward_check_game},
/* 6x93 */ {0x00, 0x00, 0x93, on_activate_timed_switch},
/* 6x94 */ {0x00, 0x00, 0x94, on_warp},
/* 6x95 */ {0x00, 0x00, 0x95, on_forward_check_game},
/* 6x96 */ {0x00, 0x00, 0x96, on_forward_check_game},
+8 -3
View File
@@ -2615,8 +2615,8 @@ void send_game_set_state(shared_ptr<Client> c) {
G_SyncSetFlagState_6x6E_Decompressed header;
header.entity_set_flags_size = sizeof(entity_set_flags_header) + (num_object_sets + num_enemy_sets) * sizeof(le_uint16_t);
header.event_set_flags_size = sizeof(le_uint16_t) * l->map->events.size();
header.unused_size = is_v1(c->version()) ? 0x200 : 0x240;
header.total_size = header.entity_set_flags_size + header.event_set_flags_size + header.unused_size;
header.switch_flags_size = is_v1(c->version()) ? 0x200 : 0x240;
header.total_size = header.entity_set_flags_size + header.event_set_flags_size + header.switch_flags_size;
StringWriter w;
w.put(header);
@@ -2630,7 +2630,12 @@ void send_game_set_state(shared_ptr<Client> c) {
for (const auto& event : l->map->events) {
w.put_u16l(event.flags);
}
w.extend_by(header.unused_size, 0x00);
if (l->switch_flags) {
static_assert(sizeof(SwitchFlags) == 0x240, "switch_flags size is incorrect");
w.write(l->switch_flags->data.data(), header.switch_flags_size);
} else {
w.extend_by(header.switch_flags_size, 0x00);
}
send_game_join_sync_command(c, w.str(), 0x5F, 0x66, 0x6E);
}
+13
View File
@@ -775,6 +775,19 @@ const char* name_for_floor(Episode episode, uint8_t floor) {
}
}
bool floor_is_boss_arena(Episode episode, uint8_t floor) {
switch (episode) {
case Episode::EP1:
return (floor >= 0x0B) && (floor <= 0x0E);
case Episode::EP2:
return (floor >= 0x0C) && (floor <= 0x0F);
case Episode::EP4:
return (floor == 0x09);
default:
return false;
}
}
uint32_t class_flags_for_class(uint8_t char_class) {
static constexpr uint8_t flags[12] = {
0x25, 0x2A, 0x31, 0x45, 0x51, 0x52, 0x86, 0x89, 0x8A, 0x32, 0x85, 0x46};
+1
View File
@@ -76,6 +76,7 @@ extern const std::unordered_map<std::string, uint8_t> mag_color_for_name;
size_t floor_limit_for_episode(Episode ep);
uint8_t floor_for_name(const std::string& name);
const char* name_for_floor(Episode episode, uint8_t floor);
bool floor_is_boss_arena(Episode episode, uint8_t floor);
uint32_t class_flags_for_class(uint8_t char_class);