diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index ca249032..90f743cb 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -1654,8 +1654,7 @@ void ItemCreator::generate_weapon_shop_item_bonus1( } else { const auto* range = this->weapon_random_set->get_bonus_range(0, table_index); - item.data1[7] = bonus_values.at(max( - this->rand_int(range->max + 1), range->min)); + item.data1[7] = bonus_values.at(max(this->rand_int(range->max + 1), range->min)); } } @@ -1700,8 +1699,7 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player } else { const auto* range = this->weapon_random_set->get_bonus_range(1, table_index); - item.data1[9] = bonus_values.at(max( - this->rand_int(range->max + 1), range->min)); + item.data1[9] = bonus_values.at(max(this->rand_int(range->max + 1), range->min)); } } diff --git a/src/Map.cc b/src/Map.cc index fe261bb7..58208496 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -4320,13 +4320,11 @@ string SuperMap::Enemy::id_str() const { } string SuperMap::Enemy::str() const { - string ret = std::format("[Enemy ES-{:02X}-{:03X}-{:03X} type={} child_index={:X} alias_enemy_index_delta={:X} is_default_rare_v123={} is_default_rare_bb={}", - this->floor, - this->super_set_id, - this->super_id, + string ret = std::format("[Enemy {} type={} child_index={:X} alias_target_ene={} is_default_rare_v123={} is_default_rare_bb={}", + this->id_str(), phosg::name_for_enum(this->type), this->child_index, - this->alias_enemy_index_delta, + (this->alias_target_ene ? this->alias_target_ene->id_str() : "(none)"), this->is_default_rare_v123 ? "true" : "false", this->is_default_rare_bb ? "true" : "false"); for (Version v : ALL_NON_PATCH_VERSIONS) { @@ -4473,7 +4471,11 @@ shared_ptr SuperMap::add_enemy_and_children( auto add = [&](EnemyType type, bool is_default_rare_v123 = false, bool is_default_rare_bb = false, - int16_t alias_enemy_index_delta = 0) -> void { + std::shared_ptr alias_target_ene = nullptr) -> std::shared_ptr { + if (alias_target_ene && alias_target_ene->alias_target_ene) { + throw std::logic_error("alias may not point to an enemy that also is an alias"); + } + auto& entities = this->version(version); // TODO: It'd be nice to share some code between this function and @@ -4488,7 +4490,7 @@ shared_ptr SuperMap::add_enemy_and_children( ene->type = type; ene->is_default_rare_v123 = is_default_rare_v123; ene->is_default_rare_bb = is_default_rare_bb; - ene->alias_enemy_index_delta = alias_enemy_index_delta; + ene->alias_target_ene = alias_target_ene; auto& ene_ver = ene->version(version); ene_ver.set_entry = set_entry; ene_ver.relative_enemy_index = entities.enemies.size(); @@ -4510,6 +4512,7 @@ shared_ptr SuperMap::add_enemy_and_children( // Add to room/group index uint64_t k = room_index_key(ene->floor, set_entry->room, set_entry->wave_number); entities.enemy_for_floor_room_and_wave_number.emplace(k, ene); + return ene; }; // The following logic was originally based on the public version of @@ -4753,26 +4756,28 @@ shared_ptr SuperMap::add_enemy_and_children( case 0x00C5: // Unnamed subclass of TObjEnemyCustom add(EnemyType::VOL_OPT_2); break; - case 0x00C8: // TBoss4DarkFalz + case 0x00C8: { // TBoss4DarkFalz if ((set_entry->num_children != 0) && (set_entry->num_children != 0x200)) { this->log.warning_f("DARK_FALZ has an unusual num_children (0x{:X})", set_entry->num_children); } - add(EnemyType::DARK_FALZ_3); + auto root_ene = add(EnemyType::DARK_FALZ_3); default_num_children = -1; // Skip adding children (because we do it here) for (size_t x = 0; x < 0x1FD; x++) { add(EnemyType::DARVANT); } - add(EnemyType::DARK_FALZ_3, false, false, -0x1FE); - add(EnemyType::DARK_FALZ_2, false, false, -0x1FF); - add(EnemyType::DARK_FALZ_1, false, false, -0x200); + add(EnemyType::DARK_FALZ_3, false, false, root_ene); + add(EnemyType::DARK_FALZ_2, false, false, root_ene); + add(EnemyType::DARK_FALZ_1, false, false, root_ene); break; - case 0x00CA: // TBoss6PlotFalz - add(EnemyType::OLGA_FLOW_2); + } + case 0x00CA: { // TBoss6PlotFalz + auto root_ene = add(EnemyType::OLGA_FLOW_2); default_num_children = -1; // Skip adding children (because we do it here) for (size_t x = 0; x < 0x200; x++) { - add(EnemyType::OLGA_FLOW_2, false, false, -(x + 1)); + add(EnemyType::OLGA_FLOW_2, false, false, root_ene); } break; + } case 0x00CB: // TBoss7DeRolLeC add(EnemyType::BARBA_RAY); child_type = EnemyType::PIG_RAY; @@ -5486,25 +5491,16 @@ vector> SuperMap::doors_for_switch_flag( return ret; } -shared_ptr SuperMap::enemy_for_index(Version version, uint16_t enemy_id, bool follow_alias) const { +shared_ptr SuperMap::enemy_for_index(Version version, uint16_t enemy_index) const { const auto& entities = this->version(version); if (entities.enemies.empty()) { throw out_of_range("no enemies defined"); } - if (enemy_id >= entities.enemies.size()) { + if (enemy_index >= entities.enemies.size()) { throw out_of_range("enemy ID out of range"); } - auto& enemy = entities.enemies[enemy_id]; - if (follow_alias && (enemy->alias_enemy_index_delta != 0)) { - uint16_t target_id = enemy_id + enemy->alias_enemy_index_delta; - if (target_id >= entities.enemies.size()) { - throw out_of_range("aliased enemy ID out of range"); - } - return entities.enemies[target_id]; - } else { - return enemy; - } + return entities.enemies[enemy_index]; } shared_ptr SuperMap::enemy_for_floor_type(Version version, uint8_t floor, EnemyType type) const { @@ -5513,10 +5509,13 @@ shared_ptr SuperMap::enemy_for_floor_type(Version version if (entities.enemies.empty()) { throw out_of_range("no enemies defined"); } - // TODO: Linear search is bad here. Do something better, like binary search - // for the floor start and just linear search through the floor enemies. - for (auto& ene : entities.enemies) { - if ((ene->floor == floor) && (ene->type == type)) { + size_t start_z = entities.enemy_floor_start_indexes.at(floor); + size_t end_z = (floor < entities.enemy_floor_start_indexes.size() - 1) + ? entities.enemy_floor_start_indexes[floor + 1] + : entities.enemy_floor_start_indexes.size(); + for (size_t z = start_z; z < end_z; z++) { + auto& ene = entities.enemies[z]; + if ((ene->floor == floor) || (ene->type == type)) { return ene; } } @@ -6153,12 +6152,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrall_enemies()) { auto& ene_st = this->enemy_states.emplace_back(make_shared()); - if (ene->alias_enemy_index_delta) { - ene_st->alias_ene_st = this->enemy_states.at((this->enemy_states.size() - 1) + ene->alias_enemy_index_delta); - if (ene_st->alias_ene_st->alias_ene_st) { - throw std::runtime_error("target for enemy state alias is itself an alias"); - } - } + if (ene->child_index == 0) { this->enemy_set_states.emplace_back(ene_st); } @@ -6166,13 +6160,22 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrset_id = this->enemy_set_states.size() - 1; ene_st->super_ene = ene; + if (ene->alias_target_ene) { + ssize_t delta = ene->alias_target_ene->super_id - ene->super_id; + ene_st->alias_target_ene_st = this->enemy_states.at(ene_st->e_id + delta); + if (ene_st->alias_target_ene_st->super_ene != ene->alias_target_ene) { + throw std::logic_error("found incorrect alias target state for enemy"); + } + if (ene_st->alias_target_ene_st->super_ene->alias_target_ene) { + throw std::runtime_error("target for enemy state alias is itself an alias"); + } + } + // Handle random rare enemies and difficulty-based effects EnemyType type; switch (ene->type) { case EnemyType::DARK_FALZ_3: - type = ((this->difficulty == Difficulty::NORMAL) && (ene->alias_enemy_index_delta == 0)) - ? EnemyType::DARK_FALZ_2 - : EnemyType::DARK_FALZ_3; + type = (this->difficulty == Difficulty::NORMAL) ? EnemyType::DARK_FALZ_2 : EnemyType::DARK_FALZ_3; break; case EnemyType::DARVANT: type = (this->difficulty == Difficulty::ULTIMATE) ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT; @@ -6556,18 +6559,18 @@ void MapState::import_enemy_states_from_sync(Version from_version, const SyncEne const auto& entry = entries[enemy_index]; const auto& ene = entities.enemies.at(enemy_index - base_indexes.base_enemy_index); auto& ene_st = this->enemy_states.at(fc.base_super_ids.base_enemy_index + ene->super_id); - if (ene_st->super_ene != ene) { - throw logic_error("super enemy link is incorrect"); - } - if (ene_st->game_flags != entry.flags) { - this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}) do not match game flags from map ({:08X})", - enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags); - ene_st->game_flags = entry.flags; - } - if (ene_st->total_damage != entry.total_damage) { - this->log.warning_f("({:04X} => E-{:03X}) Total damage from client ({}) does not match total damage from map ({})", - enemy_index, ene_st->e_id, entry.total_damage, ene_st->total_damage); - ene_st->total_damage = entry.total_damage; + // Only set the state if it's not an alias + if (ene_st->super_ene == ene) { + if (ene_st->game_flags != entry.flags) { + this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}) do not match game flags from map ({:08X})", + enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags); + ene_st->game_flags = entry.flags; + } + if (ene_st->total_damage != entry.total_damage) { + this->log.warning_f("({:04X} => E-{:03X}) Total damage from client ({}) does not match total damage from map ({})", + enemy_index, ene_st->e_id, entry.total_damage, ene_st->total_damage); + ene_st->total_damage = entry.total_damage; + } } } } @@ -6876,12 +6879,9 @@ void MapState::print(FILE* stream) const { } phosg::fwrite_fmt(stream, "Enemies:\n"); - phosg::fwrite_fmt(stream, " FL ENEID ALIAS DCTE----- DCPR----- DCV1----- DCV2----- PCTE----- PCV2----- GCTE----- GCV3----- EP3TE---- GCEP3---- XBV3----- BBV4----- ENEMY\n"); + phosg::fwrite_fmt(stream, " FL ENEID DCTE----- DCPR----- DCV1----- DCV2----- PCTE----- PCV2----- GCTE----- GCV3----- EP3TE---- GCEP3---- XBV3----- BBV4----- ENEMY\n"); for (const auto& ene_st : this->enemy_states) { - std::string alias_str = ene_st->super_ene->alias_enemy_index_delta - ? std::format("E-{:03X}", ene_st->e_id + ene_st->super_ene->alias_enemy_index_delta) - : "-----"; - phosg::fwrite_fmt(stream, " {:02X} E-{:03X} {}", ene_st->super_ene->floor, ene_st->e_id, alias_str); + phosg::fwrite_fmt(stream, " {:02X} E-{:03X}", ene_st->super_ene->floor, ene_st->e_id); const auto& fc = this->floor_config(ene_st->super_ene->floor); for (Version v : ALL_NON_PATCH_VERSIONS) { const auto& ene_v = ene_st->super_ene->version(v); diff --git a/src/Map.hh b/src/Map.hh index a749c060..74a02ec4 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -491,7 +491,7 @@ public: size_t super_id = 0; size_t super_set_id = 0; uint16_t child_index = 0; - int16_t alias_enemy_index_delta = 0; // 0 = no alias + std::shared_ptr alias_target_ene; // May be null EnemyType type = EnemyType::UNKNOWN; bool is_default_rare_v123 = false; bool is_default_rare_bb = false; @@ -585,7 +585,7 @@ public: std::vector> doors_for_switch_flag( Version version, uint8_t floor, uint8_t switch_flag) const; - std::shared_ptr enemy_for_index(Version version, uint16_t enemy_index, bool follow_alias) const; + std::shared_ptr enemy_for_index(Version version, uint16_t enemy_index) const; std::shared_ptr enemy_for_floor_type(Version version, uint8_t floor, EnemyType type) const; std::vector> enemies_for_floor_room_wave( Version version, uint8_t floor, uint16_t room, uint16_t wave_number) const; @@ -710,7 +710,7 @@ public: }; struct EnemyState { - std::shared_ptr alias_ene_st; // Null for most enemies + std::shared_ptr alias_target_ene_st; // Null for most enemies std::shared_ptr super_ene; enum Flag { LAST_HIT_MASK = 0x0003, @@ -756,7 +756,7 @@ public: return EnemyType::MERICAROL; } } else if (this->super_ene->type == EnemyType::DARK_FALZ_3) { - return ((difficulty == Difficulty::NORMAL) && (this->super_ene->alias_enemy_index_delta == 0)) + return ((difficulty == Difficulty::NORMAL) && !this->super_ene->alias_target_ene) ? EnemyType::DARK_FALZ_2 : EnemyType::DARK_FALZ_3; } else { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 40f0ff1c..be556012 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2842,8 +2842,8 @@ DropReconcileResult reconcile_drop_request_with_map( res.should_drop = true; res.ignore_def = (cmd.ignore_def != 0); - if (is_box) { - if (map) { + if (map) { + if (is_box) { res.obj_st = map->object_state_for_index(version, cmd.floor, cmd.entity_index); if (!res.obj_st->super_obj) { throw std::runtime_error("referenced object from drop request is a player trap"); @@ -2878,23 +2878,23 @@ DropReconcileResult reconcile_drop_request_with_map( string type_name = MapFile::name_for_object_type(set_entry->base_type, version); send_text_message_fmt(c, "$C5K-{:03X} {} {}", res.obj_st->k_id, res.ignore_def ? 'G' : 'S', type_name); } - } - } else { - if (map) { - res.ene_st = map->enemy_state_for_index(version, cmd.floor, cmd.entity_index); - EnemyType type = res.ene_st->type(version, episode, difficulty, event); - c->log.info_f("Drop check for E-{:03X} {}", res.ene_st->e_id, phosg::name_for_enum(type)); + } else { + res.ref_ene_st = map->enemy_state_for_index(version, cmd.floor, cmd.entity_index); + res.target_ene_st = res.ref_ene_st->alias_target_ene_st ? res.ref_ene_st->alias_target_ene_st : res.ref_ene_st; + EnemyType type = res.target_ene_st->type(version, episode, difficulty, event); + c->log.info_f("Drop check for E-{:03X} (target E-{:03X}, type {})", + res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(type)); res.effective_rt_index = type_definition_for_enemy(type).rt_index; // rt_indexes in Episode 4 don't match those sent in the command; we just // ignore what the client sends. if ((episode != Episode::EP4) && (cmd.rt_index != res.effective_rt_index)) { // Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER if (cmd.rt_index == 0x27 && type == EnemyType::BULCLAW) { - c->log.info_f("E-{:03X} killed as BULK instead of BULCLAW", res.ene_st->e_id); + c->log.info_f("E-{:03X} killed as BULK instead of BULCLAW", res.target_ene_st->e_id); res.effective_rt_index = 0x27; } else if (cmd.rt_index == 0x23 && type == EnemyType::DARK_GUNNER) { - c->log.info_f("E-{:03X} killed as DEATH_GUNNER instead of DARK_GUNNER", res.ene_st->e_id); + c->log.info_f("E-{:03X} killed as DEATH_GUNNER instead of DARK_GUNNER", res.target_ene_st->e_id); res.effective_rt_index = 0x23; } else { c->log.warning_f("rt_index {:02X} from command does not match entity\'s expected index {:02X}", @@ -2904,12 +2904,12 @@ DropReconcileResult reconcile_drop_request_with_map( } } } - if (cmd.floor != res.ene_st->super_ene->floor) { + if (cmd.floor != res.target_ene_st->super_ene->floor) { c->log.warning_f("Floor {:02X} from command does not match entity\'s expected floor {:02X}", - cmd.floor, res.ene_st->super_ene->floor); + cmd.floor, res.target_ene_st->super_ene->floor); } if (c->check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_fmt(c, "$C5E-{:03X} {}", res.ene_st->e_id, phosg::name_for_enum(type)); + send_text_message_fmt(c, "$C5E-{:03X} {}", res.target_ene_st->e_id, phosg::name_for_enum(type)); } } } @@ -2923,12 +2923,12 @@ DropReconcileResult reconcile_drop_request_with_map( res.obj_st->item_drop_checked = true; } } - if (res.ene_st) { - if (res.ene_st->server_flags & MapState::EnemyState::Flag::ITEM_DROPPED) { - c->log.info_f("Drop check has already occurred for E-{:03X}; skipping it", res.ene_st->e_id); + if (res.target_ene_st) { + if (res.target_ene_st->server_flags & MapState::EnemyState::Flag::ITEM_DROPPED) { + c->log.info_f("Drop check has already occurred for E-{:03X}; skipping it", res.target_ene_st->e_id); res.should_drop = false; } else { - res.ene_st->server_flags |= MapState::EnemyState::Flag::ITEM_DROPPED; + res.target_ene_st->server_flags |= MapState::EnemyState::Flag::ITEM_DROPPED; } } } @@ -2989,9 +2989,9 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S return l->item_creator->on_specialized_box_item_drop( cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6); } - } else if (rec.ene_st) { + } else if (rec.target_ene_st) { l->log.info_f("Creating item from enemy {:04X} => E-{:03X} (area {:02X})", - cmd.entity_index, rec.ene_st->e_id, cmd.effective_area); + cmd.entity_index, rec.target_ene_st->e_id, cmd.effective_area); return l->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area); } else { throw runtime_error("neither object nor enemy were present"); @@ -3001,8 +3001,8 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S auto get_entity_index = [&](Version v) -> uint16_t { if (rec.obj_st) { return l->map_state->index_for_object_state(v, rec.obj_st); - } else if (rec.ene_st) { - return l->map_state->index_for_enemy_state(v, rec.ene_st); + } else if (rec.ref_ene_st) { + return l->map_state->index_for_enemy_state(v, rec.ref_ene_st); } else { return 0xFFFF; } @@ -3028,7 +3028,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S res.item.id = l->generate_item_id(0xFF); l->log.info_f("Creating item {:08X} at {:02X}:{:g},{:g} for {}", res.item.id, cmd.floor, cmd.pos.x, cmd.pos.z, lc->channel->name); - l->add_item(cmd.floor, res.item, cmd.pos, rec.obj_st, rec.ene_st, 0x1000 | (1 << lc->lobby_client_id)); + l->add_item(cmd.floor, res.item, cmd.pos, rec.obj_st, rec.ref_ene_st, 0x1000 | (1 << lc->lobby_client_id)); send_drop_item_to_channel( s, lc->channel, res.item, rec.obj_st ? 2 : 1, cmd.floor, cmd.pos, get_entity_index(lc->version())); send_item_notification_if_needed(lc, res.item, res.is_from_rare_table); @@ -3039,7 +3039,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S res.item.id = l->generate_item_id(0xFF); l->log.info_f("Creating item {:08X} at {:02X}:{:g},{:g} for all clients", res.item.id, cmd.floor, cmd.pos.x, cmd.pos.z); - l->add_item(cmd.floor, res.item, cmd.pos, rec.obj_st, rec.ene_st, 0x100F); + l->add_item(cmd.floor, res.item, cmd.pos, rec.obj_st, rec.ref_ene_st, 0x100F); for (auto lc : l->clients) { if (lc) { send_drop_item_to_channel( @@ -3063,7 +3063,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S res.item.id = l->generate_item_id(0xFF); l->log.info_f("Creating item {:08X} at {:02X}:{:g},{:g} for {}", res.item.id, cmd.floor, cmd.pos.x, cmd.pos.z, lc->channel->name); - l->add_item(cmd.floor, res.item, cmd.pos, rec.obj_st, rec.ene_st, 0x1000 | (1 << lc->lobby_client_id)); + l->add_item(cmd.floor, res.item, cmd.pos, rec.obj_st, rec.ref_ene_st, 0x1000 | (1 << lc->lobby_client_id)); send_drop_item_to_channel( s, lc->channel, res.item, rec.obj_st ? 2 : 1, cmd.floor, cmd.pos, get_entity_index(lc->version())); send_item_notification_if_needed(lc, res.item, res.is_from_rare_table); @@ -3151,7 +3151,10 @@ static asio::awaitable on_set_quest_flag(shared_ptr c, SubcommandM uint16_t enemy_index = 0xFFFF; uint8_t enemy_floor = 0xFF; try { - const auto& ene_st = l->map_state->enemy_state_for_floor_type(c->version(), c->floor, boss_enemy_type); + auto ene_st = l->map_state->enemy_state_for_floor_type(c->version(), c->floor, boss_enemy_type); + if (ene_st->alias_target_ene_st) { + ene_st = ene_st->alias_target_ene_st; + } enemy_index = l->map_state->index_for_enemy_state(c->version(), ene_st); enemy_floor = ene_st->super_ene->floor; if (c->floor != ene_st->super_ene->floor) { @@ -3271,8 +3274,7 @@ static asio::awaitable on_set_entity_set_flag(shared_ptr c, Subcom } room = set_entry->room; wave_number = set_entry->wave_number; - l->log.info_f("Client set set flags {:04X} on E-{:03X} (flags are now {:04X})", - cmd.flags, ene_st->e_id, cmd.flags); + l->log.info_f("Client set set flags {:04X} on E-{:03X} (flags are now {:04X})", cmd.flags, ene_st->e_id, cmd.flags); } catch (const out_of_range&) { l->log.warning_f("Flag update refers to missing enemy"); } @@ -3282,8 +3284,7 @@ static asio::awaitable on_set_entity_set_flag(shared_ptr c, Subcom // they are defeated, set event_flags = (event_flags | 0x18) & (~4), // which means it is done and should not trigger bool all_enemies_defeated = true; - l->log.info_f("Checking for defeated enemies with room={:04X} wave_number={:04X}", - room, wave_number); + l->log.info_f("Checking for defeated enemies with room={:04X} wave_number={:04X}", room, wave_number); for (auto ene_st : l->map_state->enemy_states_for_floor_room_wave(c->version(), cmd.floor, room, wave_number)) { if (ene_st->super_ene->child_index) { l->log.info_f("E-{:03X} is a child of another enemy", ene_st->e_id); @@ -3460,12 +3461,12 @@ static asio::awaitable on_update_enemy_state(shared_ptr c, Subcomm ene_st->total_damage = cmd.total_damage; ene_st->set_last_hit_by_client_id(c->lobby_client_id); l->log.info_f("E-{:03X} updated to damage={} game_flags={:08X}", ene_st->e_id, ene_st->total_damage, ene_st->game_flags); - auto& alias_ene_st = ene_st->alias_ene_st; - if (alias_ene_st) { - alias_ene_st->game_flags = ene_st->game_flags; - alias_ene_st->total_damage = ene_st->total_damage; - alias_ene_st->server_flags = ene_st->server_flags; - l->log.info_f("E-{:03X} updated via alias from E-{:03X}", alias_ene_st->e_id, ene_st->e_id); + if (ene_st->alias_target_ene_st) { + ene_st->alias_target_ene_st->game_flags = src_flags; + ene_st->alias_target_ene_st->total_damage = cmd.total_damage; + ene_st->alias_target_ene_st->set_last_hit_by_client_id(c->lobby_client_id); + l->log.info_f("Alias target E-{:03X} updated to damage={} game_flags={:08X}", + ene_st->alias_target_ene_st->e_id, ene_st->alias_target_ene_st->total_damage, ene_st->alias_target_ene_st->game_flags); } for (auto lc : l->clients) { @@ -3504,6 +3505,10 @@ static asio::awaitable on_incr_enemy_damage(shared_ptr c, Subcomma cmd.current_hp_before_hit.load(), cmd.max_hp.load()); ene_st->total_damage = std::min(ene_st->total_damage + cmd.hit_amount, cmd.max_hp); + if (ene_st->alias_target_ene_st) { + ene_st->alias_target_ene_st->total_damage = std::min( + ene_st->alias_target_ene_st->total_damage + cmd.hit_amount, cmd.max_hp); + } co_await forward_subcommand_with_entity_id_transcode_t(c, msg); } @@ -3528,6 +3533,11 @@ static asio::awaitable on_set_enemy_low_game_flags_ultimate(shared_ptrgame_flags |= cmd.low_game_flags; l->log.info_f("E-{:03X} updated to game_flags={:08X}", ene_st->e_id, ene_st->game_flags); } + if (ene_st->alias_target_ene_st && !(ene_st->alias_target_ene_st->game_flags & cmd.low_game_flags)) { + ene_st->alias_target_ene_st->game_flags |= cmd.low_game_flags; + l->log.info_f("Alias E-{:03X} updated to game_flags={:08X}", + ene_st->alias_target_ene_st->e_id, ene_st->alias_target_ene_st->game_flags); + } co_await forward_subcommand_with_entity_id_transcode_t(c, msg); } @@ -3651,6 +3661,9 @@ static asio::awaitable on_dragon_actions_6x12(shared_ptr c, Subcom if (ene_st->super_ene->type != EnemyType::DRAGON) { throw runtime_error("6x12 command sent for incorrect enemy type"); } + if (ene_st->alias_target_ene_st) { + throw runtime_error("DRAGON enemy is an alias"); + } G_DragonBossActions_GC_6x12 sw_cmd = {{cmd.header.subcommand, cmd.header.size, cmd.header.entity_id.load()}, cmd.unknown_a2.load(), cmd.unknown_a3.load(), cmd.unknown_a4.load(), cmd.x.load(), cmd.z.load()}; @@ -3687,6 +3700,9 @@ static asio::awaitable on_gol_dragon_actions(shared_ptr c, Subcomm if (ene_st->super_ene->type != EnemyType::GOL_DRAGON) { throw runtime_error("6xA8 command sent for incorrect enemy type"); } + if (ene_st->alias_target_ene_st) { + throw runtime_error("GOL_DRAGON enemy is an alias"); + } G_GolDragonBossActions_GC_6xA8 sw_cmd = {{cmd.header.subcommand, cmd.header.size, cmd.header.entity_id}, cmd.unknown_a2.load(), @@ -3992,7 +4008,10 @@ static asio::awaitable on_steal_exp_bb(shared_ptr c, SubcommandMes co_return; } - const auto& ene_st = l->map_state->enemy_state_for_index(c->version(), cmd.enemy_index); + auto ene_st = l->map_state->enemy_state_for_index(c->version(), cmd.enemy_index); + if (ene_st->alias_target_ene_st) { + ene_st = ene_st->alias_target_ene_st; + } if (ene_st->super_ene->floor != c->floor) { throw runtime_error("enemy is on a different floor"); } @@ -4049,6 +4068,10 @@ static asio::awaitable on_enemy_exp_request_bb(shared_ptr c, Subco auto ene_st = l->map_state->enemy_state_for_index(c->version(), cmd.enemy_index); string ene_str = ene_st->super_ene->str(); c->log.info_f("EXP requested for E-{:03X}: {}", ene_st->e_id, ene_str); + if (ene_st->alias_target_ene_st) { + c->log.info_f("E-{:03X} is an alias for E-{:03X}", ene_st->e_id, ene_st->alias_target_ene_st->e_id); + ene_st = ene_st->alias_target_ene_st; + } // If the requesting player never hit this enemy, they are probably cheating; // ignore the command. Also, each player sends a 6xC8 if they ever hit the diff --git a/src/ReceiveSubcommands.hh b/src/ReceiveSubcommands.hh index 61b7c85b..8eb261e1 100644 --- a/src/ReceiveSubcommands.hh +++ b/src/ReceiveSubcommands.hh @@ -19,7 +19,10 @@ G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, siz struct DropReconcileResult { std::shared_ptr obj_st; - std::shared_ptr ene_st; + // The ref ene_st is the one the client referenced in the drop request; the target ene_st is the one actually used + // for drop computation (which may be the result of following an alias from the ref ene_st) + std::shared_ptr ref_ene_st; + std::shared_ptr target_ene_st; uint8_t effective_rt_index; bool should_drop; bool ignore_def; diff --git a/tests/GC-AllEp1BossesCheatMode.test.txt b/tests/GC-AllEp1BossesCheatMode.test.txt index 14452461..ecdf8168 100644 --- a/tests/GC-AllEp1BossesCheatMode.test.txt +++ b/tests/GC-AllEp1BossesCheatMode.test.txt @@ -40754,7 +40754,7 @@ I 38563 2025-09-19 00:11:38 - [Game:15] Creating item from final boss (DARK_FALZ I 38563 2025-09-19 00:11:38 - [Game:15] Found enemy E-9A3 at index 083E on floor E I 38563 2025-09-19 00:11:38 - [C-2] Drop check for E-9A3 DARK_FALZ_2 I 38563 2025-09-19 00:11:38 - [Commands] Sending to C-2 (Jess Lv.51) @ ipss:N-1:127.0.0.1:64785 (version=GC_V3 command=62 flag=00) -0000 | 62 00 1C 00 60 06 00 00 0E 2F 3E 08 00 00 68 C2 | b ` /> h +0000 | 62 00 1C 00 60 06 00 00 0E 2F 3F 06 00 00 68 C2 | b ` /> h 0010 | 00 00 F8 41 02 00 00 00 0E 00 00 00 | A I 38563 2025-09-19 00:11:38 - [Commands] Received from C-2 (Jess Lv.51) @ ipss:N-1:127.0.0.1:64785 (version=GC_V3 command=60 flag=00) 0000 | 60 00 10 00 52 03 00 00 08 00 00 00 14 F6 00 00 | ` R