support dynamic objects in map state; closes #589

This commit is contained in:
Martin Michelsen
2025-01-04 22:53:39 -08:00
parent 1c5b0e4667
commit 149e746e3a
4 changed files with 117 additions and 56 deletions
+9 -2
View File
@@ -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)
+49 -13
View File
@@ -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_ptr<PSOLFGEncryptio
}
}
void MapState::compute_dynamic_object_base_indexes() {
this->dynamic_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<size_t>(v)];
size_t base_index = fc.base_indexes_for_version(v).base_object_index;
last_obj_index = max<size_t>(base_index + fc.super_map->version(v).objects.size(), last_obj_index);
}
}
}
}
uint16_t MapState::index_for_object_state(Version version, shared_ptr<const ObjectState> 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<size_t>(version)) + k_id_delta;
}
}
uint16_t MapState::index_for_enemy_state(Version version, shared_ptr<const EnemyState> ene_st) const {
@@ -3535,16 +3561,26 @@ uint16_t MapState::index_for_event_state(Version version, shared_ptr<const Event
}
shared_ptr<MapState::ObjectState> 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<size_t>(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<ObjectState>();
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<shared_ptr<MapState::ObjectState>> MapState::object_states_for_floor_section_group(
+43 -27
View File
@@ -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<const RareEnemyRates> 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<const SuperMap::Object> 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)
: "<PLAYER TRAP>";
}
};
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<const SuperMap> super_map;
std::array<EntityBaseIndexes, NUM_VERSIONS> indexes;
EntityBaseIndexes base_super_ids;
EntityBaseIndexes& base_indexes_for_version(Version v) {
return this->indexes.at(static_cast<size_t>(v));
}
const EntityBaseIndexes& base_indexes_for_version(Version v) const {
return this->indexes.at(static_cast<size_t>(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<const SuperMap> super_map;
std::array<EntityBaseIndexes, NUM_VERSIONS> indexes;
EntityBaseIndexes base_super_ids;
EntityBaseIndexes& base_indexes_for_version(Version v) {
return this->indexes.at(static_cast<size_t>(v));
}
const EntityBaseIndexes& base_indexes_for_version(Version v) const {
return this->indexes.at(static_cast<size_t>(v));
}
};
class ObjectIterator : public EntityIterator {
public:
using EntityIterator::EntityIterator;
@@ -803,15 +816,17 @@ struct MapState {
phosg::PrefixedLogger log;
std::vector<FloorConfig> 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<const RareEnemyRates> bb_rare_rates;
std::vector<std::shared_ptr<ObjectState>> object_states;
std::vector<std::shared_ptr<EnemyState>> enemy_states;
std::vector<std::shared_ptr<EnemyState>> enemy_set_states;
std::vector<std::shared_ptr<EventState>> event_states;
std::vector<size_t> bb_rare_enemy_indexes;
size_t dynamic_obj_base_k_id = 0;
std::array<size_t, NUM_VERSIONS> dynamic_obj_base_index_for_version = {};
// Constructor for free play
MapState(
@@ -833,13 +848,11 @@ struct MapState {
std::shared_ptr<const SuperMap> 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<PSOLFGEncryption> opt_rand_crypt);
void compute_dynamic_object_base_indexes();
inline FloorConfig& floor_config(uint8_t floor) {
return this->floor_config_entries[std::min<uint8_t>(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<uint8_t>(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<ObjectIterator> iter_object_states(Version version) {
return Range<ObjectIterator>{.map_state = this, .version = version};
+16 -14
View File
@@ -363,10 +363,10 @@ void forward_subcommand_with_entity_id_transcode_t(
shared_ptr<const MapState::EnemyState> ene_st;
shared_ptr<const MapState::ObjectState> 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<TargetResolution> 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<Client> 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<Client> 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<Client> 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<Client> 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");
}