From df29a60a6ece8167bea52ad35ca46fc0f62ff60b Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 3 Jan 2024 00:22:28 -0800 Subject: [PATCH] load maps on all versions --- src/ItemCreator.cc | 31 +- src/ItemCreator.hh | 12 +- src/Lobby.cc | 67 +++- src/Map.cc | 755 ++++++++++++++++++++++++++++++++++---- src/Map.hh | 17 +- src/ReceiveCommands.cc | 31 +- src/ReceiveSubcommands.cc | 284 ++++++++------ src/Version.cc | 35 ++ src/Version.hh | 2 + 9 files changed, 968 insertions(+), 266 deletions(-) diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 081e1977..e6f3374b 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -52,19 +52,6 @@ void ItemCreator::set_random_state(uint32_t seed, uint32_t absolute_offset) { } } -void ItemCreator::set_box_destroyed(uint16_t entity_id) { - this->destroyed_boxes.emplace(entity_id); -} - -void ItemCreator::set_monster_destroyed(uint16_t entity_id) { - this->destroyed_monsters.emplace(entity_id); -} - -void ItemCreator::clear_destroyed_entities() { - this->destroyed_monsters.clear(); - this->destroyed_boxes.clear(); -} - bool ItemCreator::are_rare_drops_allowed() const { // Note: The client has an additional check here, which appears to be a subtle // anti-cheating measure. There is a flag on the client, initially zero, which @@ -134,16 +121,12 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const { } } -ItemCreator::DropResult ItemCreator::on_box_item_drop(uint16_t entity_id, uint8_t area) { - return this->destroyed_boxes.count(entity_id) - ? DropResult() - : this->on_box_item_drop_with_area_norm(this->normalize_area_number(area)); +ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) { + return this->on_box_item_drop_with_area_norm(this->normalize_area_number(area)); } -ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area) { - return this->destroyed_monsters.count(entity_id) - ? DropResult() - : this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area)); +ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) { + return this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area)); } ItemCreator::DropResult ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) { @@ -1680,11 +1663,7 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player } ItemCreator::DropResult ItemCreator::on_specialized_box_item_drop( - uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) { - if (this->destroyed_boxes.count(entity_id)) { - return DropResult(); - } - + uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) { DropResult res; res.item = this->base_item_for_specialized_box(def0, def1, def2); if (def_z == 0.0f) { diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index 5f2be0c6..e5f0f917 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -29,19 +29,15 @@ public: ~ItemCreator() = default; void set_random_state(uint32_t seed, uint32_t absolute_offset); - void clear_destroyed_entities(); struct DropResult { ItemData item; bool is_from_rare_table = false; }; - DropResult on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area); - DropResult on_box_item_drop(uint16_t entity_id, uint8_t area); - DropResult on_specialized_box_item_drop(uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2); - - void set_monster_destroyed(uint16_t entity_id); - void set_box_destroyed(uint16_t entity_id); + DropResult on_monster_item_drop(uint32_t enemy_type, uint8_t area); + DropResult on_box_item_drop(uint8_t area); + DropResult on_specialized_box_item_drop(uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2); ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const; @@ -82,8 +78,6 @@ private: // Note: The original implementation uses 17 different random states for some // reason. We forego that and use only one for simplicity. PSOV2Encryption random_crypt; - std::unordered_set destroyed_monsters; - std::unordered_set destroyed_boxes; inline bool is_v3() const { return !is_v1_or_v2(this->version); diff --git a/src/Lobby.cc b/src/Lobby.cc index 7f60d06b..7ce55a93 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -262,16 +262,20 @@ void Lobby::create_item_creator() { } void Lobby::load_maps() { - auto s = this->require_server_state(); this->map = make_shared(this->lobby_id, this->random_crypt); + auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates) + ? this->rare_enemy_rates + : Map::DEFAULT_RARE_ENEMIES; + + auto s = this->require_server_state(); if (this->quest) { auto leader_c = this->clients.at(this->leader_id); if (!leader_c) { throw logic_error("lobby leader is missing"); } - auto vq = this->quest->version(Version::BB_V4, leader_c->language()); + auto vq = this->quest->version(this->base_version, leader_c->language()); auto dat_contents = prs_decompress(*vq->dat_contents); this->map->clear(); this->map->add_enemies_and_objects_from_quest_data( @@ -281,16 +285,48 @@ void Lobby::load_maps() { dat_contents.data(), dat_contents.size(), this->random_seed, - this->rare_enemy_rates ? this->rare_enemy_rates : Map::NO_RARE_ENEMIES); + rare_rates); + + } else if (this->mode != GameMode::CHALLENGE) { + // No quest loaded - load free-roam maps instead. Don't load free-roam maps + // in Challenge mode, since players can't go to Ragol without a quest loaded - } else { // No quest loaded for (size_t floor = 0; floor < 0x10; floor++) { this->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32, floor, this->variations[floor * 2].load(), this->variations[floor * 2 + 1].load()); + auto get_file_data = [&](const string& filename) -> shared_ptr { + if (this->base_version == Version::BB_V4) { + try { + return s->load_bb_file(filename); + } catch (const exception& e) { + this->log.info("[Map/%zu:e] Failed to load %s from BB patch tree: %s", floor, filename.c_str(), e.what()); + } + } else if (this->base_version == Version::PC_V2) { + try { + string path = "system/patch-pc/Media/PSO/" + filename; + auto ret = make_shared(load_file(path)); + this->log.info("[Map/%zu:e] Loaded %s from PC patch tree", floor, filename.c_str()); + return ret; + } catch (const exception& e) { + this->log.info("[Map/%zu:e] Failed to load %s from PC patch tree: %s", floor, filename.c_str(), e.what()); + } + } + try { + string path = string_printf("system/maps/%s/%s", file_path_token_for_version(this->base_version), filename.c_str()); + auto ret = make_shared(load_file(path)); + this->log.info("[Map/%zu:e] Loaded %s from default maps", floor, filename.c_str()); + return ret; + } catch (const exception& e) { + this->log.info("[Map/%zu:e] Failed to load %s from PC patch tree: %s", floor, filename.c_str(), e.what()); + } + return nullptr; + }; + auto enemy_filenames = map_filenames_for_variation( + this->base_version, this->episode, - (this->mode == GameMode::SOLO), + this->mode, floor, this->variations[floor * 2], this->variations[floor * 2 + 1], @@ -300,8 +336,8 @@ void Lobby::load_maps() { } else { bool any_map_loaded = false; for (const string& filename : enemy_filenames) { - try { - auto map_data = s->load_bb_file(filename, "", "map/" + filename); + auto map_data = get_file_data(filename); + if (map_data) { this->map->add_enemies_from_map_data( this->episode, this->difficulty, @@ -309,11 +345,9 @@ void Lobby::load_maps() { floor, map_data->data(), map_data->size(), - this->rare_enemy_rates); + rare_rates); any_map_loaded = true; break; - } catch (const exception& e) { - this->log.info("[Map/%zu:e] Failed to load %s: %s", floor, filename.c_str(), e.what()); } } if (!any_map_loaded) { @@ -322,8 +356,9 @@ void Lobby::load_maps() { } auto object_filenames = map_filenames_for_variation( + this->base_version, this->episode, - (this->mode == GameMode::SOLO), + this->mode, floor, this->variations[floor * 2], this->variations[floor * 2 + 1], @@ -333,13 +368,11 @@ void Lobby::load_maps() { } else { bool any_map_loaded = false; for (const string& filename : object_filenames) { - try { - auto map_data = s->load_bb_file(filename, "", "map/" + filename); + auto map_data = get_file_data(filename); + if (map_data) { this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); any_map_loaded = true; break; - } catch (const exception& e) { - this->log.info("[Map/%zu:o] Failed to load %s: %s", floor, filename.c_str(), e.what()); } } if (!any_map_loaded) { @@ -361,10 +394,6 @@ void Lobby::load_maps() { } this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)", this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size()); - - if (this->item_creator) { - this->item_creator->clear_destroyed_entities(); - } } void Lobby::create_ep3_server() { diff --git a/src/Map.cc b/src/Map.cc index 3aaefbae..6baf3c9d 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -14,6 +14,561 @@ using namespace std; static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f; +const char* Map::name_for_object_type(uint16_t type) { + switch (type) { + case 0x0000: + return "TObjPlayerSet"; + case 0x0001: + return "TObjParticle"; + case 0x0002: + return "TObjAreaWarpForest"; + case 0x0003: + return "TObjMapWarpForest"; + case 0x0004: + return "TObjLight"; + case 0x0006: + return "TObjEnvSound"; + case 0x0007: + return "TObjFogCollision"; + case 0x0008: + return "TObjEvtCollision"; + case 0x0009: + return "TObjCollision"; + case 0x000A: + return "TOMineIcon01"; + case 0x000B: + return "TOMineIcon02"; + case 0x000C: + return "TOMineIcon03"; + case 0x000D: + return "TOMineIcon04"; + case 0x000E: + return "TObjRoomId"; + case 0x000F: + return "TOSensorGeneral01"; + case 0x0011: + return "TEF_LensFlare"; + case 0x0012: + return "TObjQuestCol"; + case 0x0013: + return "TOHealGeneral"; + case 0x0014: + return "TObjMapCsn"; + case 0x0015: + return "TObjQuestColA"; + case 0x0016: + return "TObjItemLight"; + case 0x0017: + return "TObjRaderCol"; + case 0x0018: + return "TObjFogCollisionSwitch"; + case 0x0019: + return "TObjWarpBossMulti(off)/TObjWarpBoss(on)"; + case 0x001A: + return "TObjSinBoard"; + case 0x001B: + return "TObjAreaWarpQuest"; + case 0x001C: + return "TObjAreaWarpEnding"; + case 0x001D: + return "__UNNAMED_001D__"; + case 0x001E: + return "__UNNAMED_001E__"; + case 0x001F: + return "TObjRaderHideCol"; + case 0x0020: + return "TOSwitchItem"; + case 0x0021: + return "TOSymbolchatColli"; + case 0x0022: + return "TOKeyCol"; + case 0x0023: + return "TOAttackableCol"; + case 0x0024: + return "TOSwitchAttack"; + case 0x0025: + return "TOSwitchTimer"; + case 0x0026: + return "TOChatSensor"; + case 0x0027: + return "TObjRaderIcon"; + case 0x0028: + return "TObjEnvSoundEx"; + case 0x0029: + return "TObjEnvSoundGlobal"; + case 0x0040: + return "TShopGenerator"; + case 0x0041: + return "TObjLuker"; + case 0x0042: + return "TObjBgmCol"; + case 0x0043: + return "TObjCityMainWarp"; + case 0x0044: + return "TObjCityAreaWarp"; + case 0x0045: + return "TObjCityMapWarp"; + case 0x0046: + return "TObjCityDoor_Shop"; + case 0x0047: + return "TObjCityDoor_Guild"; + case 0x0048: + return "TObjCityDoor_Warp"; + case 0x0049: + return "TObjCityDoor_Med"; + case 0x004A: + return "__UNNAMED_004A__"; + case 0x004B: + return "TObjCity_Season_EasterEgg"; + case 0x004C: + return "TObjCity_Season_ValentineHeart"; + case 0x004D: + return "TObjCity_Season_XmasTree"; + case 0x004E: + return "TObjCity_Season_XmasWreath"; + case 0x004F: + return "TObjCity_Season_HalloweenPumpkin"; + case 0x0050: + return "TObjCity_Season_21_21"; + case 0x0051: + return "TObjCity_Season_SonicAdv2"; + case 0x0052: + return "TObjCity_Season_Board"; + case 0x0053: + return "TObjCity_Season_FireWorkCtrl"; + case 0x0054: + return "TObjCityDoor_Lobby"; + case 0x0055: + return "TObjCityMainWarpChallenge"; + case 0x0056: + return "TODoorLabo"; + case 0x0057: + return "TObjTradeCollision"; + case 0x0080: + return "TObjDoor"; + case 0x0081: + return "TObjDoorKey"; + case 0x0082: + return "TObjLazerFenceNorm"; + case 0x0083: + return "TObjLazerFence4"; + case 0x0084: + return "TLazerFenceSw"; + case 0x0085: + return "TKomorebi"; + case 0x0086: + return "TButterfly"; + case 0x0087: + return "TMotorcycle"; + case 0x0088: + return "TObjContainerItem"; + case 0x0089: + return "TObjTank"; + case 0x008B: + return "TObjComputer"; + case 0x008C: + return "TObjContainerIdo"; + case 0x008D: + return "TOCapsuleAncient01"; + case 0x008E: + return "TOBarrierEnergy01"; + case 0x008F: + return "TObjHashi"; + case 0x0090: + return "TOKeyGenericSw"; + case 0x0091: + return "TObjContainerEnemy"; + case 0x0092: + return "TObjContainerBase"; + case 0x0093: + return "TObjContainerAbeEnemy"; + case 0x0095: + return "TObjContainerNoItem"; + case 0x0096: + return "TObjLazerFenceExtra"; + case 0x00C0: + return "TOKeyCave01"; + case 0x00C1: + return "TODoorCave01"; + case 0x00C2: + return "TODoorCave02"; + case 0x00C3: + return "TOHangceilingCave01Key/TOHangceilingCave01Normal/TOHangceilingCave01KeyQuick"; + case 0x00C4: + return "TOSignCave01"; + case 0x00C5: + return "TOSignCave02"; + case 0x00C6: + return "TOSignCave03"; + case 0x00C7: + return "TOAirconCave01"; + case 0x00C8: + return "TOAirconCave02"; + case 0x00C9: + return "TORevlightCave01"; + case 0x00CB: + return "TORainbowCave01"; + case 0x00CC: + return "TOKurage"; + case 0x00CD: + return "TODragonflyCave01"; + case 0x00CE: + return "TODoorCave03"; + case 0x00CF: + return "TOBind"; + case 0x00D0: + return "TOCakeshopCave01"; + case 0x00D1: + return "TORockCaveS01"; + case 0x00D2: + return "TORockCaveM01"; + case 0x00D3: + return "TORockCaveL01"; + case 0x00D4: + return "TORockCaveS02"; + case 0x00D5: + return "TORockCaveM02"; + case 0x00D6: + return "TORockCaveL02"; + case 0x00D7: + return "TORockCaveSS02"; + case 0x00D8: + return "TORockCaveSM02"; + case 0x00D9: + return "TORockCaveSL02"; + case 0x00DA: + return "TORockCaveS03"; + case 0x00DB: + return "TORockCaveM03"; + case 0x00DC: + return "TORockCaveL03"; + case 0x00DE: + return "TODummyKeyCave01"; + case 0x00DF: + return "TORockCaveBL01"; + case 0x00E0: + return "TORockCaveBL02"; + case 0x00E1: + return "TORockCaveBL03"; + case 0x0100: + return "TODoorMachine01"; + case 0x0101: + return "TOKeyMachine01"; + case 0x0102: + return "TODoorMachine02"; + case 0x0103: + return "TOCapsuleMachine01"; + case 0x0104: + return "TOComputerMachine01"; + case 0x0105: + return "TOMonitorMachine01"; + case 0x0106: + return "TODragonflyMachine01"; + case 0x0107: + return "TOLightMachine01"; + case 0x0108: + return "TOExplosiveMachine01"; + case 0x0109: + return "TOExplosiveMachine02"; + case 0x010A: + return "TOExplosiveMachine03"; + case 0x010B: + return "TOSparkMachine01"; + case 0x010C: + return "TOHangerMachine01"; + case 0x0130: + return "TODoorVoShip"; + case 0x0140: + return "TObjGoalWarpAncient"; + case 0x0141: + return "TObjMapWarpAncient"; + case 0x0142: + return "TOKeyAncient02"; + case 0x0143: + return "TOKeyAncient03"; + case 0x0144: + return "TODoorAncient01"; + case 0x0145: + return "TODoorAncient03"; + case 0x0146: + return "TODoorAncient04"; + case 0x0147: + return "TODoorAncient05"; + case 0x0148: + return "TODoorAncient06"; + case 0x0149: + return "TODoorAncient07"; + case 0x014A: + return "TODoorAncient08"; + case 0x014B: + return "TODoorAncient09"; + case 0x014C: + return "TOSensorAncient01"; + case 0x014D: + return "TOKeyAncient01"; + case 0x014E: + return "TOFenceAncient01"; + case 0x014F: + return "TOFenceAncient02"; + case 0x0150: + return "TOFenceAncient03"; + case 0x0151: + return "TOFenceAncient04"; + case 0x0152: + return "TContainerAncient01"; + case 0x0153: + return "TOTrapAncient01"; + case 0x0154: + return "TOTrapAncient02"; + case 0x0155: + return "TOMonumentAncient01"; + case 0x0156: + return "TOMonumentAncient02"; + case 0x0159: + return "TOWreckAncient01"; + case 0x015A: + return "TOWreckAncient02"; + case 0x015B: + return "TOWreckAncient03"; + case 0x015C: + return "TOWreckAncient04"; + case 0x015D: + return "TOWreckAncient05"; + case 0x015E: + return "TOWreckAncient06"; + case 0x015F: + return "TOWreckAncient07"; + case 0x0160: + return "TObjFogCollisionPoison/TObjWarpBoss03"; + case 0x0161: + return "TOContainerAncientItemCommon"; + case 0x0162: + return "TOContainerAncientItemRare"; + case 0x0163: + return "TOContainerAncientEnemyCommon"; + case 0x0164: + return "TOContainerAncientEnemyRare"; + case 0x0165: + return "TOContainerAncientItemNone"; + case 0x0166: + return "TOWreckAncientBrakable05"; + case 0x0167: + return "TOTrapAncient02R"; + case 0x0170: + return "TOBoss4Bird"; + case 0x0171: + return "TOBoss4Tower"; + case 0x0172: + return "TOBoss4Rock"; + case 0x0180: + return "TObjInfoCol"; + case 0x0181: + return "TObjWarpLobby"; + case 0x0182: + return "TObjLobbyMain"; + case 0x0183: + return "__TObjPathObj_subclass_0183__"; + case 0x0184: + return "TObjButterflyLobby"; + case 0x0185: + return "TObjRainbowLobby"; + case 0x0186: + return "TObjKabochaLobby"; + case 0x0187: + return "TObjStendGlassLobby"; + case 0x0188: + return "TObjCurtainLobby"; + case 0x0189: + return "TObjWeddingLobby"; + case 0x018A: + return "TObjTreeLobby"; + case 0x018B: + return "TObjSuisouLobby"; + case 0x018C: + return "TObjParticleLobby"; + case 0x0190: + return "TObjCamera"; + case 0x0191: + return "TObjTuitate"; + case 0x0192: + return "TObjDoaEx01"; + case 0x0193: + return "TObjBigTuitate"; + case 0x01A0: + return "TODoorVS2Door01"; + case 0x01A1: + return "TOVS2Wreck01"; + case 0x01A2: + return "TOVS2Wreck02"; + case 0x01A3: + return "TOVS2Wreck03"; + case 0x01A4: + return "TOVS2Wreck04"; + case 0x01A5: + return "TOVS2Wreck05"; + case 0x01A6: + return "TOVS2Wreck06"; + case 0x01A7: + return "TOVS2Wall01"; + case 0x01A8: + return "__UNNAMED_01A8__"; + case 0x01A9: + return "TObjHashiVersus1"; + case 0x01AA: + return "TObjHashiVersus2"; + case 0x01AB: + return "TODoorFourLightRuins"; + case 0x01C0: + return "TODoorFourLightSpace"; + case 0x0200: + return "TObjContainerJung"; + case 0x0201: + return "TObjWarpJung"; + case 0x0202: + return "TObjDoorJung"; + case 0x0203: + return "TObjContainerJungEx"; + case 0x0204: + return "TODoorJungleMain"; + case 0x0205: + return "TOKeyJungleMain"; + case 0x0206: + return "TORockJungleS01"; + case 0x0207: + return "TORockJungleM01"; + case 0x0208: + return "TORockJungleL01"; + case 0x0209: + return "TOGrassJungle"; + case 0x020A: + return "TObjWarpJungMain"; + case 0x020B: + return "TBGLightningCtrl"; + case 0x020C: + return "__TObjPathObj_subclass_020C__"; + case 0x020D: + return "__TObjPathObj_subclass_020D__"; + case 0x020E: + return "TObjContainerJungEnemy"; + case 0x020F: + return "TOTrapChainSawDamage"; + case 0x0210: + return "TOTrapChainSawKey"; + case 0x0211: + return "TOBiwaMushi"; + case 0x0212: + return "__TObjPathObj_subclass_0212__"; + case 0x0213: + return "TOJungleDesign"; + case 0x0220: + return "TObjFish"; + case 0x0221: + return "TODoorFourLightSeabed"; + case 0x0222: + return "TODoorFourLightSeabedU"; + case 0x0223: + return "TObjSeabedSuiso_CH"; + case 0x0224: + return "TObjSeabedSuisoBrakable"; + case 0x0225: + return "TOMekaFish00"; + case 0x0226: + return "TOMekaFish01"; + case 0x0227: + return "__TObjPathObj_subclass_0227__"; + case 0x0228: + return "TOTrapSeabed01"; + case 0x0229: + return "TOCapsuleLabo"; + case 0x0240: + return "TObjParticle"; + case 0x0280: + return "__TObjAreaWarpForest_subclass_0280__"; + case 0x02A0: + return "TObjLiveCamera"; + case 0x02B0: + return "TContainerAncient01R"; + case 0x02B1: + return "TObjLaboDesignBase"; + case 0x02B2: + return "TObjLaboDesignBase"; + case 0x02B3: + return "TObjLaboDesignBase"; + case 0x02B4: + return "TObjLaboDesignBase"; + case 0x02B5: + return "TObjLaboDesignBase"; + case 0x02B6: + return "TObjLaboDesignBase"; + case 0x02B7: + return "TObjGbAdvance"; + case 0x02B8: + return "TObjQuestColALock2"; + case 0x02B9: + return "TObjMapForceWarp"; + case 0x02BA: + return "TObjQuestCol2"; + case 0x02BB: + return "TODoorLaboNormal"; + case 0x02BC: + return "TObjAreaWarpEndingJung"; + case 0x02BD: + return "TObjLaboMapWarp"; + case 0x0300: + return "__UNKNOWN_0300__"; + case 0x0301: + return "__UNKNOWN_0301__"; + case 0x0302: + return "__UNKNOWN_0302__"; + case 0x0303: + return "__UNKNOWN_0303__"; + case 0x0340: + return "__UNKNOWN_0340__"; + case 0x0341: + return "__UNKNOWN_0341__"; + case 0x0380: + return "__UNKNOWN_0380__"; + case 0x0381: + return "__UNKNOWN_0381__"; + case 0x0382: + return "__UNKNOWN_0382__"; + case 0x0383: + return "__UNKNOWN_0383__"; + case 0x0385: + return "__UNKNOWN_0385__"; + case 0x0386: + return "__UNKNOWN_0386__"; + case 0x0387: + return "__UNKNOWN_0387__"; + case 0x0388: + return "__UNKNOWN_0388__"; + case 0x0389: + return "__UNKNOWN_0389__"; + case 0x038A: + return "__UNKNOWN_038A__"; + case 0x038B: + return "__UNKNOWN_038B__"; + case 0x038C: + return "__UNKNOWN_038C__"; + case 0x038D: + return "__UNKNOWN_038D__"; + case 0x038E: + return "__UNKNOWN_038E__"; + case 0x038F: + return "__UNKNOWN_038F__"; + case 0x0390: + return "__UNKNOWN_0390__"; + case 0x0391: + return "__UNKNOWN_0391__"; + case 0x03C0: + return "__UNKNOWN_03C0__"; + case 0x03C1: + return "__UNKNOWN_03C1__"; + default: + return "__UNKNOWN__"; + } +} + Map::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate) : hildeblue(enemy_rate), rappy(enemy_rate), @@ -100,8 +655,9 @@ string Map::EnemyEntry::str() const { this->unused.load()); } -Map::Enemy::Enemy(size_t source_index, uint8_t floor, EnemyType type) +Map::Enemy::Enemy(uint16_t enemy_id, size_t source_index, uint8_t floor, EnemyType type) : source_index(source_index), + enemy_id(enemy_id), type(type), floor(floor), state_flags(0), @@ -109,19 +665,22 @@ Map::Enemy::Enemy(size_t source_index, uint8_t floor, EnemyType type) } string Map::Enemy::str() const { - return string_printf("[Map::Enemy source %zX %s floor=%02hhX flags=%02hhX last_hit_by_client_id=%hu]", - this->source_index, name_for_enum(this->type), this->floor, this->state_flags, this->last_hit_by_client_id); + return string_printf("[Map::Enemy E-%hX source %zX %s floor=%02hhX flags=%02hhX last_hit_by_client_id=%hu]", + this->enemy_id, this->source_index, name_for_enum(this->type), this->floor, this->state_flags, this->last_hit_by_client_id); } string Map::Object::str() const { - if (this->param1 <= 0.0f) { - return string_printf("[Map::Object source %zX %04hX @%04hX p1=%g (specialized: %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ") floor=%02hhX item_drop_checked=%s]", - this->source_index, this->base_type, this->section, this->param1, this->param4, this->param5, this->param6, this->floor, this->item_drop_checked ? "true" : "false"); - } else { - return string_printf("[Map::Object source %zX %04hX @%04hX p1=%g (generic) p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]", - this->source_index, this->base_type, this->section, this->param1, this->param4, this->param5, this->param6, - this->floor, this->item_drop_checked ? "true" : "false"); - } + 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, + this->base_type, + Map::name_for_object_type(this->base_type), + this->section, + this->param1, + this->param4, + this->param5, + this->param6, + this->floor, + this->item_drop_checked ? "true" : "false"); } Map::Map(uint32_t lobby_id, std::shared_ptr random_crypt) @@ -142,8 +701,11 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size const auto* objects = reinterpret_cast(data); for (size_t z = 0; z < entry_count; z++) { + uint16_t object_id = this->objects.size(); this->objects.emplace_back(Object{ .source_index = z, + .object_id = object_id, + .floor = floor, .base_type = objects[z].base_type, .section = objects[z].section, .param1 = objects[z].param1, @@ -151,7 +713,6 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size .param4 = objects[z].param4, .param5 = objects[z].param5, .param6 = objects[z].param6, - .floor = floor, .item_drop_checked = false, }); } @@ -177,7 +738,8 @@ void Map::add_enemy( const EnemyEntry& e, std::shared_ptr rare_rates) { auto add = [&](EnemyType type) -> void { - this->enemies.emplace_back(index, floor, type); + uint16_t enemy_id = this->enemies.size(); + this->enemies.emplace_back(enemy_id, index, floor, type); }; EnemyType child_type = EnemyType::UNKNOWN; @@ -956,6 +1518,24 @@ void Map::add_enemies_and_objects_from_quest_data( } } +const Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) const { + return const_cast(this)->find_enemy(floor, type); +} + +Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) { + if (enemies.empty()) { + throw out_of_range("no enemies defined"); + } + // TODO: Linear search is bad here. Do something better, like binary search + // for the floor start and just linear search through the floor enemies. + for (auto& e : this->enemies) { + if (e.floor == floor && e.type == type) { + return e; + } + } + throw out_of_range("enemy not found"); +} + string Map::disassemble_quest_data(const void* data, size_t size) { auto all_floor_sections = Map::collect_quest_map_data_sections(data, size); @@ -1089,12 +1669,12 @@ void SetDataTable::print(FILE* stream) const { } } -struct AreaMapFileIndex { +struct AreaMapFileInfo { const char* name_token; vector variation1_values; vector variation2_values; - AreaMapFileIndex( + AreaMapFileInfo( const char* name_token, vector variation1_values, vector variation2_values) @@ -1103,8 +1683,27 @@ struct AreaMapFileIndex { variation2_values(variation2_values) {} }; +static const vector map_file_info_dc_protos = { + {"city00", {}, {0}}, + {"forest01", {}, {0, 1}}, + {"forest02", {}, {0, 1}}, + {"cave01", {0, 1}, {0, 1}}, + {"cave02", {0, 1}, {0, 1}}, + {"cave03", {0, 1}, {0, 1}}, + {"machine01", {0}, {0, 1}}, + {"machine02", {0}, {0, 1}}, + {"ancient01", {0, 1}, {0, 1}}, + {"ancient02", {0, 1}, {0, 1}}, + {"ancient03", {0, 1}, {0, 1}}, + {"boss01", {}, {}}, + {"boss02", {}, {}}, + {"boss03", {}, {}}, + {"boss04", {}, {}}, + {nullptr, {}, {}}, +}; + // These are indexed as [episode][is_solo][floor], where episode is 0-2 -static const vector>> map_file_info = { +static const vector>> map_file_info = { { // Episode 1 { @@ -1230,96 +1829,116 @@ static const vector>> map_file_info = { }, }; -const vector>& map_file_info_for_episode(Episode ep) { - switch (ep) { - case Episode::EP1: - return map_file_info.at(0); - case Episode::EP2: - return map_file_info.at(1); - case Episode::EP4: - return map_file_info.at(2); - default: - throw invalid_argument("episode has no maps"); +const AreaMapFileInfo& file_info_for_variation( + Version version, Episode episode, uint8_t area, bool is_solo) { + const vector* multi_index = nullptr; + const vector* solo_index = nullptr; + if (is_pre_v1(version)) { + multi_index = &map_file_info_dc_protos; + } else { + switch (episode) { + case Episode::EP1: + multi_index = &map_file_info.at(0).at(0); + solo_index = &map_file_info.at(0).at(1); + break; + case Episode::EP2: + multi_index = &map_file_info.at(1).at(0); + solo_index = &map_file_info.at(1).at(1); + break; + case Episode::EP3: { + static const AreaMapFileInfo blank_info = {nullptr, {}, {}}; + return blank_info; + } + case Episode::EP4: + multi_index = &map_file_info.at(2).at(0); + solo_index = &map_file_info.at(2).at(1); + break; + default: + throw invalid_argument("episode has no maps"); + } } + + if (is_solo && solo_index) { + const auto& ret = solo_index->at(area); + if (ret.name_token) { + return ret; + } + } + return multi_index->at(area); } void generate_variations( parray& variations, shared_ptr random_crypt, + Version version, Episode episode, bool is_solo) { - const auto& ep_index = map_file_info_for_episode(episode); for (size_t z = 0; z < 0x10; z++) { - const AreaMapFileIndex* a = nullptr; - if (is_solo) { - a = &ep_index.at(true).at(z); - } - if (!a || !a->name_token) { - a = &ep_index.at(false).at(z); - } - if (!a->name_token) { + const auto& a = file_info_for_variation(version, episode, z, is_solo); + if (!a.name_token) { variations[z * 2 + 0] = 0; variations[z * 2 + 1] = 0; } else { - variations[z * 2 + 0] = (a->variation1_values.size() < 2) ? 0 : (random_crypt->next() % a->variation1_values.size()); - variations[z * 2 + 1] = (a->variation2_values.size() < 2) ? 0 : (random_crypt->next() % a->variation2_values.size()); + variations[z * 2 + 0] = (a.variation1_values.size() < 2) ? 0 : (random_crypt->next() % a.variation1_values.size()); + variations[z * 2 + 1] = (a.variation2_values.size() < 2) ? 0 : (random_crypt->next() % a.variation2_values.size()); } } } -void generate_variations_dc_nte( - parray& variations, - shared_ptr random_crypt) { - static const std::array maxes( - {1, 1, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); - for (size_t z = 0; z < 0x20; z++) { - variations[z] = (maxes[z] < 2) ? 0 : (random_crypt->next() % maxes[z]); - } -} - vector map_filenames_for_variation( - Episode episode, bool is_solo, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies) { - // Map filenames are like map_[_VV][_VV][_off][_s].dat - // name_token comes from AreaMapFileIndex + Version version, + Episode episode, + GameMode mode, + uint8_t floor, + uint32_t var1, + uint32_t var2, + bool is_enemies) { + // Map filenames are like map___(_off)?(e|o)(_s|_c1)?.dat + // name_token comes from AreaMapFileInfo // _VV are the values from the variation<1|2>_values vector (in contrast to // the values sent in the 64 command, which are INDEXES INTO THAT VECTOR) // _off or _s are used for solo mode (try both - city uses _s whereas levels // use _off apparently) + // _c1 is used for the city map in Challenge mode (which we don't load, + // since it contains only NPCs and not enemies) // e|o specifies what kind of data: e = enemies, o = objects - const auto& ep_index = map_file_info_for_episode(episode); - const AreaMapFileIndex* a = nullptr; - if (is_solo) { - a = &ep_index.at(true).at(floor); - } - if (!a || !a->name_token) { - a = &ep_index.at(false).at(floor); - } - if (!a->name_token) { + const auto& a = file_info_for_variation(version, episode, floor, mode == GameMode::SOLO); + if (!a.name_token) { return vector(); } string filename = "map_"; - filename += a->name_token; - if (!a->variation1_values.empty()) { - filename += string_printf("_%02" PRIX32, a->variation1_values.at(var1)); + filename += a.name_token; + if (!a.variation1_values.empty()) { + filename += string_printf("_%02" PRIX32, a.variation1_values.at(var1)); } - if (!a->variation2_values.empty()) { - filename += string_printf("_%02" PRIX32, a->variation2_values.at(var2)); + if (!a.variation2_values.empty()) { + filename += string_printf("_%02" PRIX32, a.variation2_values.at(var2)); } - // Try both _off.dat and _s.dat suffixes first before falling back - // to non-solo version vector ret; if (is_enemies) { - if (is_solo) { + if (mode == GameMode::SOLO) { ret.emplace_back(filename + "_offe.dat"); ret.emplace_back(filename + "e_s.dat"); + } else if (floor == 0) { + if (mode == GameMode::BATTLE) { + ret.emplace_back(filename + "e_d.dat"); + } else if (mode == GameMode::CHALLENGE) { + ret.emplace_back(filename + "e_c1.dat"); + } } ret.emplace_back(filename + "e.dat"); } else { - if (is_solo) { + if (mode == GameMode::SOLO) { ret.emplace_back(filename + "_offo.dat"); ret.emplace_back(filename + "o_s.dat"); + } else if (floor == 0) { + if (mode == GameMode::BATTLE) { + ret.emplace_back(filename + "o_d.dat"); + } else if (mode == GameMode::CHALLENGE) { + ret.emplace_back(filename + "o_c1.dat"); + } } ret.emplace_back(filename + "o.dat"); } diff --git a/src/Map.hh b/src/Map.hh index a3946eab..e92081e4 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -15,6 +15,8 @@ #include "Text.hh" struct Map { + static const char* name_for_object_type(uint16_t type); + struct SectionHeader { enum class Type { END = 0, @@ -206,6 +208,8 @@ 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 base_type; uint16_t section; float param1; // If <= 0, this is a specialized box, and the specialization is in param4/5/6 @@ -213,7 +217,6 @@ struct Map { uint32_t param4; uint32_t param5; uint32_t param6; - uint8_t floor; bool item_drop_checked; std::string str() const; @@ -228,12 +231,13 @@ struct Map { ITEM_DROPPED = 0x10, }; size_t source_index; + uint16_t enemy_id; EnemyType type; uint8_t floor; uint8_t state_flags; uint8_t last_hit_by_client_id; - Enemy(size_t source_index, uint8_t floor, EnemyType type); + Enemy(uint16_t enemy_id, size_t source_index, uint8_t floor, EnemyType type); std::string str() const; } __attribute__((packed)); @@ -305,6 +309,9 @@ struct Map { uint32_t rare_seed, std::shared_ptr rare_rates = Map::DEFAULT_RARE_ENEMIES); + const Enemy& find_enemy(uint8_t floor, EnemyType type) const; + Enemy& find_enemy(uint8_t floor, EnemyType type); + static std::string disassemble_quest_data(const void* data, size_t size); PrefixedLogger log; @@ -346,10 +353,8 @@ private: void generate_variations( parray& variations, std::shared_ptr random, + Version version, Episode episode, bool is_solo); -void generate_variations_dc_nte( - parray& variations, - std::shared_ptr random); std::vector map_filenames_for_variation( - Episode episode, bool is_solo, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies); + Version version, Episode episode, GameMode mode, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 98d0c212..dea7e867 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2001,11 +2001,16 @@ static void on_quest_loaded(shared_ptr l) { } auto s = l->require_server_state(); - // For Challenge quests, don't replace the map now - the leader will send an - // 02DF command to create overlays, which also replaces the map. - if ((l->base_version == Version::BB_V4) && l->map && (l->quest->challenge_template_index < 0)) { + + // For BB Challenge quests, don't replace the map now - the leader will send + // an 02DF command to create overlays, which also replaces the map. (We do + // this because 02DF is also sent when a challenge is failed and retried, + // which reloads the map and recreates character overlays anyway.) + if ((l->base_version != Version::BB_V4) || (l->quest->challenge_template_index < 0)) { l->load_maps(); } + + // Delete all floor items for (auto& m : l->floor_item_managers) { m.clear(); } @@ -2443,9 +2448,7 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { if (game->count_clients() == 1) { // No one was in the game before, so the object and enemy state is lost; // regenerate it as if the game was just created - if ((game->base_version == Version::BB_V4) && game->map) { - game->load_maps(); - } + game->load_maps(); c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE); } break; @@ -4175,23 +4178,15 @@ shared_ptr create_game_generic( bool is_solo = (game->mode == GameMode::SOLO); - // Generate the map variations - if (game->is_ep3()) { - game->variations.clear(0); - } else if ((c->version() == Version::DC_NTE) || (c->version() == Version::DC_V1_11_2000_PROTOTYPE)) { - generate_variations_dc_nte(game->variations, game->random_crypt); - } else { - generate_variations(game->variations, game->random_crypt, game->episode, is_solo); - } - if (game->mode == GameMode::CHALLENGE) { game->rare_enemy_rates = s->rare_enemy_rates_challenge; } else { game->rare_enemy_rates = s->rare_enemy_rates_by_difficulty.at(game->difficulty); } - if (game->base_version == Version::BB_V4) { - game->load_maps(); - } + + generate_variations(game->variations, game->random_crypt, game->base_version, game->episode, is_solo); + game->load_maps(); + return game; } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index cea872f7..30d6ed29 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2097,7 +2097,7 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u cmd.rt_index = in_cmd.rt_index; cmd.x = in_cmd.x; cmd.z = in_cmd.z; - cmd.ignore_def = 1; + cmd.ignore_def = in_cmd.ignore_def; cmd.effective_area = in_cmd.effective_area; } else { const auto& in_cmd = check_size_t(data, size); @@ -2109,83 +2109,154 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u cmd.rt_index = in_cmd.rt_index; cmd.x = in_cmd.x; cmd.z = in_cmd.z; - cmd.ignore_def = 1; + cmd.ignore_def = in_cmd.ignore_def; cmd.effective_area = in_cmd.floor; } - auto generate_item = [&]() -> ItemCreator::DropResult { - if (cmd.rt_index == 0x30) { - if (l->map) { - auto& object = l->map->objects.at(cmd.entity_id); + Map::Object* map_object = nullptr; + Map::Enemy* map_enemy = nullptr; + bool ignore_def = (cmd.ignore_def != 0); + uint8_t effective_rt_index = 0xFF; + if (cmd.rt_index == 0x30) { + if (l->map) { + map_object = &l->map->objects.at(cmd.entity_id); + l->log.info("Drop check for K-%hX %c %s", + map_object->object_id, ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type)); + if (cmd.floor != map_object->floor) { + l->log.warning("Floor %02hhX from command does not match object\'s expected floor %02hhX", cmd.floor, map_object->floor); + } + if (is_v1_or_v2(l->base_version) && (l->base_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 = map_object->param3; + cmd.param4 = map_object->param4; + cmd.param5 = map_object->param5; + cmd.param6 = map_object->param6; + } + bool object_ignore_def = (map_object->param1 > 0.0); + if (ignore_def != object_ignore_def) { + l->log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)", + ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", map_object->param1); + } + if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + send_text_message_printf(c, "$C5K-%hX %c %s", + map_object->object_id, ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type)); + } + } - if (cmd.floor != object.floor) { - l->log.warning("Floor %02hhX from command does not match object\'s expected floor %02hhX", cmd.floor, object.floor); - } - bool object_ignore_def = (object.param1 > 0.0); - if (cmd.ignore_def != object_ignore_def) { - l->log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)", - cmd.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", object.param1); + } else { + if (l->map) { + map_enemy = &l->map->enemies.at(cmd.entity_id); + l->log.info("Drop check for E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type)); + effective_rt_index = rare_table_index_for_enemy_type(map_enemy->type); + // rt_indexes in Episode 4 don't match those sent in the command; we just + // ignore what the client sends. + if ((l->episode != Episode::EP4) && (cmd.rt_index != effective_rt_index)) { + l->log.warning("rt_index %02hhX from command does not match entity\'s expected index %02" PRIX32, + cmd.rt_index, effective_rt_index); + if (!is_v4(l->base_version)) { + effective_rt_index = cmd.rt_index; } + } + if (cmd.floor != map_enemy->floor) { + l->log.warning("Floor %02hhX from command does not match entity\'s expected floor %02hhX", + cmd.floor, map_enemy->floor); + } + if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + send_text_message_printf(c, "$C5E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type)); + } + } + } - if (cmd.ignore_def) { + bool should_drop = true; + if (map_object) { + if (map_object->item_drop_checked) { + l->log.info("Drop check has already occurred for K-%04hX; skipping it", map_object->object_id); + should_drop = false; + } else { + map_object->item_drop_checked = true; + } + } + if (map_enemy) { + if (map_enemy->state_flags & Map::Enemy::Flag::ITEM_DROPPED) { + l->log.info("Drop check has already occurred for E-%04hX; skipping it", map_enemy->enemy_id); + should_drop = false; + } else { + map_enemy->state_flags |= Map::Enemy::Flag::ITEM_DROPPED; + } + } + + if (should_drop) { + auto generate_item = [&]() -> ItemCreator::DropResult { + if (cmd.rt_index == 0x30) { + if (ignore_def) { l->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); - return l->item_creator->on_box_item_drop(cmd.entity_id, cmd.effective_area); + return l->item_creator->on_box_item_drop(cmd.effective_area); } else { l->log.info("Creating item from box %04hX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")", - cmd.entity_id.load(), cmd.effective_area, object.param3, object.param4, object.param5, object.param6); - return l->item_creator->on_specialized_box_item_drop(cmd.entity_id, cmd.effective_area, object.param3, object.param4, object.param5, object.param6); + cmd.entity_id.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load()); + return l->item_creator->on_specialized_box_item_drop(cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6); } - - } else if (cmd.ignore_def) { - l->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); - return l->item_creator->on_box_item_drop(cmd.entity_id, cmd.effective_area); - } else { - l->log.info("Creating item from box %04hX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")", - cmd.entity_id.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load()); - return l->item_creator->on_specialized_box_item_drop( - cmd.entity_id, cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6); + l->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); + return l->item_creator->on_monster_item_drop(effective_rt_index, cmd.effective_area); } + }; - } else { - l->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); + switch (l->drop_mode) { + case Lobby::DropMode::DISABLED: + case Lobby::DropMode::CLIENT: + throw logic_error("unhandled simple drop mode"); + case Lobby::DropMode::SERVER_SHARED: + case Lobby::DropMode::SERVER_DUPLICATE: { + // TODO: In SERVER_DUPLICATE mode, should we reduce the rates for rare + // items? Maybe by a factor of l->count_clients()? + auto res = generate_item(); + if (res.item.empty()) { + l->log.info("No item was created"); + } else { + string name = s->describe_item(l->base_version, res.item, false); + l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str()); + if (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE) { + for (const auto& lc : l->clients) { + if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) { + res.item.id = l->generate_item_id(0xFF); + l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s", + res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str()); + l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id)); + send_drop_item_to_channel(s, lc->channel, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + if (res.is_from_rare_table) { + send_rare_notification_if_needed(lc, res.item); + } + } + } - uint8_t effective_rt_index = cmd.rt_index; - if (l->map) { - const auto& enemy = l->map->enemies.at(cmd.entity_id); - effective_rt_index = rare_table_index_for_enemy_type(enemy.type); - // rt_indexes in Episode 4 don't match those sent in the command; we just - // ignore what the client sends. - if ((l->episode != Episode::EP4) && (cmd.rt_index != effective_rt_index)) { - l->log.warning("rt_index %02hhX from command does not match entity\'s expected index %02" PRIX32, - cmd.rt_index, effective_rt_index); - } - if (cmd.floor != enemy.floor) { - l->log.warning("Floor %02hhX from command does not match entity\'s expected floor %02hhX", - cmd.floor, enemy.floor); + } else { + res.item.id = l->generate_item_id(0xFF); + l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients", + res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load()); + l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x00F); + send_drop_item_to_lobby(l, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + if (res.is_from_rare_table) { + for (auto lc : l->clients) { + if (lc) { + send_rare_notification_if_needed(lc, res.item); + } + } + } + } } + break; } - return l->item_creator->on_monster_item_drop(cmd.entity_id, effective_rt_index, cmd.effective_area); - } - }; - - switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: - case Lobby::DropMode::CLIENT: - throw logic_error("unhandled simple drop mode"); - case Lobby::DropMode::SERVER_SHARED: - case Lobby::DropMode::SERVER_DUPLICATE: { - // TODO: In SERVER_DUPLICATE mode, should we reduce the rates for rare - // items? Maybe by a factor of l->count_clients()? - auto res = generate_item(); - if (res.item.empty()) { - l->log.info("No item was created"); - } else { - string name = s->describe_item(l->base_version, res.item, false); - l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str()); - if (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE) { - for (const auto& lc : l->clients) { - if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) { + case Lobby::DropMode::SERVER_PRIVATE: { + for (const auto& lc : l->clients) { + if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) { + auto res = generate_item(); + if (res.item.empty()) { + l->log.info("No item was created for %s", lc->channel.name.c_str()); + } else { + string name = s->describe_item(l->base_version, res.item, false); + l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str()); res.item.id = l->generate_item_id(0xFF); l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s", res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str()); @@ -2196,54 +2267,12 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u } } } - - } else { - res.item.id = l->generate_item_id(0xFF); - l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients", - res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load()); - l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x00F); - send_drop_item_to_lobby(l, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id); - if (res.is_from_rare_table) { - for (auto lc : l->clients) { - if (lc) { - send_rare_notification_if_needed(lc, res.item); - } - } - } } + break; } - break; + default: + throw logic_error("invalid drop mode"); } - case Lobby::DropMode::SERVER_PRIVATE: { - for (const auto& lc : l->clients) { - if (lc && ((cmd.rt_index == 0x30) || (lc->floor == cmd.floor))) { - auto res = generate_item(); - if (res.item.empty()) { - l->log.info("No item was created for %s", lc->channel.name.c_str()); - } else { - string name = s->describe_item(l->base_version, res.item, false); - l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str()); - res.item.id = l->generate_item_id(0xFF); - l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s", - res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str()); - l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id)); - send_drop_item_to_channel(s, lc->channel, res.item, cmd.rt_index != 0x30, cmd.floor, cmd.x, cmd.z, cmd.entity_id); - if (res.is_from_rare_table) { - send_rare_notification_if_needed(lc, res.item); - } - } - } - } - break; - } - default: - throw logic_error("invalid drop mode"); - } - - if (cmd.rt_index == 0x30) { - l->item_creator->set_box_destroyed(cmd.entity_id); - } else { - l->item_creator->set_monster_destroyed(cmd.entity_id); } } @@ -2290,30 +2319,45 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla forward_subcommand(c, command, flag, data, size); - if (is_v3(c->version())) { - bool should_send_boss_drop_req = false; + if (is_v3(c->version()) && (l->drop_mode != Lobby::DropMode::DISABLED)) { + EnemyType boss_enemy_type = EnemyType::NONE; bool is_ep2 = (l->episode == Episode::EP2); if ((l->episode == Episode::EP1) && (c->floor == 0x0E)) { // On Normal, Dark Falz does not have a third phase, so send the drop // request after the end of the second phase. On all other difficulty // levels, send it after the third phase. - if (((difficulty == 0) && (flag_index == 0x0035)) || - ((difficulty != 0) && (flag_index == 0x0037))) { - should_send_boss_drop_req = true; + if ((difficulty == 0) && (flag_index == 0x0035)) { + boss_enemy_type = EnemyType::DARK_FALZ_2; + } else if ((difficulty != 0) && (flag_index == 0x0037)) { + boss_enemy_type = EnemyType::DARK_FALZ_3; } } else if (is_ep2 && (flag_index == 0x0057) && (c->floor == 0x0D)) { - should_send_boss_drop_req = true; + boss_enemy_type = EnemyType::OLGA_FLOW_2; } - if (should_send_boss_drop_req) { - auto c = l->clients.at(l->leader_id); - if (c) { - G_StandardDropItemRequest_PC_V3_BB_6x60 req = { + if (boss_enemy_type != EnemyType::NONE) { + l->log.info("Creating item from final boss (%s)", name_for_enum(boss_enemy_type)); + uint16_t enemy_id = 0xFFFF; + if (l->map) { + try { + const auto& enemy = l->map->find_enemy(c->floor, boss_enemy_type); + enemy_id = enemy.enemy_id; + if (c->floor != enemy.floor) { + l->log.warning("Floor %02" PRIX32 " from client does not match entity\'s expected floor %02hhX", c->floor, enemy.floor); + } + l->log.info("Found enemy E-%hX on floor %" PRIX32, enemy_id, enemy.floor); + } catch (const out_of_range&) { + l->log.warning("Could not find enemy on floor %" PRIX32 "; unable to determine enemy type", c->floor); + } + } + + if (boss_enemy_type != EnemyType::NONE) { + G_StandardDropItemRequest_PC_V3_BB_6x60 drop_req = { { {0x60, 0x06, 0x0000}, static_cast(c->floor), - static_cast(is_ep2 ? 0x4E : 0x2F), - 0x0B4F, + rare_table_index_for_enemy_type(boss_enemy_type), + enemy_id == 0xFFFF ? 0x0B4F : enemy_id, is_ep2 ? -9999.0f : 10160.58984375f, 0.0f, 2, @@ -2321,7 +2365,7 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla }, 0x01, {}}; - send_command_t(c, 0x62, l->leader_id, req); + on_entity_drop_item_request(c, 0x62, l->leader_id, &drop_req, sizeof(drop_req)); } } } diff --git a/src/Version.cc b/src/Version.cc index ccee11a1..6ffeeb66 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -246,3 +246,38 @@ uint32_t default_specific_version_for_version(Version version, int64_t sub_versi return 0x00000000; } } + +const char* file_path_token_for_version(Version version) { + switch (version) { + case Version::PC_PATCH: + return "pc-patch"; + case Version::BB_PATCH: + return "bb-patch"; + case Version::DC_NTE: + return "dc-nte"; + case Version::DC_V1_11_2000_PROTOTYPE: + return "dc-11-2000"; + case Version::DC_V1: + return "dc-v1"; + case Version::DC_V2: + return "dc-v2"; + case Version::PC_NTE: + return "pc-nte"; + case Version::PC_V2: + return "pc-v2"; + case Version::GC_NTE: + return "gc-nte"; + case Version::GC_V3: + return "gc-v3"; + case Version::GC_EP3_NTE: + return "gc-ep3-nte"; + case Version::GC_EP3: + return "gc-ep3"; + case Version::XB_V3: + return "xb-v3"; + case Version::BB_V4: + return "bb-v4"; + default: + throw runtime_error("invalid game version"); + } +} diff --git a/src/Version.hh b/src/Version.hh index 301b1697..23aeafef 100644 --- a/src/Version.hh +++ b/src/Version.hh @@ -136,3 +136,5 @@ template <> const char* name_for_enum(ServerBehavior behavior); template <> ServerBehavior enum_for_name(const char* name); + +const char* file_path_token_for_version(Version version);