abstract supermap construction across entity types

This commit is contained in:
Martin Michelsen
2025-02-10 22:44:13 -08:00
parent a3428d33ae
commit 17fe80cf85
3 changed files with 220 additions and 119 deletions
+32 -15
View File
@@ -2639,6 +2639,7 @@ Action a_load_maps_test(
s->load_set_data_tables(false);
s->load_maps(false);
SuperMap::EfficiencyStats all_free_maps_eff;
for (const auto& it : s->supermaps) {
auto episode = static_cast<Episode>((it.first >> 28) & 7);
auto mode = static_cast<GameMode>((it.first >> 26) & 3);
@@ -2647,12 +2648,7 @@ Action a_load_maps_test(
uint8_t layout = (it.first >> 8) & 0xFF;
uint8_t entities = (it.first >> 0) & 0xFF;
fprintf(stderr, "FREE MAP: %08" PRIX32 " => %s %s %c floor=%02hhX layout=%02hhX entities=%02hhX\n",
it.first,
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty),
floor, layout, entities);
string filename_token;
if (save_disassembly) {
string filename = phosg::string_printf(
"supermap_%s_%s_%c_%02hhX_%02hhx_%02hhX.txt",
@@ -2662,8 +2658,21 @@ Action a_load_maps_test(
floor, layout, entities);
auto f = phosg::fopen_unique(filename, "wt");
it.second->print(f.get());
filename_token = " => " + filename;
}
auto eff = it.second->efficiency();
all_free_maps_eff += eff;
auto eff_str = eff.str();
fprintf(stderr, "FREE MAP: %08" PRIX32 " => %s %s %c floor=%02hhX layout=%02hhX entities=%02hhX => %s%s\n",
it.first,
abbreviation_for_episode(episode),
abbreviation_for_mode(mode),
abbreviation_for_difficulty(difficulty),
floor, layout, entities, eff_str.c_str(), filename_token.c_str());
}
string all_free_maps_eff_str = all_free_maps_eff.str();
fprintf(stderr, "ALL FREE MAPS: %s\n", all_free_maps_eff_str.c_str());
// Generate MapStates for a few random variations
for (size_t z = 0; z < 0x20; z++) {
@@ -2696,21 +2705,27 @@ Action a_load_maps_test(
s->load_quest_index(false);
SuperMap::EfficiencyStats all_quests_eff;
uint32_t random_seed = args.get<uint32_t>("random-seed", 0, phosg::Arguments::IntFormat::HEX);
for (const auto& it : s->default_quest_index->quests_by_number) {
auto supermap = it.second->get_supermap(random_seed);
if (!supermap) {
fprintf(stderr, "QUEST MAP: %08" PRIX32 " => (no supermap)\n", it.first);
} else {
string filename = phosg::string_printf("supermap_quest_%" PRIu32 "_%08" PRIX32 ".txt", it.first, random_seed);
fprintf(stderr, "QUEST MAP: %08" PRIX32 " => %s\n", it.first, filename.c_str());
if (save_disassembly) {
auto f = phosg::fopen_unique(filename, "wt");
fprintf(f.get(), "QUEST %" PRIu32 " (%s)\n", it.first, it.second->name.c_str());
supermap->print(f.get());
}
throw logic_error("quest does not have a supermap, even with a specified random seed");
}
string filename_token;
if (save_disassembly) {
string filename = phosg::string_printf("supermap_quest_%" PRIu32 "_%08" PRIX32 ".txt", it.first, random_seed);
auto f = phosg::fopen_unique(filename, "wt");
fprintf(f.get(), "QUEST %" PRIu32 " (%s)\n", it.first, it.second->name.c_str());
supermap->print(f.get());
filename_token = " => " + filename;
}
auto eff = supermap->efficiency();
all_quests_eff += eff;
auto eff_str = eff.str();
fprintf(stderr, "QUEST MAP: %08" PRIX32 " => %s%s\n", it.first, eff_str.c_str(), filename_token.c_str());
auto map_state = make_shared<MapState>(
0,
phosg::random_object<uint8_t>() & 3,
@@ -2727,6 +2742,8 @@ Action a_load_maps_test(
map_state->enemy_set_states.size(),
map_state->event_states.size());
}
string all_quests_eff_str = all_quests_eff.str();
fprintf(stderr, "ALL QUEST MAPS: %s\n", all_quests_eff_str.c_str());
});
Action a_parse_object_graph(
+168 -102
View File
@@ -1745,8 +1745,12 @@ string MapFile::disassemble() const {
////////////////////////////////////////////////////////////////////////////////
// Super map
string SuperMap::Object::id_str() const {
return phosg::string_printf("KS-%02hhX-%03zX", this->floor, this->super_id);
}
string SuperMap::Object::str() const {
string ret = phosg::string_printf("[Object KS-%02hhX-%03zX", this->floor, this->super_id);
string ret = "[Object " + this->id_str();
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const auto& def = this->version(v);
if (def.relative_object_index != 0xFFFF) {
@@ -1759,6 +1763,10 @@ string SuperMap::Object::str() const {
return ret;
}
string SuperMap::Enemy::id_str() const {
return phosg::string_printf("ES-%02hhX-%03zX-%03zX", this->floor, this->super_set_id, this->super_id);
}
string SuperMap::Enemy::str() const {
string ret = phosg::string_printf("[Enemy ES-%02hhX-%03zX-%03zX type=%s child_index=%hX alias_enemy_index_delta=%hX is_default_rare_v123=%s is_default_rare_bb=%s",
this->floor,
@@ -1785,8 +1793,12 @@ string SuperMap::Enemy::str() const {
return ret;
}
string SuperMap::Event::id_str() const {
return phosg::string_printf("WS-%02hhX-%03zX", this->floor, this->super_id);
}
string SuperMap::Event::str() const {
string ret = phosg::string_printf("[Event WS-%02hhX-%03zX", this->floor, this->super_id);
string ret = "[Event " + this->id_str();
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const auto& def = this->version(v);
if (def.relative_event_index != 0xFFFF) {
@@ -1864,6 +1876,10 @@ void SuperMap::link_object_version(std::shared_ptr<Object> obj, Version version,
entities.objects.emplace_back(obj);
// Add to semantic hash index
uint64_t semantic_hash = set_entry->semantic_hash();
this->objects_for_semantic_hash[semantic_hash].emplace_back(obj);
// Add to room/group index
uint64_t k = room_index_key(obj->floor, set_entry->room, set_entry->group);
entities.object_for_floor_room_and_group.emplace(k, obj);
@@ -2380,6 +2396,10 @@ void SuperMap::link_enemy_version_and_children(
entities.enemies.emplace_back(ene);
if (ene->child_index == 0) {
entities.enemy_sets.emplace_back(ene);
// Add to semantic hash index (but only for the root ene)
uint64_t semantic_hash = set_entry->semantic_hash();
this->enemy_sets_for_semantic_hash[semantic_hash].emplace_back(ene);
}
// Add to room/group index
@@ -2470,6 +2490,11 @@ void SuperMap::link_event_version(
entities.events.emplace_back(ev);
// Add to semantic hash index
uint64_t semantic_hash = entry->semantic_hash();
this->events_for_semantic_hash[semantic_hash].emplace_back(ev);
// Add to room index
uint64_t k = room_index_key(ev->floor, entry->room, entry->wave_number);
entities.event_for_floor_room_and_wave_number.emplace(k, ev);
k = (static_cast<uint64_t>(ev->floor) << 32) | entry->event_id;
@@ -2616,6 +2641,35 @@ vector<EditAction> compute_edit_path(
return reverse_path;
}
template <typename EntityT>
vector<shared_ptr<EntityT>> compute_prev_entities(
const vector<shared_ptr<EntityT>>& existing_prev_entities,
size_t prev_entities_offset,
const vector<EditAction>& edit_path) {
vector<shared_ptr<EntityT>> ret;
for (auto action : edit_path) {
switch (action) {
case EditAction::ADD:
// This object doesn't match any object from the previous version
ret.emplace_back(nullptr);
break;
case EditAction::DELETE:
// There is an object in the previous version that doesn't match any in this version; skip it
prev_entities_offset++;
break;
case EditAction::EDIT: {
// The current object in this_sf matches the current object in prev_sf; link them together
ret.emplace_back(existing_prev_entities.at(prev_entities_offset));
prev_entities_offset++;
break;
}
default:
throw logic_error("invalid edit path action");
}
}
return ret;
}
static double object_set_add_cost(const MapFile::ObjectSetEntry&) {
return 100.0;
}
@@ -2710,6 +2764,37 @@ void SuperMap::add_map_file(Version this_v, shared_ptr<const MapFile> this_map_f
}
for (uint8_t floor = 0; floor < 0x12; floor++) {
auto link_or_add_entities = [this_v, floor]<typename EntityT, typename EntryT>(
const EntryT* prev_sets,
size_t prev_set_count,
const EntryT* this_sets,
size_t this_set_count,
double (*add_cost)(const EntryT&),
double (*delete_cost)(const EntryT&),
double (*edit_cost)(const EntryT&, const EntryT& current),
const vector<EntityT>& prev_entities,
size_t prev_entities_start_index,
auto&& link_existing,
auto&& add_new) {
auto edit_path = compute_edit_path(
prev_sets, prev_set_count, this_sets, this_set_count, add_cost, delete_cost, edit_cost);
auto used_prev_entities = compute_prev_entities(prev_entities, prev_entities_start_index, edit_path);
if (used_prev_entities.size() != this_set_count) {
throw std::logic_error("incorrect previous entity list length");
}
// TODO; // Use semantic hash index to fill in the gaps
for (size_t z = 0; z < this_set_count; z++) {
auto& prev_ent = used_prev_entities[z];
if (prev_ent) {
link_existing(prev_ent, this_v, this_sets + z);
} else {
add_new(this_v, floor, this_sets + z);
}
}
};
this_entities.object_floor_start_indexes[floor] = this_entities.objects.size();
this_entities.enemy_floor_start_indexes[floor] = this_entities.enemies.size();
this_entities.enemy_set_floor_start_indexes[floor] = this_entities.enemy_sets.size();
@@ -2726,42 +2811,20 @@ void SuperMap::add_map_file(Version this_v, shared_ptr<const MapFile> this_map_f
} else if (this_sf.object_sets) {
const auto& prev_sf = prev_map_file->floor(floor);
auto edit_path = compute_edit_path(
auto& prev_entities = this->version(prev_v);
link_or_add_entities(
prev_sf.object_sets,
prev_sf.object_set_count,
this_sf.object_sets,
this_sf.object_set_count,
object_set_add_cost,
object_set_delete_cost,
object_set_edit_cost);
auto& prev_entities = this->version(prev_v);
size_t prev_entities_offset = prev_entities.object_floor_start_indexes.at(floor);
size_t this_sf_offset = 0;
for (auto action : edit_path) {
switch (action) {
case EditAction::ADD:
// This object doesn't match any object from the previous version; create a new object
this->add_object(this_v, floor, this_sf.object_sets + this_sf_offset);
this_sf_offset++;
break;
case EditAction::DELETE:
// There is an object in the previous version that doesn't match any in this version; skip it
prev_entities_offset++;
break;
case EditAction::EDIT: {
// The current object in this_sf matches the current object in prev_sf; link them together
auto prev_obj = prev_entities.objects.at(prev_entities_offset);
this->link_object_version(prev_obj, this_v, this_sf.object_sets + this_sf_offset);
prev_entities_offset++;
this_sf_offset++;
break;
}
default:
throw logic_error("invalid edit path action");
}
}
object_set_edit_cost,
prev_entities.objects,
prev_entities.object_floor_start_indexes.at(floor),
bind(&SuperMap::link_object_version, this, placeholders::_1, placeholders::_2, placeholders::_3),
bind(&SuperMap::add_object, this, placeholders::_1, placeholders::_2, placeholders::_3));
}
if (!prev_map_file || !prev_map_file->floor(floor).enemy_sets) {
@@ -2770,42 +2833,20 @@ void SuperMap::add_map_file(Version this_v, shared_ptr<const MapFile> this_map_f
}
} else if (this_sf.enemy_sets) {
const auto& prev_sf = prev_map_file->floor(floor);
auto edit_path = compute_edit_path(
auto& prev_entities = this->version(prev_v);
link_or_add_entities(
prev_sf.enemy_sets,
prev_sf.enemy_set_count,
this_sf.enemy_sets,
this_sf.enemy_set_count,
enemy_set_add_cost,
enemy_set_delete_cost,
enemy_set_edit_cost);
auto& prev_entities = this->version(prev_v);
size_t prev_entities_offset = prev_entities.enemy_set_floor_start_indexes.at(floor);
size_t this_sf_offset = 0;
for (auto action : edit_path) {
switch (action) {
case EditAction::ADD:
// This object doesn't match any object from the previous version; create a new object
this->add_enemy_and_children(this_v, floor, this_sf.enemy_sets + this_sf_offset);
this_sf_offset++;
break;
case EditAction::DELETE:
// There is an object in the previous version that doesn't match any in this version; skip it
prev_entities_offset++;
break;
case EditAction::EDIT: {
// The current object in this_sf matches the current object in prev_sf; link them together
auto prev_ene = prev_entities.enemy_sets.at(prev_entities_offset);
this->link_enemy_version_and_children(prev_ene, this_v, this_sf.enemy_sets + this_sf_offset);
prev_entities_offset++;
this_sf_offset++;
break;
}
default:
throw logic_error("invalid edit path action");
}
}
enemy_set_edit_cost,
prev_entities.enemy_sets,
prev_entities.enemy_set_floor_start_indexes.at(floor),
bind(&SuperMap::link_enemy_version_and_children, this, placeholders::_1, placeholders::_2, placeholders::_3),
bind(&SuperMap::add_enemy_and_children, this, placeholders::_1, placeholders::_2, placeholders::_3));
}
if (!prev_map_file || !prev_map_file->floor(floor).events1) {
@@ -2814,52 +2855,20 @@ void SuperMap::add_map_file(Version this_v, shared_ptr<const MapFile> this_map_f
}
} else if (this_sf.events1) {
const auto& prev_sf = prev_map_file->floor(floor);
auto edit_path = compute_edit_path(
auto& prev_entities = this->version(prev_v);
link_or_add_entities(
prev_sf.events1,
prev_sf.event_count,
this_sf.events1,
this_sf.event_count,
event_add_cost,
event_delete_cost,
event_edit_cost);
auto& prev_entities = this->version(prev_v);
size_t prev_entities_offset = prev_entities.event_floor_start_indexes.at(floor);
size_t this_sf_offset = 0;
for (auto action : edit_path) {
switch (action) {
case EditAction::ADD:
// This object doesn't match any object from the previous version; try to look it up in the semantic hash index
this->add_event(
this_v,
floor,
this_sf.events1 + this_sf_offset,
this_sf.event_action_stream,
this_sf.event_action_stream_bytes);
this_sf_offset++;
break;
case EditAction::DELETE:
// There is an object in the previous version that doesn't match any in this version; skip it
prev_entities_offset++;
break;
case EditAction::EDIT: {
// The current object in this_sf matches the current object in prev_sf; link them together
auto prev_ev = prev_entities.events.at(prev_entities_offset);
this->link_event_version(
prev_ev,
this_v,
this_sf.events1 + this_sf_offset,
this_sf.event_action_stream,
this_sf.event_action_stream_bytes);
prev_entities_offset++;
this_sf_offset++;
break;
}
default:
throw logic_error("invalid edit path action");
}
}
event_edit_cost,
prev_entities.events,
prev_entities.event_floor_start_indexes.at(floor),
bind(&SuperMap::link_event_version, this, placeholders::_1, placeholders::_2, placeholders::_3, this_sf.event_action_stream, this_sf.event_action_stream_bytes),
bind(&SuperMap::add_event, this, placeholders::_1, placeholders::_2, placeholders::_3, this_sf.event_action_stream, this_sf.event_action_stream_bytes));
}
}
}
@@ -2970,6 +2979,63 @@ vector<shared_ptr<const SuperMap::Event>> SuperMap::events_for_floor_room_wave(
return ret;
}
SuperMap::EfficiencyStats& SuperMap::EfficiencyStats::operator+=(const EfficiencyStats& other) {
this->filled_object_slots += other.filled_object_slots;
this->total_object_slots += other.total_object_slots;
this->filled_enemy_set_slots += other.filled_enemy_set_slots;
this->total_enemy_set_slots += other.total_enemy_set_slots;
this->filled_event_slots += other.filled_event_slots;
this->total_event_slots += other.total_event_slots;
return *this;
}
std::string SuperMap::EfficiencyStats::str() const {
return phosg::string_printf(
"EfficiencyStats[K = %zu/%zu (%lg%%), E = %zu/%zu (%lg%%), W = %zu/%zu (%g%%)]",
this->filled_object_slots, this->total_object_slots,
static_cast<double>(this->filled_object_slots * 100) / static_cast<double>(this->total_object_slots),
this->filled_enemy_set_slots, this->total_enemy_set_slots,
static_cast<double>(this->filled_enemy_set_slots * 100) / static_cast<double>(this->total_enemy_set_slots),
this->filled_event_slots, this->total_event_slots,
static_cast<double>(this->filled_event_slots * 100) / static_cast<double>(this->total_event_slots));
}
SuperMap::EfficiencyStats SuperMap::efficiency() const {
EfficiencyStats ret;
for (const auto& obj : this->objects) {
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const auto& obj_ver = obj->version(v);
if (obj_ver.relative_object_index != 0xFFFF) {
ret.filled_object_slots++;
}
}
}
ret.total_object_slots = this->objects.size() * ALL_ARPG_SEMANTIC_VERSIONS.size();
for (const auto& ene : this->enemy_sets) {
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const auto& ene_ver = ene->version(v);
if (ene_ver.relative_enemy_index != 0xFFFF) {
ret.filled_enemy_set_slots++;
}
}
}
ret.total_enemy_set_slots = this->enemy_sets.size() * ALL_ARPG_SEMANTIC_VERSIONS.size();
for (const auto& ev : this->events) {
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const auto& ev_ver = ev->version(v);
if (ev_ver.relative_event_index != 0xFFFF) {
ret.filled_event_slots++;
}
}
}
ret.total_event_slots = this->events.size() * ALL_ARPG_SEMANTIC_VERSIONS.size();
return ret;
}
void SuperMap::verify() const {
for (size_t super_id = 0; super_id < this->objects.size(); super_id++) {
if (this->objects[super_id]->super_id != super_id) {
+20 -2
View File
@@ -446,7 +446,8 @@ protected:
// responsible for all entities on all floors in a quest, or all entities on a
// single floor in free play. Each entity is assigned a "super ID", which
// uniquely identifies the entity on all PSO versions. (These are the IDs which
// newserv formats as K-XXX, E-XXX, and W-XXX.)
// newserv formats as K-XXX, E-XXX, and W-XXX, though they are offset as needed
// for floors beyond the first.)
// There must not be any random enemy sections in any MapFile passed to
// SuperMap; to resolve them, materialize_random_sections must be called on all
// MapFiles first. This generally only is of concern in Challenge mode.
@@ -469,6 +470,7 @@ public:
return this->def_for_version.at(static_cast<size_t>(v));
}
std::string id_str() const;
std::string str() const;
};
@@ -495,6 +497,7 @@ public:
return this->def_for_version.at(static_cast<size_t>(v));
}
std::string id_str() const;
std::string str() const;
};
@@ -516,6 +519,7 @@ public:
return this->def_for_version.at(static_cast<size_t>(v));
}
std::string id_str() const;
std::string str() const;
};
@@ -584,8 +588,19 @@ public:
std::vector<std::shared_ptr<const Event>> events_for_floor_room_wave(
Version version, uint8_t floor, uint16_t room, uint16_t wave_number) const;
void verify() const;
struct EfficiencyStats {
size_t filled_object_slots = 0;
size_t total_object_slots = 0;
size_t filled_enemy_set_slots = 0;
size_t total_enemy_set_slots = 0;
size_t filled_event_slots = 0;
size_t total_event_slots = 0;
EfficiencyStats& operator+=(const EfficiencyStats& other);
std::string str() const;
};
EfficiencyStats efficiency() const;
void verify() const;
void print(FILE* stream) const;
protected:
@@ -597,6 +612,9 @@ protected:
std::vector<std::shared_ptr<Enemy>> enemies;
std::vector<std::shared_ptr<Enemy>> enemy_sets;
std::vector<std::shared_ptr<Event>> events;
std::unordered_map<uint64_t, std::vector<std::shared_ptr<Object>>> objects_for_semantic_hash;
std::unordered_map<uint64_t, std::vector<std::shared_ptr<Enemy>>> enemy_sets_for_semantic_hash;
std::unordered_map<uint64_t, std::vector<std::shared_ptr<Event>>> events_for_semantic_hash;
std::array<EntitiesForVersion, NUM_VERSIONS> entities_for_version;
std::shared_ptr<Object> add_object(Version version, uint8_t floor, const MapFile::ObjectSetEntry* set_entry);