add enemy, object, and event tracking for persistence

This commit is contained in:
Martin Michelsen
2024-02-28 19:42:45 -08:00
parent b81385efdb
commit 34bac4c5b5
85 changed files with 1004 additions and 481 deletions
+165 -49
View File
@@ -675,6 +675,15 @@ string Map::Enemy::str() const {
this->state_flags);
}
string Map::Event::str() const {
return string_printf("[Map::Event W-%02hhX-%" PRIX32 " flags=%04hX floor=%02hhX action_stream_offset=%" PRIX32 "]",
this->floor,
this->event_id,
this->flags,
this->floor,
this->action_stream_offset);
}
string Map::Object::str() const {
return string_printf("[Map::Object source %zX %04hX(%s) @%04hX p1=%g p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]",
this->source_index,
@@ -1189,9 +1198,6 @@ void Map::add_enemy(
case 0x00C7: // TBoss3VoloptHiraisin
case 0x0118:
add(EnemyType::UNKNOWN);
this->log.warning(
"(Entry %zu, offset %zX in file) Unknown enemy type %04hX",
source_index, source_index * sizeof(EnemyEntry), e.base_type.load());
break;
default:
@@ -1320,6 +1326,10 @@ void Map::add_random_enemies_from_map_data(
}
wave_events_segment_r.go(wave_events_header.entries_offset);
size_t action_stream_base_offset = this->event_action_stream.size();
this->event_action_stream += wave_events_segment_r.pread(
wave_events_header.action_stream_offset, wave_events_segment_r.size() - wave_events_header.action_stream_offset);
const auto& locations_header = locations_segment_r.get<RandomEnemyLocationsHeader>();
const auto& definitions_header = definitions_segment_r.get<RandomEnemyDefinitionsHeader>();
auto definitions_r = definitions_segment_r.sub(
@@ -1336,6 +1346,7 @@ void Map::add_random_enemies_from_map_data(
size_t remaining_waves = random_state->rand_int_biased(1, entry.max_waves);
// Trace: at 0080E125 EAX is wave count
le_uint32_t wave_next_event_id = entry.event_id;
uint32_t wave_number = entry.wave_number;
while (remaining_waves) {
remaining_waves--;
@@ -1422,17 +1433,60 @@ void Map::add_random_enemies_from_map_data(
}
}
if (remaining_waves) {
// We don't generate the event stream here, but the client does, and in
// doing so, it uses one value from random to determine the delay
// parameter of the event. To keep our state in sync with what the
// client would do, we skip a random value here.
random_state->random.next();
/* 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->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));
this->event_action_stream.push_back(0x01);
wave_number++;
}
}
// For the same reason as above, we need to skip another random value here.
random_state->random.next();
/* 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);
wave_number++;
}
}
void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset) {
size_t index = this->events.size();
auto& ev = this->events.emplace_back();
ev.event_id = event_id;
ev.flags = flags;
ev.floor = floor;
ev.action_stream_offset = action_stream_offset;
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
if (!this->floor_and_event_id_to_index.emplace(k, index).second) {
this->log.warning("Duplicate event ID: W-%02hhX-%" PRIX32, floor, event_id);
}
}
Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) {
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
return this->events.at(this->floor_and_event_id_to_index.at(k));
}
const Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) const {
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
return this->events.at(this->floor_and_event_id_to_index.at(k));
}
void Map::add_events_from_map_data(uint8_t floor, const void* data, size_t size) {
StringReader r(data, size);
const auto& header = r.get<EventsSectionHeader>();
if (header.format != 0) {
throw runtime_error("events section format is not zero");
}
size_t action_stream_base_offset = this->event_action_stream.size();
this->event_action_stream += r.pread(header.action_stream_offset, r.size() - header.action_stream_offset);
this->events.reserve(this->events.size() + header.entry_count);
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);
}
}
@@ -1519,23 +1573,10 @@ void Map::add_entities_from_quest_data(
this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size);
}
if (floor_sections.enemies != 0xFFFFFFFF) {
const auto& header = r.pget<SectionHeader>(floor_sections.enemies);
if (header.data_size % sizeof(EnemyEntry)) {
throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size");
}
this->add_enemies_from_map_data(
episode,
difficulty,
event,
floor,
r.pgetv(floor_sections.enemies + sizeof(header), header.data_size),
header.data_size,
rare_rates);
} else if ((floor_sections.wave_events != 0xFFFFFFFF) &&
if ((floor_sections.wave_events != 0xFFFFFFFF) &&
(floor_sections.random_enemy_locations != 0xFFFFFFFF) &&
(floor_sections.random_enemy_definitions != 0xFFFFFFFF)) {
// Challenge Mode random enemy waves
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
const auto& random_enemy_locations_header = r.pget<SectionHeader>(floor_sections.random_enemy_locations);
const auto& random_enemy_definitions_header = r.pget<SectionHeader>(floor_sections.random_enemy_definitions);
@@ -1552,6 +1593,29 @@ void Map::add_entities_from_quest_data(
r.sub(floor_sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size),
random_state,
rare_rates);
} else {
// Non-Challenge (standard) enemies
if (floor_sections.enemies != 0xFFFFFFFF) {
const auto& header = r.pget<SectionHeader>(floor_sections.enemies);
if (header.data_size % sizeof(EnemyEntry)) {
throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size");
}
this->add_enemies_from_map_data(
episode,
difficulty,
event,
floor,
r.pgetv(floor_sections.enemies + sizeof(header), header.data_size),
header.data_size,
rare_rates);
}
if (floor_sections.wave_events != 0xFFFFFFFF) {
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
const void* data = r.pgetv(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size);
this->add_events_from_map_data(floor, data, wave_events_header.data_size);
}
}
}
}
@@ -1657,14 +1721,14 @@ parray<le_uint32_t, 0x20> SetDataTableBase::generate_variations(
}
vector<string> SetDataTableBase::map_filenames_for_variations(
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, bool is_enemies) const {
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, FilenameType type) const {
vector<string> ret;
for (uint8_t floor = 0; floor < 0x10; floor++) {
ret.emplace_back(this->map_filename_for_variation(
floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, is_enemies));
floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, type));
}
for (uint8_t floor = 0x10; floor < 0x12; floor++) {
ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, is_enemies));
ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, type));
}
return ret;
}
@@ -1738,8 +1802,8 @@ void SetDataTable::load_table_t(const string& data) {
while (!var2_r.eof()) {
auto& entry = var2_v.emplace_back();
entry.object_list_basename = r.pget_cstr(var2_r.get<U32T>());
entry.enemy_list_basename = r.pget_cstr(var2_r.get<U32T>());
entry.event_list_basename = r.pget_cstr(var2_r.get<U32T>());
entry.enemy_and_event_list_basename = r.pget_cstr(var2_r.get<U32T>());
entry.area_setup_filename = r.pget_cstr(var2_r.get<U32T>());
}
}
}
@@ -1750,7 +1814,10 @@ pair<uint32_t, uint32_t> SetDataTable::num_available_variations_for_floor(Episod
if (area == 0xFF) {
return make_pair(1, 1);
} else {
const auto& e = this->entries.at(area);
if (area >= this->entries.size()) {
return make_pair(1, 1);
}
const auto& e = this->entries[area];
return make_pair(e.size(), e.at(0).size());
}
}
@@ -1789,7 +1856,7 @@ pair<uint32_t, uint32_t> SetDataTable::num_free_roam_variations_for_floor(Episod
}
string SetDataTable::map_filename_for_variation(
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const {
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const {
uint8_t area = this->default_area_for_floor(episode, floor);
if (area == 0xFF) {
return "";
@@ -1800,22 +1867,35 @@ string SetDataTable::map_filename_for_variation(
}
const auto& entry = this->entries.at(area).at(var1).at(var2);
string filename = is_enemies ? entry.enemy_list_basename : entry.object_list_basename;
filename += (is_enemies ? "e" : "o");
string filename;
switch (type) {
case FilenameType::OBJECTS:
filename = entry.object_list_basename + "o";
break;
case FilenameType::ENEMIES:
filename = entry.enemy_and_event_list_basename + "e";
break;
case FilenameType::EVENTS:
filename = entry.enemy_and_event_list_basename;
break;
default:
throw logic_error("invalid map filename type");
}
bool is_events = (type == FilenameType::EVENTS);
switch ((floor != 0) ? GameMode::NORMAL : mode) {
case GameMode::NORMAL:
filename += ".dat";
filename += is_events ? ".evt" : ".dat";
break;
case GameMode::SOLO:
filename += "_s.dat";
filename += is_events ? "_s.evt" : "_s.dat";
break;
case GameMode::CHALLENGE:
filename += "_c1.dat";
filename += is_events ? "_c1.evt" : "_c1.dat";
break;
case GameMode::BATTLE:
filename += "_d.dat";
filename += is_events ? "_d.evt" : "_d.dat";
break;
default:
throw logic_error("invalid game mode");
@@ -1826,14 +1906,15 @@ string SetDataTable::map_filename_for_variation(
string SetDataTable::str() const {
vector<string> lines;
lines.emplace_back(string_printf("FL/V1/V2 => ----------------------OBJECT -----------------------ENEMY -----------------------EVENT\n"));
lines.emplace_back(string_printf("FL/V1/V2 => ----------------------OBJECT -----------------ENEMY+EVENT -----------------------SETUP\n"));
for (size_t a = 0; a < this->entries.size(); a++) {
const auto& v1_v = this->entries[a];
for (size_t v1 = 0; v1 < v1_v.size(); v1++) {
const auto& v2_v = v1_v[v1];
for (size_t v2 = 0; v2 < v2_v.size(); v2++) {
const auto& e = v2_v[v2];
lines.emplace_back(string_printf("%02zX/%02zX/%02zX => %28s %28s %28s\n", a, v1, v2, e.object_list_basename.c_str(), e.enemy_list_basename.c_str(), e.event_list_basename.c_str()));
lines.emplace_back(string_printf("%02zX/%02zX/%02zX => %28s %28s %28s\n",
a, v1, v2, e.object_list_basename.c_str(), e.enemy_and_event_list_basename.c_str(), e.area_setup_filename.c_str()));
}
}
}
@@ -1854,7 +1935,7 @@ struct AreaMapFileInfo {
variation2_values(variation2_values) {}
};
const array<vector<vector<string>>, 0x10> SetDataTableDCNTE::NAMES = {{
const array<vector<vector<string>>, 0x12> SetDataTableDCNTE::NAMES = {{
/* 00 */ {{"map_city00_00"}},
/* 01 */ {{"map_forest01_00", "map_forest01_01"}},
/* 02 */ {{"map_forest02_00", "map_forest02_03"}},
@@ -1871,12 +1952,15 @@ const array<vector<vector<string>>, 0x10> SetDataTableDCNTE::NAMES = {{
/* 0D */ {{"map_boss03"}},
/* 0E */ {{"map_boss04"}},
/* 0F */ {{"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}},
/* 10 */ {},
/* 11 */ {},
}};
SetDataTableDCNTE::SetDataTableDCNTE() : SetDataTableBase(Version::DC_NTE) {}
pair<uint32_t, uint32_t> SetDataTableDCNTE::num_available_variations_for_floor(Episode, uint8_t floor) const {
return make_pair(this->NAMES[floor].size(), this->NAMES[floor][0].size());
const auto& floor_names = this->NAMES.at(floor);
return make_pair(floor_names.size(), floor_names.empty() ? 0 : this->NAMES.at(floor)[0].size());
}
pair<uint32_t, uint32_t> SetDataTableDCNTE::num_free_roam_variations_for_floor(Episode episode, bool, uint8_t floor) const {
@@ -1884,14 +1968,29 @@ pair<uint32_t, uint32_t> SetDataTableDCNTE::num_free_roam_variations_for_floor(E
}
string SetDataTableDCNTE::map_filename_for_variation(
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, bool is_enemies) const {
if (floor >= this->NAMES.size()) {
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, FilenameType type) const {
try {
string basename = this->NAMES.at(floor).at(var1).at(var2);
switch (type) {
case FilenameType::ENEMIES:
basename += "e.dat";
break;
case FilenameType::OBJECTS:
basename += "o.dat";
break;
case FilenameType::EVENTS:
basename += ".evt";
break;
default:
throw logic_error("invalid map filename type");
}
return basename;
} catch (const out_of_range&) {
return "";
}
return this->NAMES.at(floor).at(var1).at(var2) + (is_enemies ? "e.dat" : "o.dat");
}
const array<vector<vector<string>>, 0x10> SetDataTableDC112000::NAMES = {{
const array<vector<vector<string>>, 0x12> SetDataTableDC112000::NAMES = {{
/* 00 */ {{"map_city00_00"}},
/* 01 */ {{"map_forest01_00", "map_forest01_01", "map_forest01_02", "map_forest01_03", "map_forest01_04"}},
/* 02 */ {{"map_forest02_00", "map_forest02_01", "map_forest02_02", "map_forest02_03", "map_forest02_04"}},
@@ -1908,12 +2007,15 @@ const array<vector<vector<string>>, 0x10> SetDataTableDC112000::NAMES = {{
/* 0D */ {{"map_boss03"}},
/* 0E */ {{"map_boss04"}},
/* 0F */ {{"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}},
/* 10 */ {},
/* 11 */ {},
}};
SetDataTableDC112000::SetDataTableDC112000() : SetDataTableBase(Version::DC_V1_11_2000_PROTOTYPE) {}
pair<uint32_t, uint32_t> SetDataTableDC112000::num_available_variations_for_floor(Episode, uint8_t floor) const {
return make_pair(this->NAMES[floor].size(), this->NAMES[floor][0].size());
const auto& floor_names = this->NAMES.at(floor);
return make_pair(floor_names.size(), floor_names.empty() ? 0 : this->NAMES.at(floor)[0].size());
}
pair<uint32_t, uint32_t> SetDataTableDC112000::num_free_roam_variations_for_floor(Episode episode, bool, uint8_t floor) const {
@@ -1921,11 +2023,25 @@ pair<uint32_t, uint32_t> SetDataTableDC112000::num_free_roam_variations_for_floo
}
string SetDataTableDC112000::map_filename_for_variation(
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, bool is_enemies) const {
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, FilenameType type) const {
if (floor >= this->NAMES.size()) {
return "";
}
return this->NAMES.at(floor).at(var1).at(var2) + (is_enemies ? "e.dat" : "o.dat");
string basename = this->NAMES.at(floor).at(var1).at(var2);
switch (type) {
case FilenameType::ENEMIES:
basename += "e.dat";
break;
case FilenameType::OBJECTS:
basename += "o.dat";
break;
case FilenameType::EVENTS:
basename += ".evt";
break;
default:
throw logic_error("invalid map filename type");
}
return basename;
}
static const vector<AreaMapFileInfo> map_file_info_dc_nte = {