From c482324a9715141c226c9c855842027ca35f6a8a Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 30 Nov 2025 23:12:55 -0800 Subject: [PATCH] use area instead of floor during map construction --- src/ChatCommands.cc | 3 +- src/EnemyType.cc | 37 +++++---- src/EnemyType.hh | 2 +- src/Lobby.cc | 2 +- src/Map.cc | 120 +++++++++++++++------------- src/Map.hh | 28 ++++--- src/ProxyCommands.cc | 65 +++++++++------ src/ProxySession.hh | 8 +- src/Quest.cc | 2 +- src/QuestMetadata.cc | 2 +- src/QuestMetadata.hh | 8 ++ src/ReceiveSubcommands.cc | 163 +++++++++++++++++++++----------------- src/ReceiveSubcommands.hh | 1 - src/ServerState.cc | 2 +- 14 files changed, 250 insertions(+), 193 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 78a3c50f..1e019b70 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -3003,8 +3003,9 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en if (nearest_ene) { const auto* set_entry = nearest_ene->super_ene->version(a.c->version()).set_entry; string type_name = MapFile::name_for_enemy_type(set_entry->base_type, a.c->version(), area); + uint8_t area = l->area_for_floor(a.c->version(), a.c->floor); send_text_message_fmt(a.c, "$C5E-{:03X}\n$C6{}\n$C2{}\n$C7X:{:.2f} Z:{:.2f}", - nearest_ene->e_id, phosg::name_for_enum(nearest_ene->type(a.c->version(), l->episode, l->difficulty, l->event)), + nearest_ene->e_id, phosg::name_for_enum(nearest_ene->type(a.c->version(), area, l->difficulty, l->event)), type_name, nearest_worldspace_pos.x, nearest_worldspace_pos.z); auto set_str = set_entry->str(a.c->version(), area); a.c->log.info_f("Enemy found via $whatobj: E-{:03X} {} at x={:g} y={:g} z={:g}", diff --git a/src/EnemyType.cc b/src/EnemyType.cc index d71a2de9..d2ad3a05 100644 --- a/src/EnemyType.cc +++ b/src/EnemyType.cc @@ -226,29 +226,28 @@ const vector& enemy_types_for_battle_param_index(Episode episode, uin } } -EnemyType EnemyTypeDefinition::rare_type(Episode episode, uint8_t event, uint8_t floor) const { +EnemyType EnemyTypeDefinition::rare_type(uint8_t area, uint8_t event) const { switch (this->type) { case EnemyType::HILDEBEAR: return EnemyType::HILDEBLUE; case EnemyType::RAG_RAPPY: - switch (episode) { - case Episode::EP1: - return EnemyType::AL_RAPPY; - case Episode::EP2: - switch (event) { - case 0x01: // rappy_type 1 - return EnemyType::SAINT_RAPPY; - case 0x04: // rappy_type 2 - return EnemyType::EGG_RAPPY; - case 0x05: // rappy_type 3 - return EnemyType::HALLO_RAPPY; - default: - return EnemyType::LOVE_RAPPY; - } - case Episode::EP4: - return (floor > 0x05) ? EnemyType::DEL_RAPPY_DESERT : EnemyType::DEL_RAPPY_CRATER; - default: - throw logic_error("invalid episode"); + if (area < 0x12) { + return EnemyType::AL_RAPPY; + } else if (area < 0x24) { + switch (event) { + case 0x01: // rappy_type 1 + return EnemyType::SAINT_RAPPY; + case 0x04: // rappy_type 2 + return EnemyType::EGG_RAPPY; + case 0x05: // rappy_type 3 + return EnemyType::HALLO_RAPPY; + default: + return EnemyType::LOVE_RAPPY; + } + } else if (area <= 0x28) { + return EnemyType::DEL_RAPPY_CRATER; + } else { + return EnemyType::DEL_RAPPY_DESERT; } case EnemyType::POISON_LILY: return EnemyType::NAR_LILY; diff --git a/src/EnemyType.hh b/src/EnemyType.hh index 96400b07..c53ea70d 100644 --- a/src/EnemyType.hh +++ b/src/EnemyType.hh @@ -174,7 +174,7 @@ struct EnemyTypeDefinition { inline bool is_boss() const { return (this->flags & Flag::IS_BOSS); } - EnemyType rare_type(Episode episode, uint8_t event, uint8_t floor) const; + EnemyType rare_type(uint8_t area, uint8_t event) const; }; const EnemyTypeDefinition& type_definition_for_enemy(EnemyType type); diff --git a/src/Lobby.cc b/src/Lobby.cc index 40a57bd3..342bcea2 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -174,7 +174,7 @@ uint8_t Lobby::area_for_floor(Version version, uint8_t floor) const { return this->quest->meta.floor_assignments.at(floor).area; } auto sdt = this->require_server_state()->set_data_table(version, this->episode, this->mode, this->difficulty); - return sdt->default_area_for_floor(this->episode, floor); + return sdt->default_floor_to_area(this->episode).at(floor); } shared_ptr Lobby::require_server_state() const { diff --git a/src/Map.cc b/src/Map.cc index 022133af..ef417283 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -67,7 +67,7 @@ vector SetDataTableBase::map_filenames_for_variations( return ret; } -uint8_t SetDataTableBase::default_area_for_floor(Version version, Episode episode, uint8_t floor) { +std::array SetDataTableBase::default_floor_to_area(Version version, Episode episode) { // For some inscrutable reason, Pioneer 2's area number in Episode 4 is // discontiguous with all the rest. Why, Sega?? static const array areas_ep1 = { @@ -76,24 +76,26 @@ uint8_t SetDataTableBase::default_area_for_floor(Version version, Episode episod 0x00, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0xFF, 0xFF}; static const array areas_ep2 = { 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23}; + static const array areas_ep3 = { + 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF}; static const array areas_ep4 = { 0x2D, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; switch (episode) { case Episode::EP1: - return areas_ep1.at(floor); - case Episode::EP2: { - const auto& areas = ((version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2); - return areas.at(floor); - } + return areas_ep1; + case Episode::EP2: + return (version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2; + case Episode::EP3: + return areas_ep3; case Episode::EP4: - return areas_ep4.at(floor); + return areas_ep4; default: throw logic_error("incorrect episode"); } } -uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const { - return this->default_area_for_floor(this->version, episode, floor); +std::array SetDataTableBase::default_floor_to_area(Episode episode) const { + return this->default_floor_to_area(this->version, episode); } SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBase(version) { @@ -138,7 +140,7 @@ void SetDataTable::load_table_t(const string& data) { } Variations::Entry SetDataTable::num_available_variations_for_floor(Episode episode, uint8_t floor) const { - uint8_t area = this->default_area_for_floor(episode, floor); + uint8_t area = this->default_floor_to_area(episode).at(floor); if (area == 0xFF) { return Variations::Entry{.layout = 1, .entities = 1}; } else { @@ -151,7 +153,7 @@ Variations::Entry SetDataTable::num_available_variations_for_floor(Episode episo } Variations::Entry SetDataTable::num_free_play_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const { - uint8_t area = this->default_area_for_floor(episode, floor); + uint8_t area = this->default_floor_to_area(episode).at(floor); if (area == 0xFF) { return Variations::Entry{.layout = 1, .entities = 1}; } @@ -188,7 +190,7 @@ Variations::Entry SetDataTable::num_free_play_variations_for_floor(Episode episo string SetDataTable::map_filename_for_variation( Episode episode, GameMode mode, uint8_t floor, uint32_t layout, uint32_t entities, FilenameType type) const { - uint8_t area = this->default_area_for_floor(episode, floor); + uint8_t area = this->default_floor_to_area(episode).at(floor); if (area == 0xFF) { return ""; } @@ -4365,9 +4367,11 @@ string SuperMap::Event::str() const { return ret; } -SuperMap::SuperMap(Episode episode, const std::array, NUM_VERSIONS>& map_files) +SuperMap::SuperMap( + const std::array, NUM_VERSIONS>& map_files, + const std::array& floor_to_area) : log("[SuperMap] "), - episode(episode) { + floor_to_area(floor_to_area) { for (const auto& map_file : map_files) { if (!map_file) { continue; @@ -4400,9 +4404,7 @@ static uint64_t room_index_key(uint8_t floor, uint16_t room, uint16_t wave_numbe } shared_ptr SuperMap::add_object( - Version version, - uint8_t floor, - const MapFile::ObjectSetEntry* set_entry) { + Version version, uint8_t floor, const MapFile::ObjectSetEntry* set_entry) { auto obj = make_shared(); obj->super_id = this->objects.size(); obj->floor = floor; @@ -4591,19 +4593,13 @@ shared_ptr SuperMap::add_enemy_and_children( break; } case 0x0041: { // TObjEneLappy - bool is_rare_v123 = (set_entry->param6 != 0); - bool is_rare_bb = (set_entry->param6 & 1); - switch (this->episode) { - case Episode::EP1: - case Episode::EP2: - add(EnemyType::RAG_RAPPY, is_rare_v123, is_rare_bb); - break; - case Episode::EP4: - add((floor > 0x05) ? EnemyType::SAND_RAPPY_DESERT : EnemyType::SAND_RAPPY_CRATER, is_rare_v123, is_rare_bb); - break; - default: - throw logic_error("invalid episode"); - } + uint8_t area = this->area_for_floor(floor); + EnemyType type = (area < 0x24) + ? EnemyType::RAG_RAPPY + : (area <= 0x28) + ? EnemyType::SAND_RAPPY_CRATER + : EnemyType::SAND_RAPPY_DESERT; + add(type, (set_entry->param6 != 0), (set_entry->param6 & 1)); break; } case 0x0042: // TObjEneBm3FlyNest @@ -4622,9 +4618,10 @@ shared_ptr SuperMap::add_enemy_and_children( case 0x0060: // TObjGrass add(EnemyType::GRASS_ASSASSIN); break; - case 0x0061: // TObjEneRe2Flower - add(((episode == Episode::EP2) && (floor == 0x11)) ? EnemyType::DEL_LILY : EnemyType::POISON_LILY); + case 0x0061: { // TObjEneRe2Flower + add((this->area_for_floor(floor) == 0x23) ? EnemyType::DEL_LILY : EnemyType::POISON_LILY); break; + } case 0x0062: // TObjEneNanoDrago add(EnemyType::NANO_DRAGON); break; @@ -4711,15 +4708,17 @@ shared_ptr SuperMap::add_enemy_and_children( case 0x00A8: // Unnamed subclass of TObjEneBalClawClaw add(EnemyType::CLAW); break; - case 0x00C0: // TBoss1Dragon or TBoss5Gryphon - if (episode == Episode::EP1) { + case 0x00C0: { // TBoss1Dragon or TBoss5Gryphon + uint8_t area = this->area_for_floor(floor); + if (area < 0x12) { add(EnemyType::DRAGON); - } else if (episode == Episode::EP2) { + } else if (area < 0x24) { add(EnemyType::GAL_GRYPHON); } else { - throw runtime_error("DRAGON placed outside of Episode 1 or 2"); + throw std::runtime_error("DRAGON placed outside of Episode 1 or 2"); } break; + } case 0x00C1: // TBoss2DeRolLe if ((set_entry->num_children != 0) && (set_entry->num_children != 0x13)) { this->log.warning_f("DE_ROL_LE has an unusual num_children (0x{:X})", set_entry->num_children); @@ -4787,7 +4786,7 @@ shared_ptr SuperMap::add_enemy_and_children( default_num_children = 5; break; case 0x00D4: // TObjEneMe3StelthReal - if (this->episode == Episode::EP3) { + if (this->area_for_floor(floor) == 0xFF) { // Ep3 add(EnemyType::NON_ENEMY_NPC); } else { add((set_entry->param6 > 0) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL); @@ -4795,14 +4794,14 @@ shared_ptr SuperMap::add_enemy_and_children( } break; case 0x00D5: // TObjEneMerillLia - if (this->episode == Episode::EP3) { + if (this->area_for_floor(floor) == 0xFF) { // Ep3 add(EnemyType::NON_ENEMY_NPC); } else { add((set_entry->param6 > 0) ? EnemyType::MERILTAS : EnemyType::MERILLIA); } break; case 0x00D6: // TObjEneBm9Mericarol - if (this->episode == Episode::EP3) { + if (this->area_for_floor(floor) == 0xFF) { // Ep3 add(EnemyType::NON_ENEMY_NPC); } else { switch (set_entry->param6) { @@ -4821,7 +4820,7 @@ shared_ptr SuperMap::add_enemy_and_children( } break; case 0x00D7: // TObjEneBm5GibonU - if (this->episode == Episode::EP3) { + if (this->area_for_floor(floor) == 0xFF) { // Ep3 add(EnemyType::NON_ENEMY_NPC); } else { add((set_entry->param6 > 0) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); @@ -4852,8 +4851,9 @@ shared_ptr SuperMap::add_enemy_and_children( add(EnemyType::RECOBOX); child_type = EnemyType::RECON; break; - case 0x00E0: // TObjEneMe3SinowZoaReal or TObjEneEpsilonBody - if ((episode == Episode::EP2) && (floor > 0x0F)) { + case 0x00E0: { // TObjEneMe3SinowZoaReal or TObjEneEpsilonBody + uint8_t area = this->area_for_floor(floor); + if ((area == 0x22) || (area == 0x23)) { add(EnemyType::EPSILON); default_num_children = 4; child_type = EnemyType::EPSIGARD; @@ -4861,27 +4861,30 @@ shared_ptr SuperMap::add_enemy_and_children( add((set_entry->param6 > 0) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); } break; + } case 0x00E1: // TObjEneIllGill add(EnemyType::ILL_GILL); break; case 0x0110: - if (this->episode == Episode::EP3) { + if (this->area_for_floor(floor) == 0xFF) { // Ep3 add(EnemyType::NON_ENEMY_NPC); } else { add(EnemyType::ASTARK); } break; - case 0x0111: - if (this->episode == Episode::EP3) { + case 0x0111: { + uint8_t area = this->area_for_floor(floor); + if (area == 0xFF) { // Ep3 add(EnemyType::NON_ENEMY_NPC); - } else if (floor > 0x05) { - add(set_entry->param2 ? EnemyType::YOWIE_DESERT : EnemyType::SATELLITE_LIZARD_DESERT); - } else { + } else if (area <= 0x28) { add(set_entry->param2 ? EnemyType::YOWIE_CRATER : EnemyType::SATELLITE_LIZARD_CRATER); + } else { + add(set_entry->param2 ? EnemyType::YOWIE_DESERT : EnemyType::SATELLITE_LIZARD_DESERT); } break; + } case 0x0112: - if (this->episode == Episode::EP3) { + if (this->area_for_floor(floor) == 0xFF) { // Ep3 add(EnemyType::NON_ENEMY_NPC); } else { bool is_rare = (set_entry->param6 & 1); @@ -4893,7 +4896,7 @@ shared_ptr SuperMap::add_enemy_and_children( break; case 0x0114: { bool is_rare = (set_entry->param6 & 1); - add((floor > 0x05) ? EnemyType::ZU_DESERT : EnemyType::ZU_CRATER, is_rare, is_rare); + add((this->area_for_floor(floor) <= 0x28) ? EnemyType::ZU_CRATER : EnemyType::ZU_DESERT, is_rare, is_rare); break; } case 0x0115: { @@ -5804,7 +5807,8 @@ void SuperMap::verify() const { } void SuperMap::print(FILE* stream) const { - phosg::fwrite_fmt(stream, "SuperMap {} random={:08X}\n", name_for_episode(this->episode), this->random_seed); + phosg::fwrite_fmt(stream, "SuperMap areas=[{}] random={:08X}\n", + phosg::format_data_string(&this->floor_to_area, sizeof(this->floor_to_area)), this->random_seed); phosg::fwrite_fmt(stream, " DCTE DCPR DCV1 DCV2 PCTE PCV2 GCTE GCV3 E3TE GCE3 XBV3 BBV4\n"); phosg::fwrite_fmt(stream, " MAP "); @@ -6066,6 +6070,11 @@ MapState::MapState( random_seed(random_seed), bb_rare_rates(bb_rare_rates) { + if (floor_map_defs.empty()) { + throw std::runtime_error("cannot construct a MapState with no floor maps"); + } + this->floor_to_area = floor_map_defs[0]->floor_to_area; + this->floor_config_entries.resize(0x12); for (size_t floor = 0; floor < this->floor_config_entries.size(); floor++) { auto& this_fc = this->floor_config_entries[floor]; @@ -6110,7 +6119,8 @@ MapState::MapState( std::shared_ptr bb_rare_rates, std::shared_ptr rand_crypt, std::shared_ptr quest_map_def) - : log(std::format("[MapState(free):{:08X}] ", lobby_or_session_id), lobby_log.min_level), + : log(std::format("[MapState(quest):{:08X}] ", lobby_or_session_id), lobby_log.min_level), + floor_to_area(quest_map_def->floor_to_area), difficulty(difficulty), event(event), random_seed(random_seed), @@ -6142,6 +6152,9 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrfloor_to_area != this->floor_to_area) { + throw runtime_error("supermaps have different floor configs"); + } for (const auto& obj : fc.super_map->all_objects()) { auto& obj_st = this->object_states.emplace_back(make_shared()); @@ -6183,7 +6196,8 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrtype; } - auto rare_type = type_definition_for_enemy(type).rare_type(fc.super_map->get_episode(), this->event, ene->floor); + uint8_t area = fc.super_map->area_for_floor(ene->floor); + auto rare_type = type_definition_for_enemy(type).rare_type(area, this->event); if ((type == EnemyType::MERICARAND) || (rare_type != type)) { unordered_map det_cache; uint32_t bb_rare_rate = this->bb_rare_rates->get(type); diff --git a/src/Map.hh b/src/Map.hh index 8f174ae2..a9ce457d 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -67,8 +67,8 @@ public: std::vector map_filenames_for_variations( Episode episode, GameMode mode, const Variations& variations, FilenameType type) const; - static uint8_t default_area_for_floor(Version version, Episode episode, uint8_t floor); - uint8_t default_area_for_floor(Episode episode, uint8_t floor) const; + static std::array default_floor_to_area(Version version, Episode episode); + std::array default_floor_to_area(Episode episode) const; protected: explicit SetDataTableBase(Version version); @@ -602,7 +602,9 @@ public: std::multimap> event_for_floor_and_event_id; }; - SuperMap(Episode episode, const std::array, NUM_VERSIONS>& map_files); + SuperMap( + const std::array, NUM_VERSIONS>& map_files, + const std::array& floor_to_area); ~SuperMap() = default; inline EntitiesForVersion& version(Version v) { @@ -612,9 +614,6 @@ public: return this->entities_for_version.at(static_cast(v)); } - inline Episode get_episode() const { - return this->episode; - } inline int64_t get_random_seed() const { return this->random_seed; } @@ -648,6 +647,10 @@ public: std::unordered_map count_enemy_sets_for_version(Version version) const; + uint8_t area_for_floor(uint8_t floor) const { + return this->floor_to_area.at(floor); + } + struct EfficiencyStats { size_t filled_object_slots = 0; size_t total_object_slots = 0; @@ -664,9 +667,11 @@ public: void print(FILE* stream) const; protected: + friend class MapState; + phosg::PrefixedLogger log; - Episode episode; + std::array floor_to_area = {}; int64_t random_seed = -1; std::vector> objects; std::vector> enemies; @@ -797,7 +802,7 @@ public: inline void set_mericarand_variant_flag(Version version) { this->mericarand_variant_flags |= (1 << static_cast(version)); } - inline EnemyType type(Version version, Episode episode, Difficulty difficulty, uint8_t event) const { + inline EnemyType type(Version version, uint8_t area, Difficulty difficulty, uint8_t event) const { if (this->super_ene->type == EnemyType::MERICARAND) { if (this->is_rare(version)) { return ((this->mericarand_variant_flags >> static_cast(version)) & 1) @@ -810,10 +815,10 @@ public: return ((difficulty == Difficulty::NORMAL) && !this->super_ene->alias_target_ene) ? EnemyType::DARK_FALZ_2 : EnemyType::DARK_FALZ_3; + } else if (this->is_rare(version)) { + return type_definition_for_enemy(this->super_ene->type).rare_type(area, event); } else { - return this->is_rare(version) - ? type_definition_for_enemy(this->super_ene->type).rare_type(episode, event, this->super_ene->floor) - : this->super_ene->type; + return this->super_ene->type; } } inline bool ever_hit_by_client_id(uint8_t client_id) const { @@ -929,6 +934,7 @@ public: }; phosg::PrefixedLogger log; + std::array floor_to_area = {}; std::vector floor_config_entries; Difficulty difficulty = Difficulty::NORMAL; uint8_t event = 0; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index b6e444ee..b11b205c 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -872,13 +872,7 @@ static asio::awaitable SC_6x60_6xA2(shared_ptr c, Channel G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data.data(), msg.data.size()); auto rec = reconcile_drop_request_with_map( - c, - cmd, - c->proxy_session->lobby_episode, - c->proxy_session->lobby_difficulty, - c->proxy_session->lobby_event, - c->proxy_session->map_state, - false); + c, cmd, c->proxy_session->lobby_difficulty, c->proxy_session->lobby_event, c->proxy_session->map_state, false); ItemCreator::DropResult res; if (rec.obj_st) { @@ -1342,28 +1336,47 @@ static asio::awaitable S_13_A7(shared_ptr c, Channel::Mes c->log.info_f("Download complete for file {}", sf->basename); } - if (!sf->is_download && sf->basename.ends_with(".dat")) { - auto quest_dat_data = make_shared(prs_decompress(sf->data)); - try { - auto map_file = make_shared(quest_dat_data); - auto materialized_map_file = map_file->materialize_random_sections(c->proxy_session->lobby_random_seed); + if (!sf->is_download) { + if (sf->basename.ends_with(".bin")) { + c->proxy_session->last_bin_contents = make_shared(prs_decompress(sf->data)); + } else if (sf->basename.ends_with(".dat")) { + c->proxy_session->last_dat_contents = make_shared(prs_decompress(sf->data)); + } - array, NUM_VERSIONS> map_files; - map_files.at(static_cast(c->version())) = materialized_map_file; - auto supermap = make_shared(c->proxy_session->lobby_episode, map_files); + if (c->proxy_session->last_bin_contents && c->proxy_session->last_dat_contents) { + try { + QuestMetadata meta; + populate_quest_metadata_from_script( + meta, + c->proxy_session->last_bin_contents->data(), + c->proxy_session->last_bin_contents->size(), + c->version(), + c->language()); - c->proxy_session->map_state = make_shared( - c->id, - c->proxy_session->lobby_difficulty, - c->proxy_session->lobby_event, - c->proxy_session->lobby_random_seed, - MapState::DEFAULT_RARE_ENEMIES, - make_shared(c->proxy_session->lobby_random_seed), - supermap); + auto map_file = make_shared(c->proxy_session->last_dat_contents); + auto materialized_map_file = map_file->materialize_random_sections(c->proxy_session->lobby_random_seed); - } catch (const exception& e) { - c->log.warning_f("Failed to load quest map: {}", e.what()); - c->proxy_session->map_state.reset(); + array, NUM_VERSIONS> map_files; + map_files.at(static_cast(c->version())) = materialized_map_file; + auto supermap = make_shared(map_files, meta.get_floor_to_area()); + supermap->print(stderr); // NOCOMMIT + + c->proxy_session->map_state = make_shared( + c->id, + c->proxy_session->lobby_difficulty, + c->proxy_session->lobby_event, + c->proxy_session->lobby_random_seed, + MapState::DEFAULT_RARE_ENEMIES, + make_shared(c->proxy_session->lobby_random_seed), + supermap); + + } catch (const exception& e) { + c->log.warning_f("Failed to load quest map: {}", e.what()); + c->proxy_session->map_state.reset(); + } + + c->proxy_session->last_bin_contents.reset(); + c->proxy_session->last_dat_contents.reset(); } } diff --git a/src/ProxySession.hh b/src/ProxySession.hh index 57df9e18..854940cb 100644 --- a/src/ProxySession.hh +++ b/src/ProxySession.hh @@ -53,9 +53,11 @@ struct ProxySession { std::shared_ptr quest_dat_data; std::shared_ptr item_creator; std::shared_ptr map_state; - // TODO: Be less lazy and track item IDs correctly in proxy games. (Then - // change this to use the actual client's next item ID, not this hardcoded - // default.) + std::shared_ptr last_bin_contents; + std::shared_ptr last_dat_contents; + // Note: We intentionally don't use the client's item ID space because the + // client may create items at the same time as the proxy, so server/client + // state could go out of sync uint32_t next_item_id = 0x44000000; struct PersistentConfig { diff --git a/src/Quest.cc b/src/Quest.cc index cd356150..655c7b48 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -389,7 +389,7 @@ std::shared_ptr Quest::get_supermap(int64_t random_seed) const { return nullptr; } - auto supermap = make_shared(this->meta.episode, map_files); + auto supermap = make_shared(map_files, this->meta.get_floor_to_area()); if (save_to_cache) { this->supermap = supermap; } diff --git a/src/QuestMetadata.cc b/src/QuestMetadata.cc index d4021bf9..9c044e8f 100644 --- a/src/QuestMetadata.cc +++ b/src/QuestMetadata.cc @@ -68,7 +68,7 @@ void QuestMetadata::assign_default_floors() { for (size_t z = 0; z < 0x12; z++) { auto& fa = this->floor_assignments[z]; fa.floor = z; - fa.area = SetDataTableBase::default_area_for_floor(this->version, this->episode, z); + fa.area = SetDataTableBase::default_floor_to_area(this->version, this->episode)[z]; fa.type = 0; fa.layout_var = 0; fa.entities_var = 0; diff --git a/src/QuestMetadata.hh b/src/QuestMetadata.hh index b8e27314..b4bd9abd 100644 --- a/src/QuestMetadata.hh +++ b/src/QuestMetadata.hh @@ -116,6 +116,14 @@ struct QuestMetadata { void apply_json_overrides(const phosg::JSON& json); void assign_default_floors(); + inline std::array get_floor_to_area() const { + std::array ret; + for (size_t z = 0; z < 0x12; z++) { + ret[z] = this->floor_assignments[z].area; + } + return ret; + } + void assert_compatible(const QuestMetadata& other) const; phosg::JSON json() const; std::string areas_str() const; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index cce2854e..b4910e60 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2830,7 +2830,6 @@ G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, siz DropReconcileResult reconcile_drop_request_with_map( shared_ptr c, G_SpecializableItemDropRequest_6xA2& cmd, - Episode episode, Difficulty difficulty, uint8_t event, shared_ptr map, @@ -2842,76 +2841,78 @@ DropReconcileResult reconcile_drop_request_with_map( res.effective_rt_index = 0xFF; res.should_drop = true; res.ignore_def = (cmd.ignore_def != 0); + if (!map) { + return res; + } - if (map) { - if (is_box) { - res.obj_st = map->object_state_for_index(version, cmd.floor, cmd.entity_index); - if (!res.obj_st->super_obj) { - throw std::runtime_error("referenced object from drop request is a player trap"); - } - const auto* set_entry = res.obj_st->super_obj->version(version).set_entry; - if (!set_entry) { - throw std::runtime_error("object set entry is missing"); - } + if (is_box) { + res.obj_st = map->object_state_for_index(version, cmd.floor, cmd.entity_index); + if (!res.obj_st->super_obj) { + throw std::runtime_error("referenced object from drop request is a player trap"); + } + const auto* set_entry = res.obj_st->super_obj->version(version).set_entry; + if (!set_entry) { + throw std::runtime_error("object set entry is missing"); + } + string type_name = MapFile::name_for_object_type(set_entry->base_type, version); + c->log.info_f("Drop check for K-{:03X} {} {}", + res.obj_st->k_id, + res.ignore_def ? 'G' : 'S', + type_name); + if (cmd.floor != res.obj_st->super_obj->floor) { + c->log.warning_f("Floor {:02X} from command does not match object\'s expected floor {:02X}", + cmd.floor, res.obj_st->super_obj->floor); + } + if (is_v1_or_v2(version) && (version != Version::GC_NTE)) { + // V1 and V2 don't have 6xA2, so we can't get ignore_def or the object + // parameters from the client on those versions + cmd.param3 = set_entry->param3; + cmd.param4 = set_entry->param4; + cmd.param5 = set_entry->param5; + cmd.param6 = set_entry->param6; + } + bool object_ignore_def = (set_entry->param1 > 0.0); + if (res.ignore_def != object_ignore_def) { + c->log.warning_f("ignore_def value {} from command does not match object\'s expected ignore_def {} (from p1={:g})", + res.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", set_entry->param1); + } + if (c->check_flag(Client::Flag::DEBUG_ENABLED)) { string type_name = MapFile::name_for_object_type(set_entry->base_type, version); - c->log.info_f("Drop check for K-{:03X} {} {}", - res.obj_st->k_id, - res.ignore_def ? 'G' : 'S', - type_name); - if (cmd.floor != res.obj_st->super_obj->floor) { - c->log.warning_f("Floor {:02X} from command does not match object\'s expected floor {:02X}", - cmd.floor, res.obj_st->super_obj->floor); - } - if (is_v1_or_v2(version) && (version != Version::GC_NTE)) { - // V1 and V2 don't have 6xA2, so we can't get ignore_def or the object - // parameters from the client on those versions - cmd.param3 = set_entry->param3; - cmd.param4 = set_entry->param4; - cmd.param5 = set_entry->param5; - cmd.param6 = set_entry->param6; - } - bool object_ignore_def = (set_entry->param1 > 0.0); - if (res.ignore_def != object_ignore_def) { - c->log.warning_f("ignore_def value {} from command does not match object\'s expected ignore_def {} (from p1={:g})", - res.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", set_entry->param1); - } - if (c->check_flag(Client::Flag::DEBUG_ENABLED)) { - string type_name = MapFile::name_for_object_type(set_entry->base_type, version); - send_text_message_fmt(c, "$C5K-{:03X} {} {}", res.obj_st->k_id, res.ignore_def ? 'G' : 'S', type_name); - } + send_text_message_fmt(c, "$C5K-{:03X} {} {}", res.obj_st->k_id, res.ignore_def ? 'G' : 'S', type_name); + } - } else { - res.ref_ene_st = map->enemy_state_for_index(version, cmd.floor, cmd.entity_index); - res.target_ene_st = res.ref_ene_st->alias_target_ene_st ? res.ref_ene_st->alias_target_ene_st : res.ref_ene_st; - EnemyType type = res.target_ene_st->type(version, episode, difficulty, event); - c->log.info_f("Drop check for E-{:03X} (target E-{:03X}, type {})", - res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(type)); - res.effective_rt_index = type_definition_for_enemy(type).rt_index; - // rt_indexes in Episode 4 don't match those sent in the command; we just - // ignore what the client sends. - if ((episode != Episode::EP4) && (cmd.rt_index != res.effective_rt_index)) { - // Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER - if (cmd.rt_index == 0x27 && type == EnemyType::BULCLAW) { - c->log.info_f("E-{:03X} killed as BULK instead of BULCLAW", res.target_ene_st->e_id); - res.effective_rt_index = 0x27; - } else if (cmd.rt_index == 0x23 && type == EnemyType::DARK_GUNNER) { - c->log.info_f("E-{:03X} killed as DEATH_GUNNER instead of DARK_GUNNER", res.target_ene_st->e_id); - res.effective_rt_index = 0x23; - } else { - c->log.warning_f("rt_index {:02X} from command does not match entity\'s expected index {:02X}", - cmd.rt_index, res.effective_rt_index); - if (!is_v4(version)) { - res.effective_rt_index = cmd.rt_index; - } + } else { + res.ref_ene_st = map->enemy_state_for_index(version, cmd.floor, cmd.entity_index); + res.target_ene_st = res.ref_ene_st->alias_target_ene_st ? res.ref_ene_st->alias_target_ene_st : res.ref_ene_st; + uint8_t area = map->floor_to_area.at(res.target_ene_st->super_ene->floor); + EnemyType type = res.target_ene_st->type(version, area, difficulty, event); + c->log.info_f("Drop check for E-{:03X} (target E-{:03X}, type {})", + res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(type)); + res.effective_rt_index = type_definition_for_enemy(type).rt_index; + // rt_indexes in Episode 4 don't match those sent in the command; we just + // ignore what the client sends. + if ((area < 0x24) && (cmd.rt_index != res.effective_rt_index)) { + // Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER + if (cmd.rt_index == 0x27 && type == EnemyType::BULCLAW) { + c->log.info_f("E-{:03X} killed as BULK instead of BULCLAW", res.target_ene_st->e_id); + res.effective_rt_index = 0x27; + } else if (cmd.rt_index == 0x23 && type == EnemyType::DARK_GUNNER) { + c->log.info_f("E-{:03X} killed as DEATH_GUNNER instead of DARK_GUNNER", res.target_ene_st->e_id); + res.effective_rt_index = 0x23; + } else { + c->log.warning_f("rt_index {:02X} from command does not match entity\'s expected index {:02X}", + cmd.rt_index, res.effective_rt_index); + if (!is_v4(version)) { + res.effective_rt_index = cmd.rt_index; } } - if (cmd.floor != res.target_ene_st->super_ene->floor) { - c->log.warning_f("Floor {:02X} from command does not match entity\'s expected floor {:02X}", - cmd.floor, res.target_ene_st->super_ene->floor); - } - if (c->check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_fmt(c, "$C5E-{:03X} {}", res.target_ene_st->e_id, phosg::name_for_enum(type)); - } + } + if (cmd.floor != res.target_ene_st->super_ene->floor) { + c->log.warning_f("Floor {:02X} from command does not match entity\'s expected floor {:02X}", + cmd.floor, res.target_ene_st->super_ene->floor); + } + if (c->check_flag(Client::Flag::DEBUG_ENABLED)) { + send_text_message_fmt(c, "$C5E-{:03X} {}", res.target_ene_st->e_id, phosg::name_for_enum(type)); } } @@ -2948,8 +2949,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S // mode, so that we can correctly mark enemies and objects as having dropped // their items in persistent games. G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data, msg.size); - Episode episode = episode_for_area(l->area_for_floor(c->version(), cmd.floor)); - auto rec = reconcile_drop_request_with_map(c, cmd, episode, l->difficulty, l->event, l->map_state, true); + auto rec = reconcile_drop_request_with_map(c, cmd, l->difficulty, l->event, l->map_state, true); ServerDropMode drop_mode = l->drop_mode; switch (drop_mode) { @@ -3228,7 +3228,20 @@ static asio::awaitable on_sync_quest_register(shared_ptr c, Subcom } } - forward_subcommand(c, msg); + // NOCOMMIT: Add condition here for l->quest->meta.enable_schtserv_commands + if ((cmd.register_number == 0xF1) && (cmd.value.as_int == 0x52455650)) { + // PVER => respond with specific_version in schtserv's format + G_SyncQuestRegister_6x77 ret_cmd; + ret_cmd.header.subcommand = 0x77; + ret_cmd.header.size = sizeof(ret_cmd) / 4; + ret_cmd.header.unused = 0; + ret_cmd.register_number = 0xF2; + ret_cmd.value.as_int = is_v4(c->version()) ? 0x50 : c->sub_version; + send_command_t(c, 0x60, 0x00, ret_cmd); + + } else { + forward_subcommand(c, msg); + } } static asio::awaitable on_set_entity_set_flag(shared_ptr c, SubcommandMessage& msg) { @@ -3967,7 +3980,7 @@ static uint32_t base_exp_for_enemy_type( } else if (current_episode == Episode::EP4) { uint8_t area = quest ? quest->meta.floor_assignments.at(floor).area - : SetDataTableBase::default_area_for_floor(Version::BB_V4, Episode::EP4, floor); + : SetDataTableBase::default_floor_to_area(Version::BB_V4, Episode::EP4).at(floor); if (area <= 0x28) { // Crater episode_order[1] = Episode::EP1; episode_order[2] = Episode::EP2; @@ -4039,8 +4052,9 @@ static asio::awaitable on_steal_exp_bb(shared_ptr c, SubcommandMes co_return; } - auto episode = episode_for_area(l->area_for_floor(c->version(), ene_st->super_ene->floor)); - auto type = ene_st->type(c->version(), episode, l->difficulty, l->event); + uint8_t area = l->area_for_floor(c->version(), ene_st->super_ene->floor); + Episode episode = episode_for_area(area); + auto type = ene_st->type(c->version(), area, l->difficulty, l->event); uint32_t enemy_exp = base_exp_for_enemy_type( s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); @@ -4090,8 +4104,9 @@ static asio::awaitable on_enemy_exp_request_bb(shared_ptr c, Subco } ene_st->server_flags |= MapState::EnemyState::Flag::EXP_GIVEN; - auto episode = episode_for_area(l->area_for_floor(c->version(), ene_st->super_ene->floor)); - auto type = ene_st->type(c->version(), episode, l->difficulty, l->event); + uint8_t area = l->area_for_floor(c->version(), ene_st->super_ene->floor); + Episode episode = episode_for_area(area); + auto type = ene_st->type(c->version(), area, l->difficulty, l->event); double base_exp = base_exp_for_enemy_type( s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); l->log.info_f("Base EXP for this enemy ({}) is {:g}", phosg::name_for_enum(type), base_exp); diff --git a/src/ReceiveSubcommands.hh b/src/ReceiveSubcommands.hh index 8eb261e1..06a88845 100644 --- a/src/ReceiveSubcommands.hh +++ b/src/ReceiveSubcommands.hh @@ -31,7 +31,6 @@ struct DropReconcileResult { DropReconcileResult reconcile_drop_request_with_map( std::shared_ptr c, G_SpecializableItemDropRequest_6xA2& cmd, - Episode episode, Difficulty difficulty, uint8_t event, std::shared_ptr map, diff --git a/src/ServerState.cc b/src/ServerState.cc index b28a2b3e..950b4c00 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -1739,7 +1739,7 @@ shared_ptr ServerState::get_free_play_supermap( supermap = this->supermap_for_source_hash_sum.at(source_hash_sum); static_game_data_log.info_f("Linking existing free play supermap {:016X} for key {:08X}", source_hash_sum, free_play_key); } catch (const out_of_range&) { - supermap = make_shared(episode, *map_files); + supermap = make_shared(*map_files, SetDataTableBase::default_floor_to_area(Version::BB_V4, episode)); this->supermap_for_source_hash_sum.emplace(source_hash_sum, supermap); static_game_data_log.info_f("Constructed free play supermap {:016X} for key {:08X}", source_hash_sum, free_play_key); }