From bcaa2a493ea407cf9df110240acc6608ca00f055 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 18 Jun 2023 22:55:22 -0700 Subject: [PATCH] update map loader --- src/Lobby.hh | 2 +- src/Map.cc | 589 ++++++++++++++++++-------------------- src/Map.hh | 94 ++---- src/ReceiveCommands.cc | 23 +- src/ReceiveSubcommands.cc | 143 +++++---- 5 files changed, 394 insertions(+), 457 deletions(-) diff --git a/src/Lobby.hh b/src/Lobby.hh index c8351903..911533c1 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -57,7 +57,7 @@ struct Lobby : public std::enable_shared_from_this { float z; uint8_t area; }; - std::vector enemies; + std::shared_ptr map; std::array next_item_id; uint32_t next_game_item_id; std::unordered_map item_id_to_floor_item; diff --git a/src/Map.cc b/src/Map.cc index 0b76d742..70c79a30 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -9,150 +9,93 @@ using namespace std; -string BattleParamsIndex::Entry::str() const { - string a1str = format_data_string(this->unknown_a1.data(), this->unknown_a1.bytes()); - return string_printf( - "BattleParamsEntry[ATP=%hu PSV=%hu EVP=%hu HP=%hu DFP=%hu ATA=%hu LCK=%hu ESP=%hu a1=%s EXP=%" PRIu32 " diff=%" PRIu32 "]", - this->atp.load(), - this->psv.load(), - this->evp.load(), - this->hp.load(), - this->dfp.load(), - this->ata.load(), - this->lck.load(), - this->esp.load(), - a1str.c_str(), - this->experience.load(), - this->difficulty.load()); -} +uint64_t Map::Enemy::next_enemy_id = 1; -void BattleParamsIndex::Table::print(FILE* stream) const { - auto print_entry = +[](FILE* stream, const Entry& e) { - string a1str = format_data_string(e.unknown_a1.data(), e.unknown_a1.bytes()); - fprintf(stream, - "%5hu %5hu %5hu %5hu %5hu %5hu %5hu %5hu %s %5" PRIu32 " %5" PRIu32, - e.atp.load(), - e.psv.load(), - e.evp.load(), - e.hp.load(), - e.dfp.load(), - e.ata.load(), - e.lck.load(), - e.esp.load(), - a1str.c_str(), - e.experience.load(), - e.difficulty.load()); - }; +Map::Enemy::Enemy(EnemyType type) + : id(Map::Enemy::next_enemy_id++), + type(type), + flags(0), + last_hit_by_client_id(0) {} - for (size_t diff = 0; diff < 4; diff++) { - fprintf(stream, "%c ZZ ATP PSV EVP HP DFP ATA LCK ESP A1 EXP DIFF\n", - abbreviation_for_difficulty(diff)); - for (size_t z = 0; z < 0x60; z++) { - fprintf(stream, " %02zX ", z); - print_entry(stream, this->difficulty[diff][z]); - fputc('\n', stream); - } - } -} - -BattleParamsIndex::BattleParamsIndex( - shared_ptr data_on_ep1, - shared_ptr data_on_ep2, - shared_ptr data_on_ep4, - shared_ptr data_off_ep1, - shared_ptr data_off_ep2, - shared_ptr data_off_ep4) { - this->files[0][0].data = data_on_ep1; - this->files[0][1].data = data_on_ep2; - this->files[0][2].data = data_on_ep4; - this->files[1][0].data = data_off_ep1; - this->files[1][1].data = data_off_ep2; - this->files[1][2].data = data_off_ep4; - - for (uint8_t is_solo = 0; is_solo < 2; is_solo++) { - for (uint8_t episode = 0; episode < 3; episode++) { - auto& file = this->files[is_solo][episode]; - if (file.data->size() < sizeof(Table)) { - throw runtime_error(string_printf( - "battle params table size is incorrect (expected %zX bytes, have %zX bytes; is_solo=%hhu, episode=%hhu)", - sizeof(Table), file.data->size(), is_solo, episode)); - } - file.table = reinterpret_cast(file.data->data()); - } - } -} - -const BattleParamsIndex::Entry& BattleParamsIndex::get( - bool solo, Episode episode, uint8_t difficulty, uint8_t monster_type) const { - if (difficulty > 4) { - throw invalid_argument("incorrect difficulty"); - } - if (monster_type > 0x60) { - throw invalid_argument("incorrect monster type"); - } - - uint8_t ep_index; - switch (episode) { - case Episode::EP1: - ep_index = 0; - break; - case Episode::EP2: - ep_index = 1; - break; - case Episode::EP4: - ep_index = 2; - break; - default: - throw invalid_argument("invalid episode"); - } - - return this->files[!!solo][ep_index].table->difficulty[difficulty][monster_type]; -} - -PSOEnemy::PSOEnemy(uint64_t id) : PSOEnemy(id, 0, 0, 0, 0, "__missing__") {} - -PSOEnemy::PSOEnemy( - uint64_t id, - uint16_t source_type, - uint32_t experience, - uint32_t rt_index, - size_t num_clones, - const char* type_name) - : id(id), - source_type(source_type), - hit_flags(0), - last_hit(0), - experience(experience), - rt_index(rt_index), - num_clones(num_clones), - type_name(type_name) {} - -string PSOEnemy::str() const { - return string_printf("[Enemy E-%" PRIX64 " \"%s\" source_type=%hX hit=%02hhX/%hu exp=%" PRIu32 " rt_index=%" PRIX32 " clones=%zu]", - this->id, this->type_name, this->source_type, this->hit_flags, this->last_hit, this->experience, this->rt_index, this->num_clones); +string Map::Enemy::str() const { + return string_printf("[Map::Enemy E-%" PRIX64 " type=%s flags=%02hhX last_hit_by_client_id=%hu]", + this->id, name_for_enum(this->type), this->flags, this->last_hit_by_client_id); } struct EnemyEntry { - uint32_t base; - uint16_t reserved0; - uint16_t num_clones; - uint32_t reserved[11]; - float reserved12; - uint32_t reserved13; - uint32_t reserved14; - uint32_t skin; - uint32_t reserved15; + /* 00 */ le_uint32_t base; + /* 04 */ le_uint16_t unknown_a1; + /* 06 */ le_uint16_t num_clones; + /* 08 */ le_uint16_t area; + /* 0A */ le_uint16_t unknown_a2; + /* 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=%" PRIX32 ", a1=%hX, num_clones=%hX, area=%hX, a2=%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.load(), this->unknown_a1.load(), this->num_clones.load(), this->area.load(), + this->unknown_a2.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)); -static uint64_t next_enemy_id = 1; +struct ObjectEntry { + /* 00 */ le_uint16_t 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_uint32_t unknown_a4; + /* 2C */ le_uint32_t unknown_a5; + /* 30 */ le_uint32_t 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 */ -vector parse_map( - shared_ptr battle_params, - bool is_solo, - Episode episode, - uint8_t difficulty, - shared_ptr data, - bool alt_enemies) { + string str() const { + return string_printf("ObjectEntry(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 ", a3=%" PRIX32 ", a4=%" PRIX32 ", a5=%" PRIX32 ", a6=%" PRIX32 ", a7=%" PRIX32 ", a8=%" PRIX32 ", a9=%" PRIX32 ")", + this->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_a3.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()); + } +} __attribute__((packed)); + +void Map::clear() { + this->enemies.clear(); +} + +void Map::add_enemies_from_map_data( + Episode episode, uint8_t difficulty, uint8_t event, shared_ptr data) { const auto* map = reinterpret_cast(data->data()); size_t entry_count = data->size() / sizeof(EnemyEntry); @@ -160,290 +103,302 @@ vector parse_map( throw runtime_error("data size is not a multiple of entry size"); } - vector enemies; auto create_clones = [&](size_t count) { for (; count > 0; count--) { - enemies.emplace_back(next_enemy_id++); + this->enemies.emplace_back(EnemyType::NONE); } }; - auto create_enemy = [&](const EnemyEntry& e, ssize_t bp_index, uint32_t rt_index, const char* type_name) { - const BattleParamsIndex::Entry& bp_entry = battle_params->get( - is_solo, episode, difficulty, bp_index); - enemies.emplace_back( - next_enemy_id++, - e.base, - bp_entry.experience, - rt_index, - e.num_clones, - type_name); - }; - for (size_t y = 0; y < entry_count; y++) { const auto& e = map[y]; switch (e.base) { - case 0x40: // Hildebear and Hildetorr - create_enemy(e, 0x49 + (e.skin & 0x01), 0x01 + (e.skin & 0x01), "Hilde(bear|torr)"); + case 0x40: + enemies.emplace_back((e.skin & 0x01) ? EnemyType::HILDEBLUE : EnemyType::HILDEBEAR); break; - case 0x41: // Rappies - if (episode == Episode::EP4) { // Del Rappy and Sand Rappy - if (alt_enemies) { - create_enemy(e, 0x17 + (e.skin & 0x01), 17 + (e.skin & 0x01), "(Del|Sand) Rappy"); - } else { - create_enemy(e, 0x05 + (e.skin & 0x01), 17 + (e.skin & 0x01), "(Del|Sand) Rappy"); - } - } else { // Rag Rappy and Al Rappy (Love for Episode II) - if (e.skin & 0x01) { - // TODO: Don't know (yet) which rare Rappy it is - create_enemy(e, 0x18 + (e.skin & 0x01), 0xFF, "Rare Rappy"); - } else { - create_enemy(e, 0x18 + (e.skin & 0x01), 5, "Rag Rappy"); - } + case 0x41: { + bool is_rare = (e.skin & 0x01); + switch (episode) { + case Episode::EP1: + enemies.emplace_back(is_rare ? EnemyType::AL_RAPPY : EnemyType::RAG_RAPPY); + break; + case Episode::EP2: + switch (event) { + case 0x01: + enemies.emplace_back(EnemyType::SAINT_RAPPY); + break; + case 0x04: + enemies.emplace_back(EnemyType::EGG_RAPPY); + break; + case 0x05: + enemies.emplace_back(EnemyType::HALLO_RAPPY); + break; + default: + enemies.emplace_back(EnemyType::LOVE_RAPPY); + } + break; + case Episode::EP4: + if (e.area > 0x05) { + enemies.emplace_back(is_rare ? EnemyType::DEL_RAPPY_ALT : EnemyType::SAND_RAPPY_ALT); + } else { + enemies.emplace_back(is_rare ? EnemyType::DEL_RAPPY : EnemyType::SAND_RAPPY); + } + break; + default: + throw logic_error("invalid episode"); } break; - case 0x42: // Monest + 30 Mothmants - create_enemy(e, 0x01, 4, "Monest"); - for (size_t x = 0; x < 30; x++) { - create_enemy(e, 0x00, 3, "Mothmant"); + } + case 0x42: + enemies.emplace_back(EnemyType::MONEST); + for (size_t x = 0; x < e.num_clones; x++) { + enemies.emplace_back((x < 30) ? EnemyType::MOTHMANT : EnemyType::UNKNOWN); } break; - case 0x43: // Savage Wolf and Barbarous Wolf - create_enemy(e, 0x02 + ((e.reserved[10] & 0x800000) ? 1 : 0), - 7 + ((e.reserved[10] & 0x800000) ? 1 : 0), "(Savage|Barbarous) Wolf"); + case 0x43: { + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::BARBAROUS_WOLF : EnemyType::SAVAGE_WOLF); break; - case 0x44: // Booma family - create_enemy(e, 0x4B + (e.skin % 3), 9 + (e.skin % 3), "(|Go|Gigo)Booma"); + } + case 0x44: + static const EnemyType types[3] = {EnemyType::BOOMA, EnemyType::GOBOOMA, EnemyType::GIGOBOOMA}; + enemies.emplace_back(types[e.skin % 3]); break; - case 0x60: // Grass Assassin - create_enemy(e, 0x4E, 12, "Grass Assassin"); + case 0x60: + enemies.emplace_back(EnemyType::GRASS_ASSASSIN); break; - case 0x61: // Del Lily, Poison Lily, Nar Lily - if ((episode == Episode::EP2) && (alt_enemies)) { - create_enemy(e, 0x25, 83, "Del Lily"); + case 0x61: + if ((episode == Episode::EP2) && (e.area > 0x0F)) { + enemies.emplace_back(EnemyType::DEL_LILY); } else { - create_enemy(e, 0x04 + ((e.reserved[10] & 0x800000) ? 1 : 0), - 13 + ((e.reserved[10] & 0x800000) ? 1 : 0), "(Poison|Nar) Lily"); + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::NAR_LILY : EnemyType::POISON_LILY); } break; - case 0x62: // Nano Dragon - create_enemy(e, 0x1A, 15, "Nano Dragon"); + case 0x62: + enemies.emplace_back(EnemyType::NANO_DRAGON); break; - case 0x63: // Shark family - create_enemy(e, 0x4F + (e.skin % 3), 16 + (e.skin % 3), "(Evil|Pal|Guil) Shark"); + case 0x63: { + static const EnemyType types[3] = {EnemyType::EVIL_SHARK, EnemyType::PAL_SHARK, EnemyType::GUIL_SHARK}; + enemies.emplace_back(types[e.skin % 3]); break; - case 0x64: // Slime + 4 clones - create_enemy(e, 0x2F + ((e.reserved[10] & 0x800000) ? 0 : 1), - 19 + ((e.reserved[10] & 0x800000) ? 1 : 0), "Pof?uilly Slime"); - for (size_t x = 0; x < 4; x++) { - create_enemy(e, 0x30, 19, "Pof?uilly Slime clone"); + } + case 0x64: { + bool is_common = ((e.unknown_a4 & 0x800000) ? true : false); + for (size_t x = 0; x < 5; x++) { // Main slime + 4 clones + enemies.emplace_back(is_common ? EnemyType::POFUILLY_SLIME : EnemyType::POUILLY_SLIME); } break; - case 0x65: // Pan Arms, Migium, Hidoom - for (size_t x = 0; x < 3; x++) { - create_enemy(e, 0x31 + x, 21 + x, "(Pan Arms|Hidoom|Migium)"); - } + } + case 0x65: + enemies.emplace_back(EnemyType::PAN_ARMS); + enemies.emplace_back(EnemyType::HIDOOM); + enemies.emplace_back(EnemyType::MIGIUM); break; - case 0x80: // Dubchic and Gillchic - if (e.skin & 0x01) { - create_enemy(e, 0x1B + (e.skin & 0x01), 50, "(Dub|Gill)chic"); - } else { - create_enemy(e, 0x1B + (e.skin & 0x01), 24, "(Dub|Gill)chic"); - } + case 0x80: + enemies.emplace_back((e.skin & 0x01) ? EnemyType::GILLCHIC : EnemyType::DUBCHIC); break; - case 0x81: // Garanz - create_enemy(e, 0x1D, 25, "Garanz"); + case 0x81: + enemies.emplace_back(EnemyType::GARANZ); break; - case 0x82: // Sinow Beat and Gold - if (e.reserved[10] & 0x800000) { - create_enemy(e, 0x13, 26 + ((e.reserved[10] & 0x800000) ? 1 : 0), "Sinow (Beat|Gold)"); - } else { - create_enemy(e, 0x06, 26 + ((e.reserved[10] & 0x800000) ? 1 : 0), "Sinow (Beat|Gold)"); - } + case 0x82: + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::SINOW_GOLD : EnemyType::SINOW_BEAT); if (e.num_clones == 0) { create_clones(4); } break; - case 0x83: // Canadine - create_enemy(e, 0x07, 28, "Canadine"); + case 0x83: + enemies.emplace_back(EnemyType::CANADINE); break; - case 0x84: // Canadine Group - create_enemy(e, 0x09, 29, "Canune"); + case 0x84: + enemies.emplace_back(EnemyType::CANANE); for (size_t x = 0; x < 8; x++) { - create_enemy(e, 0x08, 28, "Canadine"); + enemies.emplace_back(EnemyType::CANADINE); } break; - case 0x85: // Dubwitch - enemies.emplace_back(next_enemy_id++, e.base, 0xFFFFFFFF, 0, 0, "__dubwitch__"); + case 0x85: + enemies.emplace_back(EnemyType::DUBWITCH); break; - case 0xA0: // Delsaber - create_enemy(e, 0x52, 30, "Delsaber"); + case 0xA0: + enemies.emplace_back(EnemyType::DELSABER); break; - case 0xA1: // Chaos Sorcerer + 2 Bits - create_enemy(e, 0x0A, 31, "Chaos Sorcerer"); + case 0xA1: + enemies.emplace_back(EnemyType::CHAOS_SORCERER); create_clones(2); break; - case 0xA2: // Dark Gunner - create_enemy(e, 0x1E, 34, "Dark Gunner"); + case 0xA2: + enemies.emplace_back(EnemyType::DARK_GUNNER); break; - case 0xA4: // Chaos Bringer - create_enemy(e, 0x0D, 36, "Chaos Bringer"); + case 0xA3: + enemies.emplace_back(EnemyType::DEATH_GUNNER); break; - case 0xA5: // Dark Belra - create_enemy(e, 0x0E, 37, "Dark Belra"); + case 0xA4: + enemies.emplace_back(EnemyType::CHAOS_BRINGER); break; - case 0xA6: // Dimenian family - create_enemy(e, 0x53 + (e.skin % 3), 41 + (e.skin % 3), "(|La|So) Dimenian"); + case 0xA5: + enemies.emplace_back(EnemyType::DARK_BELRA); break; - case 0xA7: // Bulclaw + 4 claws - create_enemy(e, 0x1F, 40, "Bulclaw"); + case 0xA6: { + static const EnemyType types[3] = {EnemyType::DIMENIAN, EnemyType::LA_DIMENIAN, EnemyType::SO_DIMENIAN}; + enemies.emplace_back(types[e.skin % 3]); + break; + } + case 0xA7: + enemies.emplace_back(EnemyType::BULCLAW); for (size_t x = 0; x < 4; x++) { - create_enemy(e, 0x20, 38, "Claw"); + enemies.emplace_back(EnemyType::CLAW); } break; - case 0xA8: // Claw - create_enemy(e, 0x20, 38, "Claw"); + case 0xA8: + enemies.emplace_back(EnemyType::CLAW); break; - case 0xC0: // Dragon or Gal Gryphon + case 0xC0: if (episode == Episode::EP1) { - create_enemy(e, 0x12, 44, "Dragon"); + enemies.emplace_back(EnemyType::DRAGON); } else if (episode == Episode::EP2) { - create_enemy(e, 0x1E, 77, "Gal Gryphon"); + enemies.emplace_back(EnemyType::GAL_GRYPHON); + } else { + throw runtime_error("DRAGON-type enemy placed outside of Episodes 1 or 2"); } break; - case 0xC1: // De Rol Le - create_enemy(e, 0x0F, 45, "De Rol Le"); + case 0xC1: + enemies.emplace_back(EnemyType::DE_ROL_LE); break; - case 0xC2: // Vol Opt form 1 - enemies.emplace_back(next_enemy_id++, e.base, 0xFFFFFFFF, 0, 0, "__vol_opt_1__"); + case 0xC2: + enemies.emplace_back(EnemyType::VOL_OPT_1); break; - case 0xC5: // Vol Opt form 2 - create_enemy(e, 0x25, 46, "Vol Opt"); + case 0xC5: + enemies.emplace_back(EnemyType::VOL_OPT_2); break; - case 0xC8: // Dark Falz + 510 Darvants + case 0xC8: if (difficulty) { - create_enemy(e, 0x38, 47, "Dark Falz 3"); // Final form + enemies.emplace_back(EnemyType::DARK_FALZ_3); } else { - create_enemy(e, 0x37, 47, "Dark Falz 2"); // Second form + enemies.emplace_back(EnemyType::DARK_FALZ_2); } for (size_t x = 0; x < 510; x++) { - create_enemy(e, 0x35, 0, "Darvant"); + enemies.emplace_back(difficulty == 3 ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT); } break; - case 0xCA: // Olga Flow - create_enemy(e, 0x2C, 78, "Olga Flow"); + case 0xCA: + enemies.emplace_back(EnemyType::OLGA_FLOW_2); create_clones(0x200); break; - case 0xCB: // Barba Ray - create_enemy(e, 0x0F, 73, "Barba Ray"); + case 0xCB: + enemies.emplace_back(EnemyType::BARBA_RAY); create_clones(0x2F); break; - case 0xCC: // Gol Dragon - create_enemy(e, 0x12, 76, "Gol Dragon"); + case 0xCC: + enemies.emplace_back(EnemyType::GOL_DRAGON); create_clones(5); break; - case 0xD4: // Sinows Berill & Spigell - create_enemy(e, (e.reserved[10] & 0x800000) ? 0x13 : 0x06, - 62 + ((e.reserved[10] & 0x800000) ? 1 : 0), "Sinow (Berrill|Spigell)"); + case 0xD4: + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL); create_clones(4); break; - case 0xD5: // Merillia & Meriltas - create_enemy(e, 0x4B + (e.skin & 0x01), 52 + (e.skin & 0x01), "Meril(lia|tas)"); + case 0xD5: + enemies.emplace_back((e.skin & 0x01) ? EnemyType::MERILTAS : EnemyType::MERILLIA); break; - case 0xD6: // Mericus, Merikle, & Mericarol - if (e.skin) { - create_enemy(e, 0x44 + (e.skin % 3), 56 + (e.skin % 3), "Meri(cus|kle|carol)"); + case 0xD6: + if (e.skin == 0) { + enemies.emplace_back(EnemyType::MERICAROL); } else { - create_enemy(e, 0x3A, 56 + (e.skin % 3), "Meri(cus|kle|carol)"); + enemies.emplace_back(((e.skin % 3) == 2) ? EnemyType::MERICUS : EnemyType::MERIKLE); } break; - case 0xD7: // Ul Gibbon and Zol Gibbon - create_enemy(e, 0x3B + (e.skin & 0x01), 59 + (e.skin & 0x01), "(Ul|Zol) Gibbon"); + case 0xD7: + enemies.emplace_back((e.skin & 0x01) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); break; - case 0xD8: // Gibbles - create_enemy(e, 0x3D, 61, "Gibbles"); + case 0xD8: + enemies.emplace_back(EnemyType::GIBBLES); break; - case 0xD9: // Gee - create_enemy(e, 0x07, 54, "Gee"); + case 0xD9: + enemies.emplace_back(EnemyType::GEE); break; - case 0xDA: // Gi Gue - create_enemy(e, 0x1A, 55, "Gi Gue"); + case 0xDA: + enemies.emplace_back(EnemyType::GI_GUE); break; - case 0xDB: // Deldepth - create_enemy(e, 0x30, 71, "Deldepth"); + case 0xDB: + enemies.emplace_back(EnemyType::DELDEPTH); break; - case 0xDC: // Delbiter - create_enemy(e, 0x0D, 72, "Delbiter"); + case 0xDC: + enemies.emplace_back(EnemyType::DELBITER); break; - case 0xDD: // Dolmolm and Dolmdarl - create_enemy(e, 0x4F + (e.skin & 0x01), 64 + (e.skin & 0x01), "Dolm(olm|darl)"); + case 0xDD: + enemies.emplace_back((e.skin & 0x01) ? EnemyType::DOLMDARL : EnemyType::DOLMOLM); break; - case 0xDE: // Morfos - create_enemy(e, 0x40, 66, "Morfos"); + case 0xDE: + enemies.emplace_back(EnemyType::MORFOS); break; - case 0xDF: // Recobox & Recons - create_enemy(e, 0x41, 67, "Recobox"); + case 0xDF: + enemies.emplace_back(EnemyType::RECOBOX); for (size_t x = 0; x < e.num_clones; x++) { - create_enemy(e, 0x42, 68, "Recon"); + enemies.emplace_back(EnemyType::RECON); } break; - case 0xE0: // Epsilon, Sinow Zoa and Zele - if ((episode == Episode::EP2) && (alt_enemies)) { - create_enemy(e, 0x23, 84, "Epsilon"); + case 0xE0: + if ((episode == Episode::EP2) && (e.area > 0x0F)) { + enemies.emplace_back(EnemyType::EPSILON); create_clones(4); } else { - create_enemy(e, 0x43 + (e.skin & 0x01), 69 + (e.skin & 0x01), "Sinow Z(oa|ele)"); + enemies.emplace_back((e.skin & 0x01) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); } break; - case 0xE1: // Ill Gill - create_enemy(e, 0x26, 82, "Ill Gill"); + case 0xE1: + enemies.emplace_back(EnemyType::ILL_GILL); break; - case 0x0110: // Astark - create_enemy(e, 0x09, 1, "Astark"); + case 0x0110: + enemies.emplace_back(EnemyType::ASTARK); break; - case 0x0111: // Satellite Lizard and Yowie - create_enemy(e, 0x0D + ((e.reserved[10] & 0x800000) ? 1 : 0) + (alt_enemies ? 0x10 : 0), - 2 + ((e.reserved[10] & 0x800000) ? 0 : 1), "(Satellite Lizard|Yowie)"); - break; - case 0x0112: // Merissa A/AA - create_enemy(e, 0x19 + (e.skin & 0x01), 4 + (e.skin & 0x01), "Merissa AA?"); - break; - case 0x0113: // Girtablulu - create_enemy(e, 0x1F, 6, "Girtablulu"); - break; - case 0x0114: // Zu and Pazuzu - create_enemy(e, 0x0B + (e.skin & 0x01) + (alt_enemies ? 0x14 : 0x00), - 7 + (e.skin & 0x01), "(Pazu)?zu"); - break; - case 0x0115: // Boota family - if (e.skin & 2) { - create_enemy(e, 0x03, 9 + (e.skin % 3), "(|Ba|Ze) Boota"); + case 0x0111: + if (e.area > 0x05) { + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::YOWIE_ALT : EnemyType::SATELLITE_LIZARD_ALT); } else { - create_enemy(e, 0x00 + (e.skin % 3), 9 + (e.skin % 3), "(|Ba|Ze) Boota"); + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::YOWIE : EnemyType::SATELLITE_LIZARD); } break; - case 0x0116: // Dorphon and Eclair - create_enemy(e, 0x0F + (e.skin & 0x01), 12 + (e.skin & 0x01), "Dorphon( Eclair)?"); + case 0x0112: + enemies.emplace_back((e.skin & 0x01) ? EnemyType::MERISSA_AA : EnemyType::MERISSA_A); break; - case 0x0117: // Goran family - create_enemy(e, 0x11 + (e.skin % 3), (e.skin & 2) ? 15 : ((e.skin & 1) ? 16 : 14), "(Pyro )?Goran( Detonator)?"); + case 0x0113: + enemies.emplace_back(EnemyType::GIRTABLULU); break; + case 0x0114: + if (e.area > 0x05) { + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::PAZUZU_ALT : EnemyType::ZU_ALT); + } else { + enemies.emplace_back((e.unknown_a4 & 0x800000) ? EnemyType::PAZUZU : EnemyType::ZU); + } + break; + case 0x0115: + if (e.skin & 2) { + enemies.emplace_back(EnemyType::BA_BOOTA); + } else { + enemies.emplace_back((e.skin & 1) ? EnemyType::ZE_BOOTA : EnemyType::BOOTA); + } + break; + case 0x0116: + enemies.emplace_back((e.skin & 0x01) ? EnemyType::DORPHON_ECLAIR : EnemyType::DORPHON); + break; + case 0x0117: { + static const EnemyType types[3] = {EnemyType::GORAN, EnemyType::PYRO_GORAN, EnemyType::GORAN_DETONATOR}; + enemies.emplace_back(types[e.skin % 3]); + break; + } case 0x0119: // Saint-Million, Shambertin, Kondrieu - create_enemy(e, 0x22, - (e.reserved[10] & 0x800000) ? 21 : (19 + (e.skin & 0x01)), - "(Saint-Million|Shambertin|Kondrieu)"); + if (e.unknown_a4 & 0x800000) { + enemies.emplace_back(EnemyType::KONDRIEU); + } else { + enemies.emplace_back((e.skin & 1) ? EnemyType::SHAMBERTIN : EnemyType::SAINT_MILLION); + } break; default: - enemies.emplace_back(next_enemy_id++, e.base, 0xFFFFFFFF, 0, 0, "__unknown__"); + enemies.emplace_back(EnemyType::UNKNOWN); static_game_data_log.warning( "(Entry %zu, offset %zX in file) Unknown enemy type %08" PRIX32 " %08" PRIX32, - y, y * sizeof(EnemyEntry), e.base, e.skin); + y, y * sizeof(EnemyEntry), e.base.load(), e.skin.load()); break; } create_clones(e.num_clones); } - - return enemies; } SetDataTable::SetDataTable(shared_ptr data, bool big_endian) { diff --git a/src/Map.hh b/src/Map.hh index bbba41f3..fbe7af3d 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -8,85 +8,43 @@ #include #include +#include "BattleParamsIndex.hh" #include "PSOEncryption.hh" #include "StaticGameData.hh" #include "Text.hh" -class BattleParamsIndex { -public: - struct Entry { - le_uint16_t atp; // attack power - le_uint16_t psv; // perseverance (intelligence?) - le_uint16_t evp; // evasion - le_uint16_t hp; // hit points - le_uint16_t dfp; // defense - le_uint16_t ata; // accuracy - le_uint16_t lck; // luck - le_uint16_t esp; // ??? - parray unknown_a1; - le_uint32_t experience; - le_uint32_t difficulty; +struct Map { + struct Enemy { + static uint64_t next_enemy_id; + + enum Flag { + HIT_BY_PLAYER0 = 0x01, + HIT_BY_PLAYER1 = 0x02, + HIT_BY_PLAYER2 = 0x04, + HIT_BY_PLAYER3 = 0x08, + DEFEATED = 0x10, + ITEM_DROPPED = 0x20, + }; + uint64_t id; + EnemyType type; + uint8_t flags; + uint8_t last_hit_by_client_id; + + explicit Enemy(EnemyType type); std::string str() const; } __attribute__((packed)); - struct Table { - parray, 4> difficulty; + std::vector enemies; - void print(FILE* stream) const; - } __attribute__((packed)); - - BattleParamsIndex( - std::shared_ptr data_on_ep1, // BattleParamEntry_on.dat - std::shared_ptr data_on_ep2, // BattleParamEntry_lab_on.dat - std::shared_ptr data_on_ep4, // BattleParamEntry_ep4_on.dat - std::shared_ptr data_off_ep1, // BattleParamEntry.dat - std::shared_ptr data_off_ep2, // BattleParamEntry_lab.dat - std::shared_ptr data_off_ep4); // BattleParamEntry_ep4.dat - - const Entry& get(bool solo, Episode episode, uint8_t difficulty, - uint8_t monster_type) const; - -private: - struct LoadedFile { - std::shared_ptr data; - const Table* table; - }; - - // online/offline, episode - LoadedFile files[2][3]; + void clear(); + void add_enemies_from_map_data( + Episode episode, + uint8_t difficulty, + uint8_t event, + std::shared_ptr data); }; -struct PSOEnemy { - uint64_t id; - uint16_t source_type; - uint8_t hit_flags; - uint8_t last_hit; - uint32_t experience; - uint32_t rt_index; - size_t num_clones; - const char* type_name; - - explicit PSOEnemy(uint64_t id); - PSOEnemy( - uint64_t id, - uint16_t source_type, - uint32_t experience, - uint32_t rt_index, - size_t num_clones, - const char* type_name); - - std::string str() const; -} __attribute__((packed)); - -std::vector parse_map( - std::shared_ptr battle_params, - bool is_solo, - Episode episode, - uint8_t difficulty, - std::shared_ptr data, - bool alt_enemies); - // TODO: This class is currently unused. It would be nice if we could use this // to generate variations and link to the corresponding map filenames, but it // seems that SetDataTable.rel files link to map filenames that don't actually diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ad940f60..a643ad75 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3215,6 +3215,7 @@ shared_ptr create_game_generic( } game->next_game_item_id = 0x00810000; + game->map.reset(new Map()); for (size_t area = 0; area < 0x10; area++) { c->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32, area, game->variations[area * 2].load(), game->variations[area * 2 + 1].load()); @@ -3233,21 +3234,13 @@ shared_ptr create_game_generic( for (const string& filename : filenames) { try { auto map_data = s->load_bb_file(filename, "", "map/" + filename); - std::vector area_enemies = parse_map( - s->battle_params, - is_solo, - game->episode, - game->difficulty, - map_data, - false); - game->enemies.insert( - game->enemies.end(), - area_enemies.begin(), - area_enemies.end()); + size_t start_offset = game->map->enemies.size(); + game->map->add_enemies_from_map_data(game->episode, game->difficulty, game->event, map_data); + size_t entries_loaded = game->map->enemies.size() - start_offset; c->log.info("[Map/%zu] Loaded %s (%zu entries)", - area, filename.c_str(), area_enemies.size()); - for (size_t z = 0; z < area_enemies.size(); z++) { - string e_str = area_enemies[z].str(); + area, filename.c_str(), entries_loaded); + for (size_t z = start_offset; z < game->map->enemies.size(); z++) { + string e_str = game->map->enemies[z].str(); static_game_data_log.info("(Entry %zX) %s", z, e_str.c_str()); } any_map_loaded = true; @@ -3261,7 +3254,7 @@ shared_ptr create_game_generic( } } - c->log.info("Loaded maps contain %zu entries overall", game->enemies.size()); + c->log.info("Loaded maps contain %zu entries overall", game->map->enemies.size()); } return game; } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index ac39bc8e..0a369bc8 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1122,91 +1122,122 @@ static void on_set_quest_flag(shared_ptr, } } -// enemy hit by player static void on_enemy_hit(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { + const void* data, size_t size) { if (l->version == GameVersion::BB) { - const auto& cmd = check_size_sc(data); + const auto& cmd = check_size_t(data, size); if (!l->is_game()) { return; } - if (cmd.enemy_id >= l->enemies.size()) { + if (c->lobby_client_id > 3) { + throw logic_error("client ID is above 3"); + } + if (!l->map) { + throw runtime_error("game does not have a map loaded"); + } + if (cmd.enemy_id >= l->map->enemies.size()) { return; } - if (l->enemies[cmd.enemy_id].hit_flags & 0x80) { + auto& enemy = l->map->enemies[cmd.enemy_id]; + if (enemy.flags & Map::Enemy::Flag::DEFEATED) { return; } - l->enemies[cmd.enemy_id].hit_flags |= (1 << c->lobby_client_id); - l->enemies[cmd.enemy_id].last_hit = c->lobby_client_id; + enemy.flags |= (Map::Enemy::Flag::HIT_BY_PLAYER0 << c->lobby_client_id); + enemy.last_hit_by_client_id = c->lobby_client_id; } - forward_subcommand(l, c, command, flag, data); + forward_subcommand(l, c, command, flag, data, size); } -static void on_enemy_killed(shared_ptr s, +static void on_charge_attack_bb(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - forward_subcommand(l, c, command, flag, data); + const void* data, size_t size) { + if (l->version != GameVersion::BB) { + throw runtime_error("BB-only command sent in non-BB game"); + } - if (l->version == GameVersion::BB) { - const auto& cmd = check_size_sc(data); + forward_subcommand(l, c, command, flag, data, size); - if (!l->is_game()) { - throw runtime_error("client should not kill enemies outside of games"); + const auto& cmd = check_size_t(data, size); + auto& disp = c->game_data.player()->disp; + if (cmd.meseta_amount > disp.meseta) { + disp.meseta = 0; + } else { + disp.meseta -= cmd.meseta_amount; + } +} + +static void on_enemy_killed_bb(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const void* data, size_t size) { + if (l->version != GameVersion::BB) { + throw runtime_error("BB-only command sent in non-BB game"); + } + + forward_subcommand(l, c, command, flag, data, size); + + const auto& cmd = check_size_t(data, size); + + if (!l->is_game()) { + throw runtime_error("client should not kill enemies outside of games"); + } + if (!l->map) { + throw runtime_error("game does not have a map loaded"); + } + if (cmd.enemy_id >= l->map->enemies.size()) { + send_text_message(c, u"$C6Missing enemy killed"); + return; + } + + auto& e = l->map->enemies[cmd.enemy_id]; + string e_str = e.str(); + c->log.info("Enemy killed: entry %hu => %s", cmd.enemy_id.load(), e_str.c_str()); + if (e.flags & Map::Enemy::Flag::DEFEATED) { + if (c->options.debug) { + send_text_message_printf(c, "$C5E-%hX (already defeated)", cmd.enemy_id.load()); } - if (cmd.enemy_id >= l->enemies.size()) { - send_text_message(c, u"$C6Missing enemy killed"); - return; + return; + } + + uint32_t experience = 0xFFFFFFFF; + try { + experience = s->battle_params->get(l->mode == GameMode::SOLO, l->episode, l->difficulty, e.type).experience; + } catch (const exception& e) { + if (c->options.debug) { + send_text_message_printf(c, "$C5E-%hX (missing definition)\n%s", cmd.enemy_id.load(), e.what()); + } else { + send_text_message_printf(c, "$C4Unknown enemy type killed:\n%s", e.what()); + } + } + + e.flags |= Map::Enemy::Flag::DEFEATED; + for (size_t x = 0; x < l->max_clients; x++) { + if (!((e.flags >> x) & 1)) { + continue; // Player did not hit this enemy } - auto& e = l->enemies[cmd.enemy_id]; - string e_str = e.str(); - c->log.info("Enemy killed: entry %hu => %s", cmd.enemy_id.load(), e_str.c_str()); - if (e.hit_flags & 0x80) { - if (c->options.debug) { - send_text_message_printf(c, "$C5E-%hX (already dead)", cmd.enemy_id.load()); - } - return; // Enemy is already dead + auto other_c = l->clients[x]; + if (!other_c) { + continue; // No player } - if (e.experience == 0xFFFFFFFF) { - if (c->options.debug) { - send_text_message_printf(c, "$C5E-%hX (missing definition)", cmd.enemy_id.load()); - } else { - send_text_message(c, u"$C6Unknown enemy type killed"); - } - return; + if (other_c->game_data.player()->disp.level >= 199) { + continue; // Player is level 200 or higher } - e.hit_flags |= 0x80; - for (size_t x = 0; x < l->max_clients; x++) { - if (!((e.hit_flags >> x) & 1)) { - continue; // Player did not hit this enemy - } - - auto other_c = l->clients[x]; - if (!other_c) { - continue; // No player - } - if (other_c->game_data.player()->disp.level >= 199) { - continue; // Player is level 200 or higher - } - + if (experience != 0xFFFFFFFF) { // Killer gets full experience, others get 77% - uint32_t exp; - if (e.last_hit == other_c->lobby_client_id) { - exp = e.experience; - } else { - exp = ((e.experience * 77) / 100); - } + uint32_t player_exp = (e.last_hit_by_client_id == other_c->lobby_client_id) + ? experience + : ((experience * 77) / 100); - other_c->game_data.player()->disp.experience += exp; - send_give_experience(l, other_c, exp); + other_c->game_data.player()->disp.experience += player_exp; + send_give_experience(l, other_c, player_exp); if (other_c->options.debug) { send_text_message_printf(other_c, "$C5+%" PRIu32 " E-%hX (%s)", - exp, cmd.enemy_id.load(), e.type_name); + player_exp, cmd.enemy_id.load(), name_for_enum(e.type)); } bool leveled_up = false;