From 17fe80cf85628994bda9c81595789abd4ebe8345 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Mon, 10 Feb 2025 22:44:13 -0800 Subject: [PATCH] abstract supermap construction across entity types --- src/Main.cc | 47 ++++++--- src/Map.cc | 270 ++++++++++++++++++++++++++++++++-------------------- src/Map.hh | 22 ++++- 3 files changed, 220 insertions(+), 119 deletions(-) diff --git a/src/Main.cc b/src/Main.cc index eb9a659c..db56ae48 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -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((it.first >> 28) & 7); auto mode = static_cast((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("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( 0, phosg::random_object() & 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( diff --git a/src/Map.cc b/src/Map.cc index c61608a4..a7ae8466 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -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 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(ev->floor) << 32) | entry->event_id; @@ -2616,6 +2641,35 @@ vector compute_edit_path( return reverse_path; } +template +vector> compute_prev_entities( + const vector>& existing_prev_entities, + size_t prev_entities_offset, + const vector& edit_path) { + vector> 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 this_map_f } for (uint8_t floor = 0; floor < 0x12; floor++) { + auto link_or_add_entities = [this_v, floor]( + 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& 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 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 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 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> 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(this->filled_object_slots * 100) / static_cast(this->total_object_slots), + this->filled_enemy_set_slots, this->total_enemy_set_slots, + static_cast(this->filled_enemy_set_slots * 100) / static_cast(this->total_enemy_set_slots), + this->filled_event_slots, this->total_event_slots, + static_cast(this->filled_event_slots * 100) / static_cast(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) { diff --git a/src/Map.hh b/src/Map.hh index ba3fd49b..8daa11ca 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -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(v)); } + std::string id_str() const; std::string str() const; }; @@ -495,6 +497,7 @@ public: return this->def_for_version.at(static_cast(v)); } + std::string id_str() const; std::string str() const; }; @@ -516,6 +519,7 @@ public: return this->def_for_version.at(static_cast(v)); } + std::string id_str() const; std::string str() const; }; @@ -584,8 +588,19 @@ public: std::vector> 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> enemies; std::vector> enemy_sets; std::vector> events; + std::unordered_map>> objects_for_semantic_hash; + std::unordered_map>> enemy_sets_for_semantic_hash; + std::unordered_map>> events_for_semantic_hash; std::array entities_for_version; std::shared_ptr add_object(Version version, uint8_t floor, const MapFile::ObjectSetEntry* set_entry);