diff --git a/src/Lobby.hh b/src/Lobby.hh index 34f75302..a840b611 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -63,6 +63,7 @@ struct Lobby : public std::enable_shared_from_this { float z; uint8_t floor; }; + std::shared_ptr rare_enemy_rates; std::shared_ptr map; std::array next_item_id; uint32_t next_game_item_id; diff --git a/src/Map.cc b/src/Map.cc index c6132f1b..138c8a78 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -14,6 +14,39 @@ using namespace std; static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f; +Map::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate) + : hildeblue(enemy_rate), + rappy(enemy_rate), + nar_lily(enemy_rate), + pouilly_slime(enemy_rate), + merissa_aa(enemy_rate), + pazuzu(enemy_rate), + dorphon_eclair(enemy_rate), + kondrieu(boss_rate) {} + +Map::RareEnemyRates::RareEnemyRates(const JSON& json) + : hildeblue(json.get_int("Hildeblue")), + rappy(json.get_int("Rappy")), + nar_lily(json.get_int("NarLily")), + pouilly_slime(json.get_int("PouillySlime")), + merissa_aa(json.get_int("MerissaAA")), + pazuzu(json.get_int("Pazuzu")), + dorphon_eclair(json.get_int("DorphonEclair")), + kondrieu(json.get_int("Kondrieu")) {} + +JSON Map::RareEnemyRates::json() const { + return JSON::dict({ + {"Hildeblue", this->hildeblue}, + {"Rappy", this->rappy}, + {"NarLily", this->nar_lily}, + {"PouillySlime", this->pouilly_slime}, + {"MerissaAA", this->merissa_aa}, + {"Pazuzu", this->pazuzu}, + {"DorphonEclair", this->dorphon_eclair}, + {"Kondrieu", this->kondrieu}, + }); +} + string Map::ObjectEntry::str() const { return string_printf("[ObjectEntry type=%04hX flags=%04hX index=%04hX a2=%04hX entity_id=%04hX group=%04hX section=%04hX a3=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] unused=%08" PRIX32 "]", this->base_type.load(), @@ -145,7 +178,7 @@ void Map::add_enemy( uint8_t floor, size_t index, const EnemyEntry& e, - const RareEnemyRates& rare_rates) { + std::shared_ptr rare_rates) { auto add = [&](EnemyType type) -> void { this->enemies.emplace_back(index, floor, type); }; @@ -218,12 +251,12 @@ void Map::add_enemy( break; case 0x0040: // TObjEneMoja - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.hildeblue) + add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->hildeblue) ? EnemyType::HILDEBLUE : EnemyType::HILDEBEAR); break; case 0x0041: { // TObjEneLappy - bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.rappy); + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->rappy); switch (episode) { case Episode::EP1: add(is_rare ? EnemyType::AL_RAPPY : EnemyType::RAG_RAPPY); @@ -278,7 +311,7 @@ void Map::add_enemy( if ((episode == Episode::EP2) && (e.floor > 0x0F)) { add(EnemyType::DEL_LILY); } else { - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.nar_lily) + add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->nar_lily) ? EnemyType::NAR_LILY : EnemyType::POISON_LILY); } @@ -292,7 +325,7 @@ void Map::add_enemy( break; } case 0x0064: // TObjEneSlime - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.pouilly_slime) + add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->pouilly_slime) ? EnemyType::POFUILLY_SLIME : EnemyType::POUILLY_SLIME); default_num_children = 4; @@ -503,7 +536,7 @@ void Map::add_enemy( } break; case 0x0112: - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.merissa_aa) + add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->merissa_aa) ? EnemyType::MERISSA_AA : EnemyType::MERISSA_A); break; @@ -511,7 +544,7 @@ void Map::add_enemy( add(EnemyType::GIRTABLULU); break; case 0x0114: { - bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.pazuzu); + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->pazuzu); if (e.floor > 0x05) { add(is_rare ? EnemyType::PAZUZU_ALT : EnemyType::ZU_ALT); } else { @@ -527,7 +560,7 @@ void Map::add_enemy( } break; case 0x0116: - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.dorphon_eclair) + add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->dorphon_eclair) ? EnemyType::DORPHON_ECLAIR : EnemyType::DORPHON); break; @@ -537,7 +570,7 @@ void Map::add_enemy( break; } case 0x0119: { - bool is_rare = this->check_and_log_rare_enemy((e.fparam2 != 0.0f), rare_rates.kondrieu); + bool is_rare = this->check_and_log_rare_enemy((e.fparam2 != 0.0f), rare_rates->kondrieu); if (is_rare) { add(EnemyType::KONDRIEU); } else { @@ -584,7 +617,7 @@ void Map::add_enemies_from_map_data( uint8_t floor, const void* data, size_t size, - const RareEnemyRates& rare_rates) { + std::shared_ptr rare_rates) { size_t entry_count = size / sizeof(EnemyEntry); if (size != entry_count * sizeof(EnemyEntry)) { throw runtime_error("data size is not a multiple of entry size"); @@ -678,7 +711,7 @@ void Map::add_random_enemies_from_map_data( StringReader locations_segment_r, StringReader definitions_segment_r, uint32_t rare_seed, - const RareEnemyRates& rare_rates) { + std::shared_ptr rare_rates) { static const array rand_enemy_base_types = { 0x44, 0x43, 0x41, 0x42, 0x40, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x80, @@ -889,7 +922,7 @@ void Map::add_enemies_and_objects_from_quest_data( const void* data, size_t size, uint32_t rare_seed, - const RareEnemyRates& rare_rates) { + std::shared_ptr rare_rates) { auto all_floor_sections = this->collect_quest_map_data_sections(data, size); StringReader r(data, size); @@ -1311,25 +1344,5 @@ vector map_filenames_for_variation( return ret; } -const Map::RareEnemyRates Map::NO_RARE_ENEMIES = { - .hildeblue = 0x00000000, - .rappy = 0x00000000, - .nar_lily = 0x00000000, - .pouilly_slime = 0x00000000, - .merissa_aa = 0x00000000, - .pazuzu = 0x00000000, - .dorphon_eclair = 0x00000000, - .kondrieu = 0x00000000, -}; - -const Map::RareEnemyRates Map::DEFAULT_RARE_ENEMIES = { - // All 1/512 except Kondrieu, which is 1/10 - .hildeblue = 0x00800000, - .rappy = 0x00800000, - .nar_lily = 0x00800000, - .pouilly_slime = 0x00800000, - .merissa_aa = 0x00800000, - .pazuzu = 0x00800000, - .dorphon_eclair = 0x00800000, - .kondrieu = 0x1999999A, -}; +const shared_ptr Map::NO_RARE_ENEMIES = make_shared(0, 0); +const shared_ptr Map::DEFAULT_RARE_ENEMIES = make_shared(0x00800000, 0x1999999A); diff --git a/src/Map.hh b/src/Map.hh index 16872d37..441b264c 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -192,10 +193,15 @@ struct Map { uint32_t pazuzu; // ZU -> PAZUZU (and _ALT variants) uint32_t dorphon_eclair; // DORPHON -> DORPHON_ECLAIR uint32_t kondrieu; // {SAINT_MILLION, SHAMBERTIN} -> KONDRIEU + + RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate); + explicit RareEnemyRates(const JSON& json); + + JSON json() const; }; - static const RareEnemyRates NO_RARE_ENEMIES; - static const RareEnemyRates DEFAULT_RARE_ENEMIES; + static const std::shared_ptr NO_RARE_ENEMIES; + static const std::shared_ptr DEFAULT_RARE_ENEMIES; struct Object { // TODO: Add more fields in here if we ever care about them. Currently we @@ -249,7 +255,7 @@ struct Map { uint8_t floor, size_t index, const EnemyEntry& e, - const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); + std::shared_ptr rare_rates = DEFAULT_RARE_ENEMIES); void add_enemies_from_map_data( Episode episode, uint8_t difficulty, @@ -257,7 +263,7 @@ struct Map { uint8_t floor, const void* data, size_t size, - const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); + std::shared_ptr rare_rates = DEFAULT_RARE_ENEMIES); void add_random_enemies_from_map_data( Episode episode, uint8_t difficulty, @@ -267,7 +273,7 @@ struct Map { StringReader random_enemy_locations_r, StringReader random_enemy_definitions_r, uint32_t rare_seed, - const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); + std::shared_ptr rare_rates = DEFAULT_RARE_ENEMIES); struct DATSectionsForFloor { uint32_t objects = 0xFFFFFFFF; @@ -285,7 +291,7 @@ struct Map { const void* data, size_t size, uint32_t rare_seed, - const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); + std::shared_ptr rare_rates = Map::DEFAULT_RARE_ENEMIES); static std::string disassemble_quest_data(const void* data, size_t size); }; @@ -329,4 +335,3 @@ void generate_variations_dc_nte( 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); -void load_map_files(); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index cf506736..55515aae 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1863,7 +1863,7 @@ static void on_quest_loaded(shared_ptr l) { dat_contents.data(), dat_contents.size(), l->random_seed, - (l->mode == GameMode::CHALLENGE) ? Map::NO_RARE_ENEMIES : Map::DEFAULT_RARE_ENEMIES); + l->rare_enemy_rates ? l->rare_enemy_rates : Map::NO_RARE_ENEMIES); l->item_creator->clear_destroyed_entities(); l->log.info("Replaced objects list with quest layout (%zu entries)", l->map->objects.size()); @@ -3932,6 +3932,12 @@ shared_ptr create_game_generic( 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.at(game->difficulty); + } + if (game->base_version == Version::BB_V4) { game->map = make_shared(); for (size_t floor = 0; floor < 0x10; floor++) { @@ -3954,7 +3960,13 @@ shared_ptr create_game_generic( auto map_data = s->load_bb_file(filename, "", "map/" + filename); size_t start_offset = game->map->enemies.size(); game->map->add_enemies_from_map_data( - game->episode, game->difficulty, game->event, floor, map_data->data(), map_data->size()); + game->episode, + game->difficulty, + game->event, + floor, + map_data->data(), + map_data->size(), + game->rare_enemy_rates); size_t entries_loaded = game->map->enemies.size() - start_offset; c->log.info("[Map/%zu:e] Loaded %s (%zu entries)", floor, filename.c_str(), entries_loaded); for (size_t z = start_offset; z < game->map->enemies.size(); z++) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 480acc03..772b2da4 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2440,7 +2440,7 @@ static void on_battle_restart_bb(shared_ptr c, uint8_t, uint8_t, const v dat_contents.data(), dat_contents.size(), l->random_seed, - (l->mode == GameMode::CHALLENGE) ? Map::NO_RARE_ENEMIES : Map::DEFAULT_RARE_ENEMIES); + l->rare_enemy_rates ? l->rare_enemy_rates : Map::NO_RARE_ENEMIES); } } diff --git a/src/ServerState.cc b/src/ServerState.cc index dee5c6fd..69e18b15 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -913,6 +913,23 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { this->team_reward_defs_json = std::move(json.at("TeamRewards")); } catch (const out_of_range&) { } + + for (size_t z = 0; z < 4; z++) { + shared_ptr prev = Map::DEFAULT_RARE_ENEMIES; + try { + string key = "RareEnemyRates-"; + key += token_name_for_difficulty(z); + this->rare_enemy_rates[z] = make_shared(json.at(key)); + prev = this->rare_enemy_rates[z]; + } catch (const out_of_range&) { + this->rare_enemy_rates[z] = prev; + } + } + try { + this->rare_enemy_rates_challenge = make_shared(json.at("RareEnemyRates-Challenge")); + } catch (const out_of_range&) { + this->rare_enemy_rates_challenge = Map::DEFAULT_RARE_ENEMIES; + } } void ServerState::load_bb_private_keys() { diff --git a/src/ServerState.hh b/src/ServerState.hh index 8c4ab793..a255c9b7 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -114,6 +114,8 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr mag_evolution_table; std::shared_ptr item_name_index; std::shared_ptr word_select_table; + std::array, 4> rare_enemy_rates; + std::shared_ptr rare_enemy_rates_challenge; // Indexed as [type][difficulty][random_choice] std::vector>> quest_F95E_results; diff --git a/system/config.example.json b/system/config.example.json index 71122f81..3a92fc39 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -724,6 +724,31 @@ "ItemDropMode": "OnByDefault", "UseServerItemTables": "OffByDefault", + // Rare enemy rates for BB games. The default rates specified here match the + // original rates on the official servers. There is a hard limit of 16 rare + // enemies per room or quest, which you may run into if you increase these + // rates significantly. + // If no rates are specified for a difficulty, the previous difficulty's rates + // will be used. (In the default configuration, only Normal is specified, so + // all difficulties use the same rates.) + "RareEnemyRates-Normal": { + // These are probabilities out of 0xFFFFFFFF - so 0 means that rare enemy + // will never appear, and 0xFFFFFFFF means it will always appear (until 16 + // rare enemies have been assigned). + "Hildeblue": 0x00800000, + "Rappy": 0x00800000, + "NarLily": 0x00800000, + "PouillySlime": 0x00800000, + "MerissaAA": 0x00800000, + "Pazuzu": 0x00800000, + "DorphonEclair": 0x00800000, + "Kondrieu": 0x1999999A, + }, + // "RareEnemyRates-Hard": {...}, + // "RareEnemyRates-VeryHard": {...}, + // "RareEnemyRates-Ultimate": {...}, + // "RareEnemyRates-Challenge": {...}, + // Whether to enable certain exception handling. Disabling this causes // newserv to abort when any client causes an exception, which is generally // only useful for debugging newserv itself. This setting should usually be