diff --git a/TODO.md b/TODO.md index ecfe1a1f..258dad3d 100644 --- a/TODO.md +++ b/TODO.md @@ -38,7 +38,7 @@ ## PSOBB -- Find any remaining mismatches in enemy IDs / experience +- Find any remaining mismatches in enemy indexes / experience - Support EXP multipliers - Sale prices for non-rare weapons with specials are computed incorrectly when buying/selling at shops - Implement trade window diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index db0060fe..e6ec1b5a 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2724,7 +2724,7 @@ struct C_GuildCardDataRequest_BB_03DC { struct S_RareMonsterList_BB_DE { // Unused entries are set to FFFF - parray enemy_ids; + parray enemy_indexes; } __packed__; // DF (C->S): Set Challenge Mode parameters (BB) @@ -3646,7 +3646,7 @@ struct G_ClientIDHeader { struct G_EnemyIDHeader { uint8_t subcommand = 0; uint8_t size = 0; - le_uint16_t enemy_id = 0; // In range [0x1000, 0x4000) + le_uint16_t enemy_id = 0; // In [0x1000, 0x4000); not the same as enemy_index! } __packed__; struct G_ObjectIDHeader { uint8_t subcommand = 0; @@ -3775,8 +3775,7 @@ struct G_Unknown_6x09 { template struct G_EnemyHitByPlayer_6x0A { G_EnemyIDHeader header; - // Note: enemy_id (in header) is in the range [0x1000, 0x4000) - le_uint16_t enemy_id = 0; + le_uint16_t enemy_index = 0; // [0, 0xB50) le_uint16_t remaining_hp = 0; typename std::conditional::type flags = 0; } __packed__; @@ -3790,8 +3789,8 @@ struct G_EnemyHitByPlayer_DC_PC_XB_BB_6x0A : G_EnemyHitByPlayer_6x0A { struct G_BoxDestroyed_6x0B { G_ClientIDHeader header; - le_uint32_t unknown_a2 = 0; - le_uint32_t unknown_a3 = 0; + le_uint32_t flags = 0; + le_uint32_t object_index = 0; } __packed__; // 6x0C: Add condition (poison/slow/etc.) @@ -4528,15 +4527,13 @@ struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { // Decompressed format is a list of these struct G_SyncEnemyState_6x6B_Entry_Decompressed { // TODO: Verify this format on DC and PC. It appears correct for GC and BB. - le_uint32_t unknown_a1 = 0; // Possibly some kind of flags - // enemy_index is not the same as enemy_id, unfortunately - the enemy_id sent - // in the 6x76 command when an enemy is killed does not match enemy_index - le_uint16_t enemy_index = 0; // FFFF = enemy is dead - le_uint16_t damage_taken = 0; - uint8_t unknown_a4 = 0; - uint8_t unknown_a5 = 0; - uint8_t unknown_a6 = 0; - uint8_t unknown_a7 = 0; + le_uint32_t flags = 0; + le_uint16_t last_attacker = 0; + le_uint16_t remaining_hp = 0; + uint8_t red_buff_type = 0; + uint8_t red_buff_level = 0; + uint8_t blue_buff_type = 0; + uint8_t blue_buff_level = 0; } __packed__; // 6x6C: Sync object state (used while loading into game; same header format as 6E) @@ -5498,7 +5495,7 @@ struct G_MedicalCenterUsed_BB_6xC5 { struct G_StealEXP_BB_6xC6 { G_ClientIDHeader header; - le_uint16_t enemy_id = 0; + le_uint16_t enemy_index = 0; le_uint16_t unknown_a1 = 0; } __packed__; @@ -5515,7 +5512,7 @@ struct G_ChargeAttack_BB_6xC7 { struct G_EnemyKilled_BB_6xC8 { G_EnemyIDHeader header; - le_uint16_t enemy_id = 0; + le_uint16_t enemy_index = 0; le_uint16_t killer_client_id = 0; uint8_t unknown_a1 = 0; parray unused; diff --git a/src/CommonItemSet.hh b/src/CommonItemSet.hh index f788f91b..d038d354 100644 --- a/src/CommonItemSet.hh +++ b/src/CommonItemSet.hh @@ -116,7 +116,7 @@ public: // V2/V3: -> parray /* 14 */ U32T armor_slot_count_prob_table_offset; - // This array (indexed by enemy_id) specifies the range of meseta values + // This array (indexed by enemy_type) specifies the range of meseta values // that each enemy can drop. // V2/V3: -> parray, 0x64> /* 18 */ U32T enemy_meseta_ranges_offset; diff --git a/src/Map.cc b/src/Map.cc index f2eae199..a5117fb7 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -11,100 +11,394 @@ using namespace std; +static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f; + Map::Enemy::Enemy(EnemyType type) : type(type), flags(0), - last_hit_by_client_id(0) {} + last_hit_by_client_id(0) { +} string Map::Enemy::str() const { return string_printf("[Map::Enemy %s flags=%02hhX last_hit_by_client_id=%hu]", name_for_enum(this->type), this->flags, this->last_hit_by_client_id); } -struct EnemyEntry { - /* 00 */ le_uint16_t base_type; - /* 02 */ le_uint16_t unknown_a0; // Overwritten by client at load time - /* 04 */ le_uint16_t enemy_index; // Overwritten by client at load time - /* 06 */ le_uint16_t num_children; - /* 08 */ le_uint16_t area; - /* 0A */ le_uint16_t entity_id; // == enemy_index + 0x1000 - /* 0C */ le_uint16_t section; - /* 0E */ le_uint16_t wave_number; - /* 10 */ le_uint32_t wave_number2; - /* 14 */ le_float x; - /* 18 */ le_float y; - /* 1C */ le_float z; - /* 20 */ le_uint32_t x_angle; - /* 24 */ le_uint32_t y_angle; - /* 28 */ le_uint32_t z_angle; - /* 2C */ le_uint32_t unknown_a3; - /* 30 */ le_uint32_t unknown_a4; - /* 34 */ le_uint32_t unknown_a5; - /* 38 */ le_uint32_t unknown_a6; - /* 3C */ le_uint32_t unknown_a7; - /* 40 */ le_uint32_t skin; - /* 44 */ le_uint32_t unknown_a8; - /* 48 */ - - string str() const { - return string_printf("EnemyEntry(base_type=%hX, a0=%hX, enemy_index=%hX, num_children=%hX, area=%hX, entity_id=%hX, section=%hX, wave_number=%hX, wave_number2=%" PRIX32 ", x=%g, y=%g, z=%g, x_angle=%" PRIX32 ", y_angle=%" PRIX32 ", z_angle=%" PRIX32 ", a3=%" PRIX32 ", a4=%" PRIX32 ", a5=%" PRIX32 ", a6=%" PRIX32 ", a7=%" PRIX32 ", skin=%" PRIX32 ", a8=%" PRIX32 ")", - this->base_type.load(), this->unknown_a0.load(), this->enemy_index.load(), this->num_children.load(), this->area.load(), - this->entity_id.load(), this->section.load(), this->wave_number.load(), - this->wave_number2.load(), this->x.load(), this->y.load(), this->z.load(), this->x_angle.load(), - this->y_angle.load(), this->z_angle.load(), this->unknown_a3.load(), this->unknown_a4.load(), - this->unknown_a5.load(), this->unknown_a6.load(), this->unknown_a7.load(), this->skin.load(), - this->unknown_a8.load()); - } -} __attribute__((packed)); - -struct ObjectEntry { - /* 00 */ le_uint16_t base_type; - /* 02 */ le_uint16_t unknown_a1; - /* 04 */ le_uint32_t unknown_a2; - /* 08 */ le_uint16_t id; - /* 0A */ le_uint16_t group; - /* 0C */ le_uint16_t section; - /* 0E */ le_uint16_t unknown_a3; - /* 10 */ le_float x; - /* 14 */ le_float y; - /* 18 */ le_float z; - /* 1C */ le_uint32_t x_angle; - /* 20 */ le_uint32_t y_angle; - /* 24 */ le_uint32_t z_angle; - /* 28 */ le_float unknown_a4; - /* 2C */ le_float unknown_a5; - /* 30 */ le_float unknown_a6; - /* 34 */ le_uint32_t unknown_a7; - /* 38 */ le_uint32_t unknown_a8; - /* 3C */ le_uint32_t unknown_a9; - /* 40 */ le_uint32_t unknown_a10; - /* 44 */ - - string str() const { - return string_printf("ObjectEntry(base_type=%hX, a1=%hX, a2=%" PRIX32 ", id=%hX, group=%hX, section=%hX, a3=%hX, x=%g, y=%g, z=%g, x_angle=%" PRIX32 ", y_angle=%" PRIX32 ", z_angle=%" PRIX32 ", a4=%g, a5=%g, a6=%g, a7=%" PRIX32 ", a8=%" PRIX32 ", a9=%" PRIX32 ", a9=%" PRIX32 ")", - this->base_type.load(), this->unknown_a1.load(), this->unknown_a2.load(), this->id.load(), this->group.load(), - this->section.load(), this->unknown_a3.load(), this->x.load(), this->y.load(), this->z.load(), this->x_angle.load(), - this->y_angle.load(), this->z_angle.load(), this->unknown_a4.load(), this->unknown_a5.load(), this->unknown_a6.load(), - this->unknown_a7.load(), this->unknown_a8.load(), this->unknown_a9.load(), this->unknown_a9.load()); - } -} __attribute__((packed)); - void Map::clear() { this->enemies.clear(); this->rare_enemy_indexes.clear(); } void Map::add_objects_from_map_data(const void* data, size_t size) { - const auto* map = reinterpret_cast(data); size_t entry_count = size / sizeof(ObjectEntry); if (size != entry_count * sizeof(ObjectEntry)) { throw runtime_error("data size is not a multiple of entry size"); } - for (size_t y = 0; y < entry_count; y++) { - const auto& e = map[y]; - string hex = format_data_string(&e, sizeof(e)); - fprintf(stderr, "[%04zX] %s\n", y, hex.c_str()); + (void)data; + // TODO: Actually track objects, so we can e.g. know what to drop from fixed + // boxes + // const auto* map = reinterpret_cast(data); +} + +bool Map::check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate) { + if (default_is_rare) { + return true; + } + if ((this->rare_enemy_indexes.size() < 0x10) && (random_object() < rare_rate)) { + this->rare_enemy_indexes.emplace_back(this->enemies.size()); + return true; + } + return false; +} + +void Map::add_enemy(EnemyType type) { + static_game_data_log.info("Adding enemy E-%zX => %s", this->enemies.size(), name_for_enum(type)); + this->enemies.emplace_back(type); +} + +void Map::add_enemy( + Episode episode, + uint8_t difficulty, + uint8_t event, + size_t index, + const EnemyEntry& e, + const RareEnemyRates& rare_rates) { + switch (e.base_type) { + case 0x40: { + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.hildeblue); + this->add_enemy(is_rare ? EnemyType::HILDEBLUE : EnemyType::HILDEBEAR); + break; + } + case 0x41: { + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.rappy); + switch (episode) { + case Episode::EP1: + this->add_enemy(is_rare ? EnemyType::AL_RAPPY : EnemyType::RAG_RAPPY); + break; + case Episode::EP2: + if (is_rare) { + switch (event) { + case 0x01: + this->add_enemy(EnemyType::SAINT_RAPPY); + break; + case 0x04: + this->add_enemy(EnemyType::EGG_RAPPY); + break; + case 0x05: + this->add_enemy(EnemyType::HALLO_RAPPY); + break; + default: + this->add_enemy(EnemyType::LOVE_RAPPY); + } + } else { + this->add_enemy(EnemyType::RAG_RAPPY); + } + break; + case Episode::EP4: + if (e.floor > 0x05) { + this->add_enemy(is_rare ? EnemyType::DEL_RAPPY_ALT : EnemyType::SAND_RAPPY_ALT); + } else { + this->add_enemy(is_rare ? EnemyType::DEL_RAPPY : EnemyType::SAND_RAPPY); + } + break; + default: + throw logic_error("invalid episode"); + } + break; + } + case 0x42: { + this->add_enemy(EnemyType::MONEST); + for (size_t x = 0; x < 30; x++) { + this->add_enemy(EnemyType::MOTHMANT); + } + break; + } + case 0x43: { + this->add_enemy(e.fparam2 ? EnemyType::BARBAROUS_WOLF : EnemyType::SAVAGE_WOLF); + break; + } + case 0x44: + static const EnemyType types[3] = {EnemyType::BOOMA, EnemyType::GOBOOMA, EnemyType::GIGOBOOMA}; + this->add_enemy(types[e.uparam1 % 3]); + break; + case 0x60: + this->add_enemy(EnemyType::GRASS_ASSASSIN); + break; + case 0x61: + if ((episode == Episode::EP2) && (e.floor > 0x0F)) { + this->add_enemy(EnemyType::DEL_LILY); + } else { + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.nar_lily); + this->add_enemy(is_rare ? EnemyType::NAR_LILY : EnemyType::POISON_LILY); + } + break; + case 0x62: + this->add_enemy(EnemyType::NANO_DRAGON); + break; + case 0x63: { + static const EnemyType types[3] = {EnemyType::EVIL_SHARK, EnemyType::PAL_SHARK, EnemyType::GUIL_SHARK}; + this->add_enemy(types[e.uparam1 % 3]); + break; + } + case 0x64: { + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.pouilly_slime); + for (size_t x = 0; x < 5; x++) { // Main slime + 4 clones + this->add_enemy(is_rare ? EnemyType::POFUILLY_SLIME : EnemyType::POUILLY_SLIME); + } + break; + } + case 0x65: + this->add_enemy(EnemyType::PAN_ARMS); + this->add_enemy(EnemyType::HIDOOM); + this->add_enemy(EnemyType::MIGIUM); + break; + case 0x80: + this->add_enemy((e.uparam1 & 0x01) ? EnemyType::GILLCHIC : EnemyType::DUBCHIC); + break; + case 0x81: + this->add_enemy(EnemyType::GARANZ); + break; + case 0x82: { + EnemyType type = e.fparam2 ? EnemyType::SINOW_GOLD : EnemyType::SINOW_BEAT; + size_t count = (e.num_children == 0) ? 5 : (e.num_children + 1); + for (size_t z = 0; z < count; z++) { + this->add_enemy(type); + } + break; + } + case 0x83: + this->add_enemy(EnemyType::CANADINE); + break; + case 0x84: + this->add_enemy(EnemyType::CANANE); + for (size_t x = 0; x < 8; x++) { + this->add_enemy(EnemyType::CANADINE_GROUP); + } + break; + case 0x85: + this->add_enemy(EnemyType::DUBWITCH); + break; + case 0xA0: + this->add_enemy(EnemyType::DELSABER); + break; + case 0xA1: + this->add_enemy(EnemyType::CHAOS_SORCERER); + this->add_enemy(EnemyType::BEE_R); + this->add_enemy(EnemyType::BEE_L); + break; + case 0xA2: + this->add_enemy(EnemyType::DARK_GUNNER); + break; + case 0xA3: + this->add_enemy(EnemyType::DEATH_GUNNER); + break; + case 0xA4: + this->add_enemy(EnemyType::CHAOS_BRINGER); + break; + case 0xA5: + this->add_enemy(EnemyType::DARK_BELRA); + break; + case 0xA6: { + static const EnemyType types[3] = {EnemyType::DIMENIAN, EnemyType::LA_DIMENIAN, EnemyType::SO_DIMENIAN}; + this->add_enemy(types[e.uparam1 % 3]); + break; + } + case 0xA7: + this->add_enemy(EnemyType::BULCLAW); + for (size_t x = 0; x < 4; x++) { + this->add_enemy(EnemyType::CLAW); + } + break; + case 0xA8: + this->add_enemy(EnemyType::CLAW); + break; + case 0xC0: + if (episode == Episode::EP1) { + this->add_enemy(EnemyType::DRAGON); + } else if (episode == Episode::EP2) { + this->add_enemy(EnemyType::GAL_GRYPHON); + } else { + throw runtime_error("DRAGON-type enemy placed outside of Episodes 1 or 2"); + } + break; + case 0xC1: + this->add_enemy(EnemyType::DE_ROL_LE); + for (size_t z = 0; z < 0x0A; z++) { + this->add_enemy(EnemyType::DE_ROL_LE_BODY); + } + for (size_t z = 0; z < 0x09; z++) { + this->add_enemy(EnemyType::DE_ROL_LE_MINE); + } + break; + case 0xC2: + this->add_enemy(EnemyType::VOL_OPT_1); + for (size_t z = 0; z < 6; z++) { + this->add_enemy(EnemyType::VOL_OPT_PILLAR); + } + for (size_t z = 0; z < 24; z++) { + this->add_enemy(EnemyType::VOL_OPT_MONITOR); + } + for (size_t z = 0; z < 2; z++) { + this->add_enemy(EnemyType::NONE); + } + this->add_enemy(EnemyType::VOL_OPT_AMP); + this->add_enemy(EnemyType::VOL_OPT_CORE); + this->add_enemy(EnemyType::NONE); + break; + case 0xC5: + this->add_enemy(EnemyType::VOL_OPT_2); + break; + case 0xC8: + if (difficulty) { + this->add_enemy(EnemyType::DARK_FALZ_3); + } else { + this->add_enemy(EnemyType::DARK_FALZ_2); + } + for (size_t x = 0; x < 0x1FD; x++) { + this->add_enemy(difficulty == 3 ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT); + } + this->add_enemy(EnemyType::DARK_FALZ_3); + this->add_enemy(EnemyType::DARK_FALZ_2); + this->add_enemy(EnemyType::DARK_FALZ_1); + break; + case 0xCA: + for (size_t z = 0; z < 0x201; z++) { + this->add_enemy(EnemyType::OLGA_FLOW_2); + } + break; + case 0xCB: + this->add_enemy(EnemyType::BARBA_RAY); + for (size_t z = 0; z < 0x2F; z++) { + this->add_enemy(EnemyType::PIG_RAY); + } + break; + case 0xCC: + for (size_t z = 0; z < 6; z++) { + this->add_enemy(EnemyType::GOL_DRAGON); + } + break; + case 0xD4: { + EnemyType type = (e.uparam1 & 1) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL; + for (size_t z = 0; z < 5; z++) { + this->add_enemy(type); + } + break; + } + case 0xD5: + this->add_enemy((e.uparam1 & 0x01) ? EnemyType::MERILTAS : EnemyType::MERILLIA); + break; + case 0xD6: + if (e.uparam1 == 0) { + this->add_enemy(EnemyType::MERICAROL); + } else { + this->add_enemy(((e.uparam1 % 3) == 2) ? EnemyType::MERICUS : EnemyType::MERIKLE); + } + break; + case 0xD7: + this->add_enemy((e.uparam1 & 0x01) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); + break; + case 0xD8: + this->add_enemy(EnemyType::GIBBLES); + break; + case 0xD9: + this->add_enemy(EnemyType::GEE); + break; + case 0xDA: + this->add_enemy(EnemyType::GI_GUE); + break; + case 0xDB: + this->add_enemy(EnemyType::DELDEPTH); + break; + case 0xDC: + this->add_enemy(EnemyType::DELBITER); + break; + case 0xDD: + this->add_enemy((e.uparam1 & 0x01) ? EnemyType::DOLMDARL : EnemyType::DOLMOLM); + break; + case 0xDE: + this->add_enemy(EnemyType::MORFOS); + break; + case 0xDF: + this->add_enemy(EnemyType::RECOBOX); + for (size_t x = 0; x < e.num_children; x++) { + this->add_enemy(EnemyType::RECON); + } + break; + case 0xE0: + if ((episode == Episode::EP2) && (e.floor > 0x0F)) { + this->add_enemy(EnemyType::EPSILON); + for (size_t z = 0; z < 4; z++) { + this->add_enemy(EnemyType::EPSIGUARD); + } + } else { + this->add_enemy((e.uparam1 & 0x01) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); + } + break; + case 0xE1: + this->add_enemy(EnemyType::ILL_GILL); + break; + case 0x0110: + this->add_enemy(EnemyType::ASTARK); + break; + case 0x0111: + if (e.floor > 0x05) { + this->add_enemy(e.fparam2 ? EnemyType::YOWIE_ALT : EnemyType::SATELLITE_LIZARD_ALT); + } else { + this->add_enemy(e.fparam2 ? EnemyType::YOWIE : EnemyType::SATELLITE_LIZARD); + } + break; + case 0x0112: { + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.merissa_aa); + this->add_enemy(is_rare ? EnemyType::MERISSA_AA : EnemyType::MERISSA_A); + break; + } + case 0x0113: + this->add_enemy(EnemyType::GIRTABLULU); + break; + case 0x0114: { + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.pazuzu); + if (e.floor > 0x05) { + this->add_enemy(is_rare ? EnemyType::PAZUZU_ALT : EnemyType::ZU_ALT); + } else { + this->add_enemy(is_rare ? EnemyType::PAZUZU : EnemyType::ZU); + } + break; + } + case 0x0115: + if (e.uparam1 & 2) { + this->add_enemy(EnemyType::BA_BOOTA); + } else { + this->add_enemy((e.uparam1 & 1) ? EnemyType::ZE_BOOTA : EnemyType::BOOTA); + } + break; + case 0x0116: { + bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.dorphon_eclair); + this->add_enemy(is_rare ? EnemyType::DORPHON_ECLAIR : EnemyType::DORPHON); + break; + } + case 0x0117: { + static const EnemyType types[3] = {EnemyType::GORAN, EnemyType::PYRO_GORAN, EnemyType::GORAN_DETONATOR}; + this->add_enemy(types[e.uparam1 % 3]); + break; + } + case 0x0119: { + bool is_rare = this->check_and_log_rare_enemy((e.fparam2 != 0.0f), rare_rates.kondrieu); + if (is_rare) { + this->add_enemy(EnemyType::KONDRIEU); + } else { + this->add_enemy((e.uparam1 & 1) ? EnemyType::SHAMBERTIN : EnemyType::SAINT_MILLION); + } + break; + } + default: + for (size_t z = 0; z < static_cast(e.num_children + 1); z++) { + this->add_enemy(EnemyType::UNKNOWN); + } + static_game_data_log.warning( + "(Entry %zu, offset %zX in file) Unknown enemy type %04hX", + index, index * sizeof(EnemyEntry), e.base_type.load()); + break; } } @@ -115,375 +409,239 @@ void Map::add_enemies_from_map_data( const void* data, size_t size, const RareEnemyRates& rare_rates) { - auto check_rare = [&](bool default_is_rare, uint32_t rare_rate) -> bool { - if (default_is_rare) { - return true; - } - if ((this->rare_enemy_indexes.size() < 0x10) && - (random_object() < rare_rate)) { - this->rare_enemy_indexes.emplace_back(this->enemies.size()); - return true; - } - return false; - }; - - const auto* map = reinterpret_cast(data); 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"); } + StringReader r(data, size); for (size_t y = 0; y < entry_count; y++) { - const auto& e = map[y]; - - string hex = format_data_string(&e, sizeof(e)); - fprintf(stderr, "[%04zX] %s\n", y, hex.c_str()); - - switch (e.base_type) { - case 0x40: { - bool is_rare = check_rare(e.skin & 0x01, rare_rates.hildeblue); - this->enemies.emplace_back(is_rare ? EnemyType::HILDEBLUE : EnemyType::HILDEBEAR); - break; - } - case 0x41: { - bool is_rare = check_rare(e.skin & 0x01, rare_rates.rappy); - switch (episode) { - case Episode::EP1: - this->enemies.emplace_back(is_rare ? EnemyType::AL_RAPPY : EnemyType::RAG_RAPPY); - break; - case Episode::EP2: - if (is_rare) { - switch (event) { - case 0x01: - this->enemies.emplace_back(EnemyType::SAINT_RAPPY); - break; - case 0x04: - this->enemies.emplace_back(EnemyType::EGG_RAPPY); - break; - case 0x05: - this->enemies.emplace_back(EnemyType::HALLO_RAPPY); - break; - default: - this->enemies.emplace_back(EnemyType::LOVE_RAPPY); - } - } else { - this->enemies.emplace_back(EnemyType::RAG_RAPPY); - } - break; - case Episode::EP4: - if (e.area > 0x05) { - this->enemies.emplace_back(is_rare ? EnemyType::DEL_RAPPY_ALT : EnemyType::SAND_RAPPY_ALT); - } else { - this->enemies.emplace_back(is_rare ? EnemyType::DEL_RAPPY : EnemyType::SAND_RAPPY); - } - break; - default: - throw logic_error("invalid episode"); - } - break; - } - case 0x42: { - this->enemies.emplace_back(EnemyType::MONEST); - for (size_t x = 0; x < 30; x++) { - this->enemies.emplace_back(EnemyType::MOTHMANT); - } - break; - } - case 0x43: { - this->enemies.emplace_back(e.unknown_a4 ? EnemyType::BARBAROUS_WOLF : EnemyType::SAVAGE_WOLF); - break; - } - case 0x44: - static const EnemyType types[3] = {EnemyType::BOOMA, EnemyType::GOBOOMA, EnemyType::GIGOBOOMA}; - this->enemies.emplace_back(types[e.skin % 3]); - break; - case 0x60: - this->enemies.emplace_back(EnemyType::GRASS_ASSASSIN); - break; - case 0x61: - if ((episode == Episode::EP2) && (e.area > 0x0F)) { - this->enemies.emplace_back(EnemyType::DEL_LILY); - } else { - bool is_rare = check_rare(e.skin & 0x01, rare_rates.nar_lily); - this->enemies.emplace_back(is_rare ? EnemyType::NAR_LILY : EnemyType::POISON_LILY); - } - break; - case 0x62: - this->enemies.emplace_back(EnemyType::NANO_DRAGON); - break; - case 0x63: { - static const EnemyType types[3] = {EnemyType::EVIL_SHARK, EnemyType::PAL_SHARK, EnemyType::GUIL_SHARK}; - this->enemies.emplace_back(types[e.skin % 3]); - break; - } - case 0x64: { - bool is_rare = check_rare(e.skin & 0x01, rare_rates.pouilly_slime); - for (size_t x = 0; x < 5; x++) { // Main slime + 4 clones - this->enemies.emplace_back(is_rare ? EnemyType::POFUILLY_SLIME : EnemyType::POUILLY_SLIME); - } - break; - } - case 0x65: - this->enemies.emplace_back(EnemyType::PAN_ARMS); - this->enemies.emplace_back(EnemyType::HIDOOM); - this->enemies.emplace_back(EnemyType::MIGIUM); - break; - case 0x80: - this->enemies.emplace_back((e.skin & 0x01) ? EnemyType::GILLCHIC : EnemyType::DUBCHIC); - break; - case 0x81: - this->enemies.emplace_back(EnemyType::GARANZ); - break; - case 0x82: { - EnemyType type = e.unknown_a4 ? EnemyType::SINOW_GOLD : EnemyType::SINOW_BEAT; - size_t count = (e.num_children == 0) ? 5 : (e.num_children + 1); - for (size_t z = 0; z < count; z++) { - this->enemies.emplace_back(type); - } - break; - } - case 0x83: - this->enemies.emplace_back(EnemyType::CANADINE); - break; - case 0x84: - this->enemies.emplace_back(EnemyType::CANANE); - for (size_t x = 0; x < 8; x++) { - this->enemies.emplace_back(EnemyType::CANADINE_GROUP); - } - break; - case 0x85: - this->enemies.emplace_back(EnemyType::DUBWITCH); - break; - case 0xA0: - this->enemies.emplace_back(EnemyType::DELSABER); - break; - case 0xA1: - this->enemies.emplace_back(EnemyType::CHAOS_SORCERER); - this->enemies.emplace_back(EnemyType::BEE_R); - this->enemies.emplace_back(EnemyType::BEE_L); - break; - case 0xA2: - this->enemies.emplace_back(EnemyType::DARK_GUNNER); - break; - case 0xA3: - this->enemies.emplace_back(EnemyType::DEATH_GUNNER); - break; - case 0xA4: - this->enemies.emplace_back(EnemyType::CHAOS_BRINGER); - break; - case 0xA5: - this->enemies.emplace_back(EnemyType::DARK_BELRA); - break; - case 0xA6: { - static const EnemyType types[3] = {EnemyType::DIMENIAN, EnemyType::LA_DIMENIAN, EnemyType::SO_DIMENIAN}; - this->enemies.emplace_back(types[e.skin % 3]); - break; - } - case 0xA7: - this->enemies.emplace_back(EnemyType::BULCLAW); - for (size_t x = 0; x < 4; x++) { - this->enemies.emplace_back(EnemyType::CLAW); - } - break; - case 0xA8: - this->enemies.emplace_back(EnemyType::CLAW); - break; - case 0xC0: - if (episode == Episode::EP1) { - this->enemies.emplace_back(EnemyType::DRAGON); - } else if (episode == Episode::EP2) { - this->enemies.emplace_back(EnemyType::GAL_GRYPHON); - } else { - throw runtime_error("DRAGON-type enemy placed outside of Episodes 1 or 2"); - } - break; - case 0xC1: - this->enemies.emplace_back(EnemyType::DE_ROL_LE); - for (size_t z = 0; z < 0x0A; z++) { - this->enemies.emplace_back(EnemyType::DE_ROL_LE_BODY); - } - for (size_t z = 0; z < 0x09; z++) { - this->enemies.emplace_back(EnemyType::DE_ROL_LE_MINE); - } - break; - case 0xC2: - this->enemies.emplace_back(EnemyType::VOL_OPT_1); - for (size_t z = 0; z < 6; z++) { - this->enemies.emplace_back(EnemyType::VOL_OPT_PILLAR); - } - for (size_t z = 0; z < 24; z++) { - this->enemies.emplace_back(EnemyType::VOL_OPT_MONITOR); - } - for (size_t z = 0; z < 2; z++) { - this->enemies.emplace_back(EnemyType::NONE); - } - this->enemies.emplace_back(EnemyType::VOL_OPT_AMP); - this->enemies.emplace_back(EnemyType::VOL_OPT_CORE); - this->enemies.emplace_back(EnemyType::NONE); - break; - case 0xC5: - this->enemies.emplace_back(EnemyType::VOL_OPT_2); - break; - case 0xC8: - if (difficulty) { - this->enemies.emplace_back(EnemyType::DARK_FALZ_3); - } else { - this->enemies.emplace_back(EnemyType::DARK_FALZ_2); - } - for (size_t x = 0; x < 0x1FD; x++) { - this->enemies.emplace_back(difficulty == 3 ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT); - } - this->enemies.emplace_back(EnemyType::DARK_FALZ_3); - this->enemies.emplace_back(EnemyType::DARK_FALZ_2); - this->enemies.emplace_back(EnemyType::DARK_FALZ_1); - break; - case 0xCA: - for (size_t z = 0; z < 0x201; z++) { - this->enemies.emplace_back(EnemyType::OLGA_FLOW_2); - } - break; - case 0xCB: - this->enemies.emplace_back(EnemyType::BARBA_RAY); - for (size_t z = 0; z < 0x2F; z++) { - this->enemies.emplace_back(EnemyType::PIG_RAY); - } - break; - case 0xCC: - for (size_t z = 0; z < 6; z++) { - this->enemies.emplace_back(EnemyType::GOL_DRAGON); - } - break; - case 0xD4: { - EnemyType type = (e.skin & 1) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL; - for (size_t z = 0; z < 5; z++) { - this->enemies.emplace_back(type); - } - break; - } - case 0xD5: - this->enemies.emplace_back((e.skin & 0x01) ? EnemyType::MERILTAS : EnemyType::MERILLIA); - break; - case 0xD6: - if (e.skin == 0) { - this->enemies.emplace_back(EnemyType::MERICAROL); - } else { - this->enemies.emplace_back(((e.skin % 3) == 2) ? EnemyType::MERICUS : EnemyType::MERIKLE); - } - break; - case 0xD7: - this->enemies.emplace_back((e.skin & 0x01) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); - break; - case 0xD8: - this->enemies.emplace_back(EnemyType::GIBBLES); - break; - case 0xD9: - this->enemies.emplace_back(EnemyType::GEE); - break; - case 0xDA: - this->enemies.emplace_back(EnemyType::GI_GUE); - break; - case 0xDB: - this->enemies.emplace_back(EnemyType::DELDEPTH); - break; - case 0xDC: - this->enemies.emplace_back(EnemyType::DELBITER); - break; - case 0xDD: - this->enemies.emplace_back((e.skin & 0x01) ? EnemyType::DOLMDARL : EnemyType::DOLMOLM); - break; - case 0xDE: - this->enemies.emplace_back(EnemyType::MORFOS); - break; - case 0xDF: - this->enemies.emplace_back(EnemyType::RECOBOX); - for (size_t x = 0; x < e.num_children; x++) { - this->enemies.emplace_back(EnemyType::RECON); - } - break; - case 0xE0: - if ((episode == Episode::EP2) && (e.area > 0x0F)) { - this->enemies.emplace_back(EnemyType::EPSILON); - for (size_t z = 0; z < 4; z++) { - this->enemies.emplace_back(EnemyType::EPSIGUARD); - } - } else { - this->enemies.emplace_back((e.skin & 0x01) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); - } - break; - case 0xE1: - this->enemies.emplace_back(EnemyType::ILL_GILL); - break; - case 0x0110: - this->enemies.emplace_back(EnemyType::ASTARK); - break; - case 0x0111: - if (e.area > 0x05) { - this->enemies.emplace_back(e.unknown_a4 ? EnemyType::YOWIE_ALT : EnemyType::SATELLITE_LIZARD_ALT); - } else { - this->enemies.emplace_back(e.unknown_a4 ? EnemyType::YOWIE : EnemyType::SATELLITE_LIZARD); - } - break; - case 0x0112: { - bool is_rare = check_rare(e.skin & 0x01, rare_rates.merissa_aa); - this->enemies.emplace_back(is_rare ? EnemyType::MERISSA_AA : EnemyType::MERISSA_A); - break; - } - case 0x0113: - this->enemies.emplace_back(EnemyType::GIRTABLULU); - break; - case 0x0114: { - bool is_rare = check_rare(e.skin & 0x01, rare_rates.pazuzu); - if (e.area > 0x05) { - this->enemies.emplace_back(is_rare ? EnemyType::PAZUZU_ALT : EnemyType::ZU_ALT); - } else { - this->enemies.emplace_back(is_rare ? EnemyType::PAZUZU : EnemyType::ZU); - } - break; - } - case 0x0115: - if (e.skin & 2) { - this->enemies.emplace_back(EnemyType::BA_BOOTA); - } else { - this->enemies.emplace_back((e.skin & 1) ? EnemyType::ZE_BOOTA : EnemyType::BOOTA); - } - break; - case 0x0116: { - bool is_rare = check_rare(e.skin & 0x01, rare_rates.dorphon_eclair); - this->enemies.emplace_back(is_rare ? EnemyType::DORPHON_ECLAIR : EnemyType::DORPHON); - break; - } - case 0x0117: { - static const EnemyType types[3] = {EnemyType::GORAN, EnemyType::PYRO_GORAN, EnemyType::GORAN_DETONATOR}; - this->enemies.emplace_back(types[e.skin % 3]); - break; - } - case 0x0119: { - bool is_rare = check_rare((e.unknown_a4 != 0), rare_rates.kondrieu); - if (is_rare) { - this->enemies.emplace_back(EnemyType::KONDRIEU); - } else { - this->enemies.emplace_back((e.skin & 1) ? EnemyType::SHAMBERTIN : EnemyType::SAINT_MILLION); - } - break; - } - default: - for (size_t z = 0; z < static_cast(e.num_children + 1); z++) { - this->enemies.emplace_back(EnemyType::UNKNOWN); - } - static_game_data_log.warning( - "(Entry %zu, offset %zX in file) Unknown enemy type %04hX", - y, y * sizeof(EnemyEntry), e.base_type.load()); - break; - } + this->add_enemy(episode, difficulty, event, y, r.get(), rare_rates); } } -struct DATSectionHeader { - le_uint32_t type; // 1 = objects, 2 = enemies. There are other types too - le_uint32_t section_size; // Includes this header - le_uint32_t area; - le_uint32_t data_size; -} __attribute__((packed)); +struct DATParserRandomState { + PSOV2Encryption global_random_crypt; + PSOV2Encryption local_random_crypt; + std::array location_index_table; + uint32_t location_indexes_populated; + uint32_t location_indexes_used; + uint32_t location_entries_base_offset; + + DATParserRandomState(uint32_t rare_seed) + : global_random_crypt(rare_seed), + local_random_crypt(0), + location_indexes_populated(0), + location_indexes_used(0), + location_entries_base_offset(0) { + this->location_index_table.fill(0); + } + + size_t rand_int_biased(size_t min_v, size_t max_v) { + float max_f = static_cast(max_v + 1); + uint32_t crypt_v = this->global_random_crypt.next(); + fprintf(stderr, "(global) => %08" PRIX32 "\n", crypt_v); + float det_f = static_cast(crypt_v); + return max(floorf((max_f * det_f) / UINT32_MAX_AS_FLOAT), min_v); + } + + uint32_t next_location_index() { + if (this->location_indexes_used < this->location_indexes_populated) { + return this->location_index_table.at(this->location_indexes_used++); + } + return 0; + } + + void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section) { + if (header.num_sections == 0) { + throw runtime_error("no locations defined"); + } + + StringReader sections_r = r.sub(header.section_table_offset, header.num_sections * sizeof(Map::RandomEnemyLocationSection)); + + size_t bs_min = 0; + size_t bs_max = header.num_sections - 1; + do { + size_t bs_mid = (bs_min + bs_max) / 2; + if (sections_r.pget(bs_mid * sizeof(Map::RandomEnemyLocationSection)).section < section) { + bs_min = bs_mid + 1; + } else { + bs_max = bs_mid; + } + } while (bs_min < bs_max); + + const auto& sec = sections_r.pget(bs_min * sizeof(Map::RandomEnemyLocationSection)); + if (section != sec.section) { + return; + } + + this->location_indexes_populated = sec.count; + this->location_indexes_used = 0; + this->location_entries_base_offset = sec.offset; + for (size_t z = 0; z < sec.count; z++) { + this->location_index_table.at(z) = z; + } + + for (size_t z = 0; z < 4; z++) { + for (size_t x = 0; x < sec.count; x++) { + uint32_t crypt_v = this->local_random_crypt.next(); + fprintf(stderr, "(local?) => %08" PRIX32 "\n", crypt_v); + size_t choice = floorf((static_cast(sec.count) * static_cast(crypt_v)) / UINT32_MAX_AS_FLOAT); + uint32_t t = this->location_index_table[x]; + this->location_index_table[x] = this->location_index_table[choice]; + this->location_index_table[choice] = t; + } + } + } +}; + +void Map::add_random_enemies_from_map_data( + Episode episode, + uint8_t difficulty, + uint8_t event, + StringReader wave_events_segment_r, + StringReader locations_segment_r, + StringReader definitions_segment_r, + uint32_t rare_seed, + const RareEnemyRates& rare_rates) { + + static const array rand_enemy_base_types = { + 0x44, 0x43, 0x41, 0x42, 0x40, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x80, + 0x81, 0x82, 0x83, 0x84, 0x85, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, + 0xA7, 0xA8, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, + 0xDE, 0xDF, 0xE0, 0xE0, 0xE1}; + + const auto& wave_events_header = wave_events_segment_r.get(); + if (wave_events_header.format != 0x65767432) { // 'evt2' + throw runtime_error("cannot generate random enemies from non-evt2 event stream"); + } + wave_events_segment_r.go(wave_events_header.entries_offset); + + const auto& locations_header = locations_segment_r.get(); + const auto& definitions_header = definitions_segment_r.get(); + auto definitions_r = definitions_segment_r.sub( + definitions_header.entries_offset, + definitions_header.entry_count * sizeof(RandomEnemyDefinition)); + auto weights_r = definitions_segment_r.sub( + definitions_header.weight_entries_offset, + definitions_header.weight_entry_count * sizeof(RandomEnemyWeight)); + + DATParserRandomState random(rare_seed); + + for (size_t wave_entry_index = 0; wave_entry_index < wave_events_header.entry_count; wave_entry_index++) { + auto entry_log = static_game_data_log.sub(string_printf("(Entry %zu/%" PRIu32 ") ", wave_entry_index, wave_events_header.entry_count.load())); + entry_log.info("Start"); + const auto& entry = wave_events_segment_r.get(); + + size_t remaining_waves = random.rand_int_biased(1, entry.max_waves); + entry_log.info("Chose %zu waves (max=%hu)", remaining_waves, entry.max_waves.load()); + // BP 0080E125 EAX is wave count + + uint32_t wave_number = entry.wave_number; + while (remaining_waves) { + remaining_waves--; + auto wave_log = entry_log.sub(string_printf("(Wave %zu) ", remaining_waves)); + + size_t remaining_enemies = random.rand_int_biased(entry.min_enemies, entry.max_enemies); + wave_log.info("Chose %zu enemies (range=[%hhu, %hhu])", remaining_enemies, entry.min_enemies, entry.max_enemies); + // BP 0080E208 EDI is enemy count + + random.generate_shuffled_location_table(locations_header, locations_segment_r, entry.section); + wave_log.info("Generated shuffled location table"); + for (size_t z = 0; z < random.location_indexes_populated; z++) { + wave_log.info(" table[%zX] = %" PRIX32, z, random.location_index_table[z]); + } + // BP 0080EBB0 *(EBP + 4) points to table (0x20 uint32_ts) + + while (remaining_enemies) { + remaining_enemies--; + auto enemy_log = wave_log.sub(string_printf("(Enemy %zu) ", remaining_enemies)); + + // TODO: Factor this sum out of the loops + weights_r.go(0); + size_t weight_total = 0; + while (!weights_r.eof()) { + weight_total += weights_r.get().weight; + } + // BP 0080E2C2 EBX is weight_total + + size_t det = random.rand_int_biased(0, weight_total - 1); + enemy_log.info("weight_total=%zX, det=%zX", weight_total, det); + // BP 0080E300 EDX is det + + weights_r.go(0); + while (!weights_r.eof()) { + const auto& weight_entry = weights_r.get(); + if (det < weight_entry.weight) { + if ((weight_entry.base_type_index != 0xFF) && (weight_entry.definition_entry_num != 0xFF)) { + EnemyEntry e; + e.base_type = rand_enemy_base_types.at(weight_entry.base_type_index); + e.wave_number = wave_number; + e.section = entry.section; + + size_t bs_min = 0; + size_t bs_max = definitions_header.entry_count - 1; + if (bs_max == 0) { + throw runtime_error("no available random enemy definitions"); + } + do { + size_t bs_mid = (bs_min + bs_max) / 2; + if (definitions_r.pget(bs_mid * sizeof(RandomEnemyDefinition)).entry_num < weight_entry.definition_entry_num) { + bs_min = bs_mid + 1; + } else { + bs_max = bs_mid; + } + } while (bs_min < bs_max); + + const auto& def = definitions_r.pget(bs_min * sizeof(RandomEnemyDefinition)); + if (def.entry_num == weight_entry.definition_entry_num) { + e.fparam1 = def.fparam1; + e.fparam2 = def.fparam2; + e.fparam3 = def.fparam3; + e.fparam4 = def.fparam4; + e.fparam5 = def.fparam5; + e.uparam1 = def.uparam1; + e.uparam2 = def.uparam2; + e.num_children = random.rand_int_biased(def.min_children, def.max_children); + } else { + throw runtime_error("random enemy definition not found"); + } + + const auto& loc = locations_segment_r.pget( + locations_header.entries_offset + sizeof(RandomEnemyLocationEntry) * random.next_location_index()); + e.x = loc.x; + e.y = loc.y; + e.z = loc.z; + e.x_angle = loc.x_angle; + e.y_angle = loc.y_angle; + e.z_angle = loc.z_angle; + + enemy_log.info("Creating enemy with base_type %04hX fparam2 %g uparam1 %04hX", e.base_type.load(), e.fparam2.load(), e.uparam1.load()); + // BP 0080E6FE CX is base_type + this->add_enemy(episode, difficulty, event, 0, e, rare_rates); + } else { + enemy_log.info("Cannot create enemy: parameters are missing"); + } + break; + } else { + det -= weight_entry.weight; + } + } + } + if (remaining_waves) { + // We don't generate the event stream here, but the client does, and in + // doing so, it uses one value from global_random_crypt to determine the + // delay parameter of the event. To keep our state in sync with what the + // client would do, we skip a random value here. + random.global_random_crypt.next(); + wave_number++; + } + } + + // For the same reason as above, we need to skip another random value here. + random.global_random_crypt.next(); + } +} void Map::add_enemies_and_objects_from_quest_data( Episode episode, @@ -491,35 +649,119 @@ void Map::add_enemies_and_objects_from_quest_data( uint8_t event, const void* data, size_t size, + uint32_t rare_seed, const RareEnemyRates& rare_rates) { + + struct DATSectionsForFloor { + uint32_t objects = 0xFFFFFFFF; + uint32_t enemies = 0xFFFFFFFF; + uint32_t wave_events = 0xFFFFFFFF; + uint32_t random_enemy_locations = 0xFFFFFFFF; + uint32_t random_enemy_definitions = 0xFFFFFFFF; + }; + + vector floor_sections; StringReader r(data, size); while (!r.eof()) { - const auto& header = r.get(); - static_game_data_log.info("(DAT:%08zX) type=%08" PRIX32 " area=%08" PRIX32 " data_size=%08" PRIX32, - r.where() - sizeof(DATSectionHeader), header.type.load(), header.area.load(), header.data_size.load()); + size_t header_offset = r.where(); + const auto& header = r.get(); + static_game_data_log.info("(DAT:%08zX) type=%08" PRIX32 " floor=%08" PRIX32 " data_size=%08" PRIX32, + header_offset, header.le_type.load(), header.floor.load(), header.data_size.load()); - if (header.type == 0 && header.section_size == 0) { + if (header.type() == SectionHeader::Type::END && header.section_size == 0) { break; } if (header.section_size < sizeof(header)) { throw runtime_error(string_printf("quest layout has invalid section header at offset 0x%zX", r.where() - sizeof(header))); } - if (header.type == 1) { + if (header.floor > 0x100) { + throw runtime_error("section floor number too large"); + } + + if (header.floor >= floor_sections.size()) { + floor_sections.resize(header.floor + 1); + } + auto& sections = floor_sections[header.floor]; + switch (header.type()) { + case SectionHeader::Type::OBJECTS: + if (sections.objects != 0xFFFFFFFF) { + throw runtime_error("multiple objects sections for same floor"); + } + sections.objects = header_offset; + break; + case SectionHeader::Type::ENEMIES: + if (sections.enemies != 0xFFFFFFFF) { + throw runtime_error("multiple enemies sections for same floor"); + } + sections.enemies = header_offset; + break; + case SectionHeader::Type::WAVE_EVENTS: + if (sections.wave_events != 0xFFFFFFFF) { + throw runtime_error("multiple wave events sections for same floor"); + } + sections.wave_events = header_offset; + break; + case SectionHeader::Type::RANDOM_ENEMY_LOCATIONS: + if (sections.random_enemy_locations != 0xFFFFFFFF) { + throw runtime_error("multiple random enemy locations sections for same floor"); + } + sections.random_enemy_locations = header_offset; + break; + case SectionHeader::Type::RANDOM_ENEMY_DEFINITIONS: + if (sections.random_enemy_definitions != 0xFFFFFFFF) { + throw runtime_error("multiple random enemy definitions sections for same floor"); + } + sections.random_enemy_definitions = header_offset; + break; + default: + throw runtime_error("invalid section type"); + } + r.skip(header.data_size); + } + + for (size_t floor = 0; floor < floor_sections.size(); floor++) { + const auto& sections = floor_sections[floor]; + + if (sections.objects != 0xFFFFFFFF) { + const auto& header = r.pget(sections.objects); if (header.data_size % sizeof(ObjectEntry)) { throw runtime_error("quest layout object section size is not a multiple of object entry size"); } - this->add_objects_from_map_data(r.getv(header.data_size), header.data_size); + static_game_data_log.info("(Floor %02zX) Adding objects", floor); + this->add_objects_from_map_data(r.pgetv(sections.objects + sizeof(header), header.data_size), header.data_size); + } - } else if (header.type == 2) { + if (sections.enemies != 0xFFFFFFFF) { + const auto& header = r.pget(sections.enemies); if (header.data_size % sizeof(EnemyEntry)) { throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size"); } - this->add_enemies_from_map_data(episode, difficulty, event, r.getv(header.data_size), header.data_size, rare_rates); - static_game_data_log.info("There are now %zu enemies", this->enemies.size()); + static_game_data_log.info("(Floor %02zX) Adding enemies", floor); + this->add_enemies_from_map_data( + episode, + difficulty, + event, + r.pgetv(sections.enemies + sizeof(header), header.data_size), + header.data_size, + rare_rates); - } else { - r.skip(header.section_size - sizeof(header)); + } else if ((sections.wave_events != 0xFFFFFFFF) && + (sections.random_enemy_locations != 0xFFFFFFFF) && + (sections.random_enemy_definitions != 0xFFFFFFFF)) { + static_game_data_log.info("(Floor %02zX) Adding random enemies", floor); + const auto& wave_events_header = r.pget(sections.wave_events); + const auto& random_enemy_locations_header = r.pget(sections.random_enemy_locations); + const auto& random_enemy_definitions_header = r.pget(sections.random_enemy_definitions); + this->add_random_enemies_from_map_data( + episode, + difficulty, + event, + r.sub(sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size), + r.sub(sections.random_enemy_locations + sizeof(SectionHeader), random_enemy_locations_header.data_size), + r.sub(sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size), + rare_seed, + rare_rates); } } } @@ -567,9 +809,9 @@ void SetDataTable::load_table_t(shared_ptr data) { auto var2_r = r.sub(var2_table_offset, var2_table_count * 0x0C); while (!var2_r.eof()) { auto& entry = var2_v.emplace_back(); - entry.name1 = r.pget_cstr(var2_r.get()); + entry.object_list_basename = r.pget_cstr(var2_r.get()); entry.enemy_list_basename = r.pget_cstr(var2_r.get()); - entry.name3 = r.pget_cstr(var2_r.get()); + entry.event_list_basename = r.pget_cstr(var2_r.get()); } } } @@ -582,7 +824,7 @@ void SetDataTable::print(FILE* stream) const { const auto& v2_v = v1_v[v1]; for (size_t v2 = 0; v2 < v2_v.size(); v2++) { const auto& e = v2_v[v2]; - fprintf(stream, "[%02zX/%02zX/%02zX] %s %s %s\n", a, v1, v2, e.name1.c_str(), e.enemy_list_basename.c_str(), e.name3.c_str()); + fprintf(stream, "[%02zX/%02zX/%02zX] %s %s %s\n", a, v1, v2, e.object_list_basename.c_str(), e.enemy_list_basename.c_str(), e.event_list_basename.c_str()); } } } diff --git a/src/Map.hh b/src/Map.hh index 71ee2c8b..10fa6f85 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -14,6 +14,170 @@ #include "Text.hh" struct Map { + struct SectionHeader { + enum class Type { + END = 0, + OBJECTS = 1, + ENEMIES = 2, + WAVE_EVENTS = 3, + RANDOM_ENEMY_LOCATIONS = 4, + RANDOM_ENEMY_DEFINITIONS = 5, + }; + le_uint32_t le_type; + le_uint32_t section_size; // Includes this header + le_uint32_t floor; + le_uint32_t data_size; + + inline Type type() const { + return static_cast(this->le_type.load()); + } + } __attribute__((packed)); + + struct ObjectEntry { // Section type 1 (OBJECTS) + /* 00 */ le_uint16_t base_type; + /* 02 */ le_uint16_t flags; + /* 04 */ le_uint16_t index; + /* 06 */ le_uint16_t unknown_a2; + /* 08 */ le_uint16_t entity_id; // == index + 0x4000 + /* 0A */ le_uint16_t group; + /* 0C */ le_uint16_t section; + /* 0E */ le_uint16_t unknown_a3; + /* 10 */ le_float x; + /* 14 */ le_float y; + /* 18 */ le_float z; + /* 1C */ le_uint32_t x_angle; + /* 20 */ le_uint32_t y_angle; + /* 24 */ le_uint32_t z_angle; + /* 28 */ le_float param1; + /* 2C */ le_float param2; + /* 30 */ le_float param3; + /* 34 */ le_uint32_t param4; + /* 38 */ le_uint32_t param5; + /* 3C */ le_uint32_t param6; + /* 40 */ le_uint32_t unused; // Reserved for pointer in client's memory; unused by server + /* 44 */ + } __attribute__((packed)); + + struct EnemyEntry { // Section type 2 (ENEMIES) + /* 00 */ le_uint16_t base_type; + /* 02 */ le_uint16_t flags; + /* 04 */ le_uint16_t index; + /* 06 */ le_uint16_t num_children; + /* 08 */ le_uint16_t floor; + /* 0A */ le_uint16_t entity_id; // == index + 0x1000 + /* 0C */ le_uint16_t section; + /* 0E */ le_uint16_t wave_number; + /* 10 */ le_uint16_t wave_number2; + /* 12 */ le_uint16_t unknown_a1; + /* 14 */ le_float x; + /* 18 */ le_float y; + /* 1C */ le_float z; + /* 20 */ le_uint32_t x_angle; + /* 24 */ le_uint32_t y_angle; + /* 28 */ le_uint32_t z_angle; + /* 2C */ le_float fparam1; + /* 30 */ le_float fparam2; + /* 34 */ le_float fparam3; + /* 38 */ le_float fparam4; + /* 3C */ le_float fparam5; + /* 40 */ le_uint16_t uparam1; + /* 42 */ le_uint16_t uparam2; + /* 44 */ le_uint32_t unused; // Reserved for pointer in client's memory; unused by server + /* 48 */ + } __attribute__((packed)); + + struct EventsSectionHeader { // Section type 3 (WAVE_EVENTS) + /* 00 */ le_uint32_t footer_offset; + /* 04 */ le_uint32_t entries_offset; + /* 08 */ le_uint32_t entry_count; + /* 0C */ be_uint32_t format; // 0 or 'evt2' + /* 10 */ + } __attribute__((packed)); + + struct Event1Entry { // Section type 3 (WAVE_EVENTS) if format == 0 + /* 00 */ le_uint32_t event_id; + /* 04 */ le_uint16_t flags; + /* 06 */ le_uint16_t unknown_a2; + /* 08 */ le_uint16_t section; + /* 0A */ le_uint16_t wave_number; + /* 0C */ le_uint32_t delay; + /* 10 */ le_uint32_t clear_events_index; + /* 14 */ + } __attribute__((packed)); + + struct Event2Entry { // Section type 3 (WAVE_EVENTS) if format == 'evt2' + /* 00 */ le_uint32_t event_id; + /* 04 */ le_uint16_t flags; + /* 06 */ le_uint16_t unknown_a2; + /* 08 */ le_uint16_t section; + /* 0A */ le_uint16_t wave_number; + /* 0C */ le_uint16_t min_delay; + /* 0E */ le_uint16_t max_delay; + /* 10 */ uint8_t min_enemies; + /* 11 */ uint8_t max_enemies; + /* 12 */ le_uint16_t max_waves; + /* 14 */ le_uint32_t clear_events_index; + /* 18 */ + } __attribute__((packed)); + + struct RandomEnemyLocationsHeader { // Section type 4 (RANDOM_ENEMY_LOCATIONS) + /* 00 */ le_uint32_t section_table_offset; // Offset to RandomEnemyLocationSegment structs, from start of this struct + /* 04 */ le_uint32_t entries_offset; // Offset to RandomEnemyLocationEntry structs, from start of this struct + /* 08 */ le_uint32_t num_sections; + /* 0C */ + } __attribute__((packed)); + + struct RandomEnemyLocationSection { // Section type 4 (RANDOM_ENEMY_LOCATIONS) + /* 00 */ le_uint16_t section; + /* 02 */ le_uint16_t count; + /* 04 */ le_uint32_t offset; + /* 08 */ + } __attribute__((packed)); + + struct RandomEnemyLocationEntry { // Section type 4 (RANDOM_ENEMY_LOCATIONS) + /* 00 */ le_float x; + /* 04 */ le_float y; + /* 08 */ le_float z; + /* 0C */ le_uint32_t x_angle; + /* 10 */ le_uint32_t y_angle; + /* 14 */ le_uint32_t z_angle; + /* 18 */ uint16_t unknown_a9; + /* 1A */ uint16_t unknown_a10; + /* 1C */ + } __attribute__((packed)); + + struct RandomEnemyDefinitionsHeader { // Section type 5 (RANDOM_ENEMY_DEFINITIONS) + /* 00 */ le_uint32_t entries_offset; // Offset to RandomEnemyDefinition structs, from start of this struct + /* 04 */ le_uint32_t weight_entries_offset; // Offset to RandomEnemyDefinitionWeights structs, from start of this struct + /* 08 */ le_uint32_t entry_count; + /* 0C */ le_uint32_t weight_entry_count; + /* 10 */ + } __attribute__((packed)); + + struct RandomEnemyDefinition { // Section type 5 (RANDOM_ENEMY_DEFINITIONS) + // All fields through entry_num map to the corresponding fields in + // EnemyEntry. Note that the order of the uparam fields is switched! + /* 00 */ le_float fparam1; + /* 04 */ le_float fparam2; + /* 08 */ le_float fparam3; + /* 0C */ le_float fparam4; + /* 10 */ le_float fparam5; + /* 14 */ le_uint16_t uparam2; + /* 16 */ le_uint16_t uparam1; + /* 18 */ le_uint32_t entry_num; + /* 1C */ le_uint16_t min_children; + /* 1E */ le_uint16_t max_children; + /* 20 */ + } __attribute__((packed)); + + struct RandomEnemyWeight { // Section type 5 (RANDOM_ENEMY_DEFINITIONS) + /* 00 */ uint8_t base_type_index; + /* 01 */ uint8_t definition_entry_num; + /* 02 */ uint8_t weight; + /* 03 */ uint8_t unknown_a4; + /* 04 */ + } __attribute__((packed)); + struct RareEnemyRates { uint32_t hildeblue; // HILDEBEAR -> HILDEBLUE uint32_t rappy; // RAG_RAPPY -> {AL_RAPPY or seasonal rappies}; SAND_RAPPY -> DEL_RAPPY @@ -51,6 +215,15 @@ struct Map { void clear(); void add_objects_from_map_data(const void* data, size_t size); + bool check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate); + void add_enemy(EnemyType type); + void add_enemy( + Episode episode, + uint8_t difficulty, + uint8_t event, + size_t index, + const EnemyEntry& e, + const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); void add_enemies_from_map_data( Episode episode, uint8_t difficulty, @@ -58,12 +231,22 @@ struct Map { const void* data, size_t size, const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); + void add_random_enemies_from_map_data( + Episode episode, + uint8_t difficulty, + uint8_t event, + StringReader wave_events_r, + StringReader random_enemy_locations_r, + StringReader random_enemy_definitions_r, + uint32_t rare_seed, + const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); void add_enemies_and_objects_from_quest_data( Episode episode, uint8_t difficulty, uint8_t event, const void* data, size_t size, + uint32_t rare_seed, const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); }; @@ -74,9 +257,9 @@ struct Map { class SetDataTable { public: struct SetEntry { - std::string name1; + std::string object_list_basename; std::string enemy_list_basename; - std::string name3; + std::string event_list_basename; }; SetDataTable(std::shared_ptr data, bool big_endian); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 2e2e3169..88096ec4 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2620,8 +2620,9 @@ static void on_AC_V3_BB(shared_ptr c, uint16_t, uint32_t, string& data) l->event, dat_contents.data(), dat_contents.size(), + l->random_seed, (l->mode == GameMode::CHALLENGE) ? Map::NO_RARE_ENEMIES : Map::DEFAULT_RARE_ENEMIES); - c->log.info("Replaced enemies list with quest layout (%zu entries)", + l->log.info("Replaced enemies list with quest layout (%zu entries)", l->map->enemies.size()); for (size_t z = 0; z < l->map->enemies.size(); z++) { string e_str = l->map->enemies[z].str(); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index b6b25619..cfbf7c32 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1621,11 +1621,11 @@ static void on_enemy_hit(shared_ptr c, uint8_t command, uint8_t, const v if (!l->map) { throw runtime_error("game does not have a map loaded"); } - if (cmd.enemy_id >= l->map->enemies.size()) { + if (cmd.enemy_index >= l->map->enemies.size()) { return; } - auto& enemy = l->map->enemies[cmd.enemy_id]; + auto& enemy = l->map->enemies[cmd.enemy_index]; if (enemy.flags & Map::Enemy::Flag::DEFEATED) { return; } @@ -1633,7 +1633,7 @@ static void on_enemy_hit(shared_ptr c, uint8_t command, uint8_t, const v enemy.last_hit_by_client_id = c->lobby_client_id; } - G_EnemyHitByPlayer_GC_6x0A sw_cmd = {{{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id}, cmd.enemy_id, cmd.remaining_hp, cmd.flags.load()}}; + G_EnemyHitByPlayer_GC_6x0A sw_cmd = {{{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id}, cmd.enemy_index, cmd.remaining_hp, cmd.flags.load()}}; bool sender_is_gc = (c->version() == GameVersion::GC); for (auto lc : l->clients) { if (lc && (lc != c)) { @@ -1721,7 +1721,7 @@ static void on_steal_exp_bb(shared_ptr c, uint8_t, uint8_t, const void* const auto& cmd = check_size_t(data, size); auto p = c->game_data.player(); - const auto& enemy = l->map->enemies.at(cmd.enemy_id); + const auto& enemy = l->map->enemies.at(cmd.enemy_index); const auto& inventory = p->inventory; const auto& weapon = inventory.items[inventory.find_equipped_weapon()]; @@ -1751,7 +1751,7 @@ static void on_steal_exp_bb(shared_ptr c, uint8_t, uint8_t, const void* uint32_t stolen_exp = min((enemy_exp * percent) / 100, (l->difficulty + 1) * 20); if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { send_text_message_printf(c, "$C5+%" PRIu32 " E-%hX %s", - stolen_exp, cmd.enemy_id.load(), name_for_enum(enemy.type)); + stolen_exp, cmd.enemy_index.load(), name_for_enum(enemy.type)); } add_player_exp(c, stolen_exp); } @@ -1774,17 +1774,17 @@ static void on_enemy_killed_bb(shared_ptr c, uint8_t command, uint8_t fl if (!l->map) { throw runtime_error("game does not have a map loaded"); } - if (cmd.enemy_id >= l->map->enemies.size()) { + if (cmd.enemy_index >= l->map->enemies.size()) { send_text_message(c, "$C6Missing enemy killed"); return; } - auto& e = l->map->enemies[cmd.enemy_id]; + auto& e = l->map->enemies[cmd.enemy_index]; string e_str = e.str(); - c->log.info("Enemy killed: E-%hX => %s", cmd.enemy_id.load(), e_str.c_str()); + c->log.info("Enemy killed: E-%hX => %s", cmd.enemy_index.load(), e_str.c_str()); if (e.flags & Map::Enemy::Flag::DEFEATED) { if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5E-%hX __DEFEATED__", cmd.enemy_id.load()); + send_text_message_printf(c, "$C5E-%hX __DEFEATED__", cmd.enemy_index.load()); } return; } @@ -1796,7 +1796,7 @@ static void on_enemy_killed_bb(shared_ptr c, uint8_t command, uint8_t fl experience = bp_table.stats[l->difficulty][bp_index].experience * l->exp_multiplier; } catch (const exception& e) { if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5E-%hX __MISSING__\n%s", cmd.enemy_id.load(), e.what()); + send_text_message_printf(c, "$C5E-%hX __MISSING__\n%s", cmd.enemy_index.load(), e.what()); } else { send_text_message_printf(c, "$C4Unknown enemy type killed:\n%s", e.what()); } @@ -1812,19 +1812,18 @@ static void on_enemy_killed_bb(shared_ptr c, uint8_t command, uint8_t fl if (!other_c) { continue; // No player } - if (other_c->game_data.player()->disp.stats.level >= 199) { - continue; // Player is level 200 or higher - } if (experience != 0xFFFFFFFF) { // Killer gets full experience, others get 77% bool is_killer = (e.last_hit_by_client_id == other_c->lobby_client_id); uint32_t player_exp = is_killer ? experience : ((experience * 77) / 100); - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + if (other_c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { send_text_message_printf(c, "$C5+%" PRIu32 " E-%hX %s", - player_exp, cmd.enemy_id.load(), name_for_enum(e.type)); + player_exp, cmd.enemy_index.load(), name_for_enum(e.type)); + } + if (other_c->game_data.player()->disp.stats.level < 199) { + add_player_exp(other_c, player_exp); } - add_player_exp(c, player_exp); } } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 85238cce..14c7c43f 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2327,13 +2327,13 @@ void send_set_exp_multiplier(std::shared_ptr l) { void send_rare_enemy_index_list(shared_ptr c, const vector& indexes) { S_RareMonsterList_BB_DE cmd; - if (indexes.size() > cmd.enemy_ids.size()) { + if (indexes.size() > cmd.enemy_indexes.size()) { throw runtime_error("too many rare enemies"); } for (size_t z = 0; z < indexes.size(); z++) { - cmd.enemy_ids[z] = indexes[z]; + cmd.enemy_indexes[z] = indexes[z]; } - cmd.enemy_ids.clear_after(indexes.size(), 0xFFFF); + cmd.enemy_indexes.clear_after(indexes.size(), 0xFFFF); send_command_t(c, 0xDE, 0x00, cmd); }