From 149e746e3a0b44e81c3ec38b8d835a71bc634ff1 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 4 Jan 2025 22:53:39 -0800 Subject: [PATCH] support dynamic objects in map state; closes #589 --- src/CommandFormats.hh | 11 ++++-- src/Map.cc | 62 ++++++++++++++++++++++++++-------- src/Map.hh | 70 ++++++++++++++++++++++++--------------- src/ReceiveSubcommands.cc | 30 +++++++++-------- 4 files changed, 117 insertions(+), 56 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index e655e397..5a0e9b0c 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -5213,7 +5213,14 @@ check_struct_size(G_BattleScoresBE_6x7F, 0x24); struct G_TriggerTrap_6x80 { G_ClientIDHeader header; // Traps set by players are numbered according to their type and who set - // them. The trap number is computed as (client_id * 80) + (trap_type * 20). + // them. The trap number is (client_id * 80) + (trap_type * 20) + trap_index. + // trap_index comes from the 6x83 command, described below. + // Note that the trap number does not directly correspond to a specific + // object ID. Instead, object IDs past the end of the map data are + // dynamically allocated when players place traps. + // TODO: What happens in the case of data races, e.g. if two players set + // traps at the same time? Does the game just desync because they get + // different object IDs on different clients? le_uint16_t trap_number = 0; le_uint16_t what = 0; // Must be 0, 1, or 2 } __packed_ws__(G_TriggerTrap_6x80, 8); @@ -5235,7 +5242,7 @@ struct G_EnableDropWeaponOnDeath_6x82 { struct G_PlaceTrap_6x83 { G_ClientIDHeader header; le_uint16_t trap_type = 0; - le_uint16_t unknown_a2 = 0; + le_uint16_t trap_index = 0; } __packed_ws__(G_PlaceTrap_6x83, 8); // 6x84: Vol Opt boss actions (not valid on Episode 3) diff --git a/src/Map.cc b/src/Map.cc index 8090dc29..fa1f083c 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -3377,6 +3377,7 @@ MapState::MapState( } } + this->compute_dynamic_object_base_indexes(); this->verify(); } @@ -3396,6 +3397,7 @@ MapState::MapState( FloorConfig& fc = this->floor_config_entries.emplace_back(); fc.super_map = quest_map_def; this->index_super_map(fc, opt_rand_crypt); + this->compute_dynamic_object_base_indexes(); this->verify(); } @@ -3506,11 +3508,35 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrdynamic_obj_base_k_id = this->object_states.size(); + + // Compute the maximum object ID for each version. We can't just use the last + // object because that object may not exist on all versions, and we can't + // just look at the last floor, because that floor may be empty on some + // versions. + this->dynamic_obj_base_index_for_version.fill(0); + for (const auto& fc : this->floor_config_entries) { + for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) { + if (fc.super_map) { + auto& last_obj_index = this->dynamic_obj_base_index_for_version[static_cast(v)]; + size_t base_index = fc.base_indexes_for_version(v).base_object_index; + last_obj_index = max(base_index + fc.super_map->version(v).objects.size(), last_obj_index); + } + } + } +} + uint16_t MapState::index_for_object_state(Version version, shared_ptr obj_st) const { - uint16_t relative_index = obj_st->super_obj->version(version).relative_object_index; - return (relative_index == 0xFFFF) - ? 0xFFFF - : (relative_index + this->floor_config(obj_st->super_obj->floor).base_indexes_for_version(version).base_object_index); + if (obj_st->super_obj) { + uint16_t relative_index = obj_st->super_obj->version(version).relative_object_index; + return (relative_index == 0xFFFF) + ? 0xFFFF + : (relative_index + this->floor_config(obj_st->super_obj->floor).base_indexes_for_version(version).base_object_index); + } else { + size_t k_id_delta = obj_st->k_id - this->dynamic_obj_base_k_id; + return this->dynamic_obj_base_index_for_version.at(static_cast(version)) + k_id_delta; + } } uint16_t MapState::index_for_enemy_state(Version version, shared_ptr ene_st) const { @@ -3535,16 +3561,26 @@ uint16_t MapState::index_for_event_state(Version version, shared_ptr MapState::object_state_for_index(Version version, uint8_t floor, uint16_t object_index) { - const auto& fc = this->floor_config(floor); - size_t base_object_index = fc.base_indexes_for_version(version).base_object_index; - if (object_index < base_object_index) { - throw runtime_error("object is not on the specified floor"); + size_t dynamic_obj_base_index = this->dynamic_obj_base_index_for_version.at(static_cast(version)); + if (object_index < dynamic_obj_base_index) { + const auto& fc = this->floor_config(floor); + size_t base_object_index = fc.base_indexes_for_version(version).base_object_index; + if (object_index < base_object_index) { + throw runtime_error("object is not on the specified floor"); + } + if (!fc.super_map) { + throw out_of_range("there are no objects on the specified floor"); + } + const auto& obj = fc.super_map->version(version).objects.at(object_index - base_object_index); + return this->object_states.at(fc.base_super_ids.base_object_index + obj->super_id); + + } else { + size_t k_id_delta = object_index - dynamic_obj_base_index; + auto obj_st = make_shared(); + obj_st->k_id = this->dynamic_obj_base_k_id + k_id_delta; + obj_st->super_obj = nullptr; + return obj_st; } - if (!fc.super_map) { - throw out_of_range("there are no objects on the specified floor"); - } - const auto& obj = fc.super_map->version(version).objects.at(object_index - base_object_index); - return this->object_states.at(fc.base_super_ids.base_object_index + obj->super_id); } vector> MapState::object_states_for_floor_section_group( diff --git a/src/Map.hh b/src/Map.hh index e91b1f73..436e50c9 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -615,7 +615,8 @@ protected: // for every game; the others are essentially immutable data once loaded, which // this class refers to. -struct MapState { +class MapState { +public: struct RareEnemyRates { uint32_t hildeblue; // HILDEBEAR -> HILDEBLUE uint32_t rappy; // RAG_RAPPY -> {AL_RAPPY or seasonal rappies}; SAND_RAPPY -> DEL_RAPPY @@ -639,6 +640,12 @@ struct MapState { static const std::shared_ptr DEFAULT_RARE_ENEMIES; struct ObjectState { + // WARNING: super_obj CAN BE NULL! This is not the case for enemies and + // events; their super entities are never null. In the case of objects, + // dynamic objects like player-set traps have object IDs past the end of + // the map's object list, and when queried, the MapState will return a + // temporary ObjectState with a null super_obj. (In these cases, only k_id + // is needed for correctness.) std::shared_ptr super_obj; size_t k_id = 0; uint16_t game_flags = 0; @@ -650,6 +657,12 @@ struct MapState { this->set_flags = 0; this->item_drop_checked = false; } + + inline const char* type_name(Version v) const { + return this->super_obj + ? MapFile::name_for_object_type(this->super_obj->version(v).set_entry->base_type) + : ""; + } }; struct EnemyState { @@ -710,25 +723,6 @@ struct MapState { } }; - struct FloorConfig { - struct EntityBaseIndexes { - size_t base_object_index = 0; - size_t base_enemy_index = 0; - size_t base_enemy_set_index = 0; - size_t base_event_index = 0; - }; - std::shared_ptr super_map; - std::array indexes; - EntityBaseIndexes base_super_ids; - - EntityBaseIndexes& base_indexes_for_version(Version v) { - return this->indexes.at(static_cast(v)); - } - const EntityBaseIndexes& base_indexes_for_version(Version v) const { - return this->indexes.at(static_cast(v)); - } - }; - class EntityIterator { public: EntityIterator(MapState* map_state, Version version, bool at_end); @@ -750,6 +744,25 @@ struct MapState { size_t relative_index; }; + struct FloorConfig { + struct EntityBaseIndexes { + size_t base_object_index = 0; + size_t base_enemy_index = 0; + size_t base_enemy_set_index = 0; + size_t base_event_index = 0; + }; + std::shared_ptr super_map; + std::array indexes; + EntityBaseIndexes base_super_ids; + + EntityBaseIndexes& base_indexes_for_version(Version v) { + return this->indexes.at(static_cast(v)); + } + const EntityBaseIndexes& base_indexes_for_version(Version v) const { + return this->indexes.at(static_cast(v)); + } + }; + class ObjectIterator : public EntityIterator { public: using EntityIterator::EntityIterator; @@ -803,15 +816,17 @@ struct MapState { phosg::PrefixedLogger log; std::vector floor_config_entries; - uint8_t difficulty; - uint8_t event; - uint32_t random_seed; + uint8_t difficulty = 0; + uint8_t event = 0; + uint32_t random_seed = 0; std::shared_ptr bb_rare_rates; std::vector> object_states; std::vector> enemy_states; std::vector> enemy_set_states; std::vector> event_states; std::vector bb_rare_enemy_indexes; + size_t dynamic_obj_base_k_id = 0; + std::array dynamic_obj_base_index_for_version = {}; // Constructor for free play MapState( @@ -833,13 +848,11 @@ struct MapState { std::shared_ptr quest_map_def); // Constructor for empty maps (used in challenge mode before a quest starts) MapState(); + ~MapState() = default; - // Resets states of all entities to their initial values. Used when - // restarting battles/challenges. - void reset(); - void index_super_map(const FloorConfig& floor_config, std::shared_ptr opt_rand_crypt); + void compute_dynamic_object_base_indexes(); inline FloorConfig& floor_config(uint8_t floor) { return this->floor_config_entries[std::min(floor, this->floor_config_entries.size() - 1)]; @@ -847,6 +860,9 @@ struct MapState { inline const FloorConfig& floor_config(uint8_t floor) const { return this->floor_config_entries[std::min(floor, this->floor_config_entries.size() - 1)]; } + // Resets states of all entities to their initial values. Used when + // restarting battles/challenges. + void reset(); inline Range iter_object_states(Version version) { return Range{.map_state = this, .version = version}; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 3095828a..ff675391 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -363,10 +363,10 @@ void forward_subcommand_with_entity_id_transcode_t( shared_ptr ene_st; shared_ptr obj_st; - if ((cmd_entity_id >= 0x1000) && (cmd_entity_id < 0x2000)) { - ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd_entity_id & 0x0FFF); - } else if ((cmd_entity_id >= 0x4000) && (cmd_entity_id < 0x5000)) { - obj_st = l->map_state->object_state_for_index(c->version(), c->floor, cmd_entity_id & 0x0FFF); + if ((cmd_entity_id >= 0x1000) && (cmd_entity_id < 0x4000)) { + ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd_entity_id - 0x1000); + } else if ((cmd_entity_id >= 0x4000) && (cmd_entity_id < 0xFFFF)) { + obj_st = l->map_state->object_state_for_index(c->version(), c->floor, cmd_entity_id - 0x4000); } for (auto& lc : l->clients) { @@ -424,10 +424,10 @@ void forward_subcommand_with_entity_targets_transcode_t( vector resolutions; for (size_t z = 0; z < cmd.target_count; z++) { auto& res = resolutions.emplace_back(TargetResolution{nullptr, nullptr, cmd.targets[z].entity_id}); - if ((res.entity_id >= 0x1000) && (res.entity_id < 0x2000)) { - res.ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, res.entity_id & 0x0FFF); - } else if ((res.entity_id >= 0x4000) && (res.entity_id < 0x5000)) { - res.obj_st = l->map_state->object_state_for_index(c->version(), c->floor, res.entity_id & 0x0FFF); + if ((res.entity_id >= 0x1000) && (res.entity_id < 0x4000)) { + res.ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, res.entity_id - 0x1000); + } else if ((res.entity_id >= 0x4000) && (res.entity_id < 0xFFFF)) { + res.obj_st = l->map_state->object_state_for_index(c->version(), c->floor, res.entity_id - 0x4000); } } @@ -1705,7 +1705,7 @@ static void on_switch_state_changed(shared_ptr c, uint8_t command, uint8 (cmd.header.entity_id != 0xFFFF) && (cmd.switch_flag_num < 0x100) && c->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) { - auto sw_obj_st = l->map_state->object_state_for_index(c->version(), cmd.switch_flag_floor, cmd.header.entity_id & 0x0FFF); + auto sw_obj_st = l->map_state->object_state_for_index(c->version(), cmd.switch_flag_floor, cmd.header.entity_id - 0x4000); c->log.info("Switch assist triggered by K-%03zX setting SW-%02hhX-%02hX", sw_obj_st->k_id, cmd.switch_flag_floor, cmd.switch_flag_num.load()); for (auto obj_st : l->map_state->door_states_for_switch_flag(c->version(), cmd.switch_flag_floor, cmd.switch_flag_num)) { @@ -1741,9 +1741,8 @@ static void on_switch_state_changed(shared_ptr c, uint8_t command, uint8 if (cmd.header.entity_id != 0xFFFF && c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { const auto& obj_st = l->map_state->object_state_for_index( - c->version(), cmd.switch_flag_floor, cmd.header.entity_id & 0x0FFF); - send_text_message_printf(c, "$C5K-%03zX A %s", - obj_st->k_id, MapFile::name_for_object_type(obj_st->super_obj->version(c->version()).set_entry->base_type)); + c->version(), cmd.switch_flag_floor, cmd.header.entity_id - 0x4000); + send_text_message_printf(c, "$C5K-%03zX A %s", obj_st->k_id, obj_st->type_name(c->version())); } if (l->switch_flags) { @@ -2757,6 +2756,9 @@ DropReconcileResult reconcile_drop_request_with_map( if (is_box) { if (map) { 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"); + } const auto* set_entry = res.obj_st->super_obj->version(version).set_entry; if (!set_entry) { throw std::runtime_error("object set entry is missing"); @@ -3476,7 +3478,7 @@ static void on_dragon_actions(shared_ptr c, uint8_t command, uint8_t, vo return; } - auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.header.entity_id & 0x0FFF); + auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.header.entity_id - 0x1000); if (ene_st->super_ene->type != EnemyType::DRAGON) { throw runtime_error("6x12 command sent for incorrect enemy type"); } @@ -3512,7 +3514,7 @@ static void on_gol_dragon_actions(shared_ptr c, uint8_t command, uint8_t return; } - auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.header.entity_id & 0x0FFF); + auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.header.entity_id - 0x1000); if (ene_st->super_ene->type != EnemyType::GOL_DRAGON) { throw runtime_error("6xA8 command sent for incorrect enemy type"); }