diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 3b21efa5..78adc6cb 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -3890,27 +3890,27 @@ struct G_Unknown_6x09 { G_EnemyIDHeader header; } __packed__; -// 6x0A: Enemy hit +// 6x0A: Update enemy state template -struct G_EnemyHitByPlayer_6x0A { +struct G_UpdateEnemyState_6x0A { G_EnemyIDHeader header; le_uint16_t enemy_index = 0; // [0, 0xB50) le_uint16_t total_damage = 0; // Flags: // 00000400 - should play hit animation - // 00000800 - should die + // 00000800 - is dead typename std::conditional::type flags = 0; } __packed__; -struct G_EnemyHitByPlayer_GC_6x0A : G_EnemyHitByPlayer_6x0A { +struct G_UpdateEnemyState_GC_6x0A : G_UpdateEnemyState_6x0A { } __packed__; -struct G_EnemyHitByPlayer_DC_PC_XB_BB_6x0A : G_EnemyHitByPlayer_6x0A { +struct G_UpdateEnemyState_DC_PC_XB_BB_6x0A : G_UpdateEnemyState_6x0A { } __packed__; -// 6x0B: Box destroyed +// 6x0B: Update object state -struct G_BoxDestroyed_6x0B { +struct G_UpdateObjectState_6x0B { G_ClientIDHeader header; le_uint32_t flags = 0; le_uint32_t object_index = 0; @@ -4626,15 +4626,13 @@ struct G_UseStarAtomizer_6x66 { parray target_client_ids; } __packed__; -// 6x67: Create enemy set +// 6x67: Trigger wave event -struct G_CreateEnemySet_6x67 { +struct G_TriggerWaveEvent_6x67 { G_UnusedHeader header; - // unused1 could be floor; the client checks this against a global but the - // logic is the same in both branches - le_uint32_t unused1 = 0; - le_uint32_t unknown_a1 = 0; - le_uint32_t unused2 = 0; + le_uint32_t floor = 0; + le_uint32_t event_id = 0; // NOT event index + le_uint32_t unused = 0; } __packed__; // 6x68: Create telepipe / cast Ryuker @@ -4687,9 +4685,9 @@ struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { // Decompressed format is a list of these struct G_SyncEnemyState_6x6B_Entry_Decompressed { - le_uint32_t flags = 0; - le_uint16_t last_attacker = 0; - le_uint16_t total_damage = 0; + le_uint32_t flags = 0; // Same as flags in 6x0A + le_uint16_t item_drop_id = 0; + le_uint16_t total_damage = 0; // Same as in 6x0A uint8_t red_buff_type = 0; uint8_t red_buff_level = 0; uint8_t blue_buff_type = 0; @@ -4702,7 +4700,7 @@ struct G_SyncEnemyState_6x6B_Entry_Decompressed { // Decompressed format is a list of these struct G_SyncObjectState_6x6C_Entry_Decompressed { le_uint16_t flags = 0; - le_uint16_t object_index = 0; + le_uint16_t item_drop_id = 0; } __packed__; // 6x6D: Sync item state (used while loading into game) @@ -4746,19 +4744,28 @@ struct G_SyncItemState_6x6D_Decompressed { // FloorItem items[sum(floor_item_count_per_floor)]; } __packed__; -// 6x6E: Sync flag state (used while loading into game) +// 6x6E: Sync set flag state (used while loading into game) // Compressed format is the same as 6x6B. -struct G_SyncFlagState_6x6E_Decompressed { - // The three unknowns here are the sizes (in bytes) of three fields - // immediately following this structure. It is currently unknown what these - // fields represent. The three unknown fields always sum to the size field. - le_uint16_t size = 0; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - // Three variable-length fields follow here. They are in the same order as the - // unknown fields above. +struct G_SyncSetFlagState_6x6E_Decompressed { + le_uint16_t total_size = 0; // == sum of the following 3 fields + le_uint16_t entity_set_flags_size = 0; + le_uint16_t event_set_flags_size = 0; + le_uint16_t unused_size = 0; + // Variable-length fields follow here: + // EntitySetFlags entity_set_flags; // Total size is set_flags_size + // le_uint16_t event_set_flags[event_set_flags_size / 2]; // Same order as in map files (NOT sorted by event_id) + // uint8_t unused[is_v1 ? 0x200 : 0x240]; // Possibly an early implementation of 6x6F; unused even in DC NTE + + struct EntitySetFlags { + le_uint32_t object_set_flags_offset = 0; + le_uint32_t num_object_sets = 0; + le_uint32_t enemy_set_flags_offset = 0; + le_uint32_t num_enemy_sets = 0; + // Variable-length fields follow here: + // le_uint16_t object_set_flags[num_object_sets]; + // le_uint16_t enemy_set_flags[num_enemy_sets]; + } __packed__; } __packed__; // 6x6F: Set quest flags (used while loading into game) @@ -4773,7 +4780,7 @@ struct G_SetQuestFlags_6x6F { // and instead rearranged a bunch of things. struct Telepipe { - /* 00 */ le_uint16_t client_id = 0xFFFF; + /* 00 */ le_uint16_t owner_client_id = 0xFFFF; /* 02 */ le_uint16_t unknown_a1 = 0; /* 04 */ le_uint32_t unknown_a2 = 0; /* 08 */ le_float x = 0.0f; diff --git a/src/Lobby.cc b/src/Lobby.cc index 706af209..2afe6c9e 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -272,7 +272,7 @@ shared_ptr Lobby::load_maps( shared_ptr opt_rand_crypt, shared_ptr quest_dat_contents_decompressed) { auto map = make_shared(version, lobby_id, random_seed, opt_rand_crypt); - map->add_enemies_and_objects_from_quest_data( + map->add_entities_from_quest_data( episode, difficulty, event, diff --git a/src/Map.cc b/src/Map.cc index 4de8ec55..1e1b8d4f 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -655,9 +655,12 @@ string Map::EnemyEntry::str() const { this->unused.load()); } -Map::Enemy::Enemy(uint16_t enemy_id, size_t source_index, uint8_t floor, EnemyType type) +Map::Enemy::Enemy(uint16_t enemy_id, size_t source_index, size_t set_index, uint8_t floor, EnemyType type) : source_index(source_index), + set_index(set_index), enemy_id(enemy_id), + total_damage(0), + game_flags(0), type(type), floor(floor), state_flags(0), @@ -721,6 +724,8 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size .param4 = objects[z].param4, .param5 = objects[z].param5, .param6 = objects[z].param6, + .game_flags = 0, + .set_flags = 0, .item_drop_checked = false, }); } @@ -763,12 +768,15 @@ void Map::add_enemy( uint8_t difficulty, uint8_t event, uint8_t floor, - size_t index, + size_t source_index, const EnemyEntry& e, std::shared_ptr rare_rates) { + size_t set_index = this->enemy_set_flags.size(); + this->enemy_set_flags.emplace_back(0); + auto add = [&](EnemyType type) -> void { uint16_t enemy_id = this->enemies.size(); - this->enemies.emplace_back(enemy_id, index, floor, type); + this->enemies.emplace_back(enemy_id, source_index, set_index, floor, type); }; EnemyType child_type = EnemyType::UNKNOWN; @@ -1186,14 +1194,14 @@ void Map::add_enemy( add(EnemyType::UNKNOWN); this->log.warning( "(Entry %zu, offset %zX in file) Unknown enemy type %04hX", - index, index * sizeof(EnemyEntry), e.base_type.load()); + source_index, source_index * sizeof(EnemyEntry), e.base_type.load()); break; default: add(EnemyType::UNKNOWN); this->log.warning( "(Entry %zu, offset %zX in file) Invalid enemy type %04hX", - index, index * sizeof(EnemyEntry), e.base_type.load()); + source_index, source_index * sizeof(EnemyEntry), e.base_type.load()); break; } @@ -1492,7 +1500,7 @@ vector Map::collect_quest_map_data_sections(const void return ret; } -void Map::add_enemies_and_objects_from_quest_data( +void Map::add_entities_from_quest_data( Episode episode, uint8_t difficulty, uint8_t event, diff --git a/src/Map.hh b/src/Map.hh index 2ea087df..79e5d1a4 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -104,18 +104,18 @@ struct Map { 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; + /* 06 */ le_uint16_t event_type; /* 08 */ le_uint16_t section; /* 0A */ le_uint16_t wave_number; /* 0C */ le_uint32_t delay; - /* 10 */ le_uint32_t clear_events_index; + /* 10 */ le_uint32_t action_stream_offset; /* 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; + /* 06 */ le_uint16_t event_type; /* 08 */ le_uint16_t section; /* 0A */ le_uint16_t wave_number; /* 0C */ le_uint16_t min_delay; @@ -123,7 +123,7 @@ struct Map { /* 10 */ uint8_t min_enemies; /* 11 */ uint8_t max_enemies; /* 12 */ le_uint16_t max_waves; - /* 14 */ le_uint32_t clear_events_index; + /* 14 */ le_uint32_t action_stream_offset; /* 18 */ } __attribute__((packed)); @@ -217,6 +217,10 @@ struct Map { uint32_t param4; uint32_t param5; uint32_t param6; + uint16_t game_flags; + // Technically set_flags shouldn't be part of the Object struct, but all + // object entries always generate exactly one object, so we store it here. + uint16_t set_flags; bool item_drop_checked; std::string str() const; @@ -231,13 +235,16 @@ struct Map { ITEM_DROPPED = 0x10, }; size_t source_index; + size_t set_index; uint16_t enemy_id; + uint16_t total_damage; + uint32_t game_flags; // From 6x0A EnemyType type; uint8_t floor; uint8_t state_flags; uint8_t last_hit_by_client_id; - Enemy(uint16_t enemy_id, size_t source_index, uint8_t floor, EnemyType type); + Enemy(uint16_t enemy_id, size_t source_index, size_t set_index, uint8_t floor, EnemyType type); std::string str() const; } __attribute__((packed)); @@ -300,7 +307,7 @@ struct Map { }; static std::vector collect_quest_map_data_sections(const void* data, size_t size); - void add_enemies_and_objects_from_quest_data( + void add_entities_from_quest_data( Episode episode, uint8_t difficulty, uint8_t event, @@ -319,6 +326,7 @@ struct Map { std::shared_ptr opt_rand_crypt; std::vector objects; std::vector enemies; + std::vector enemy_set_flags; std::vector rare_enemy_indexes; }; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 0d260edc..4cd8abe3 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2283,6 +2283,12 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u return; } + // Note: We always call reconcile_drop_request_with_map, even in client drop + // mode, so that we can correctly mark enemies and objects as having dropped + // their items in persistent games. + G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(data, size); + auto rec = reconcile_drop_request_with_map(c->log, c->channel, cmd, c->version(), l->episode, c->config, l->map, true); + switch (l->drop_mode) { case Lobby::DropMode::CLIENT: forward_subcommand(c, command, flag, data, size); @@ -2297,9 +2303,6 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u throw logic_error("invalid drop mode"); } - G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(data, size); - auto rec = reconcile_drop_request_with_map(c->log, c->channel, cmd, c->version(), l->episode, c->config, l->map, true); - if (rec.should_drop) { auto generate_item = [&]() -> ItemCreator::DropResult { if (rec.is_box) { @@ -2570,8 +2573,8 @@ static void on_gol_dragon_actions(shared_ptr c, uint8_t command, uint8_t } } -static void on_enemy_hit(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { - const auto& cmd = check_size_t(data, size); +static void on_update_enemy_state(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { + const auto& cmd = check_size_t(data, size); if (command_is_private(command)) { return; @@ -2581,22 +2584,21 @@ static void on_enemy_hit(shared_ptr c, uint8_t command, uint8_t, void* d return; } - if (l->base_version == Version::BB_V4) { - 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 (c->lobby_client_id > 3) { + throw logic_error("client ID is above 3"); + } + if (l->map) { if (cmd.enemy_index >= l->map->enemies.size()) { return; } - auto& enemy = l->map->enemies[cmd.enemy_index]; enemy.last_hit_by_client_id = c->lobby_client_id; + enemy.game_flags = is_big_endian(c->version()) ? bswap32(cmd.flags) : cmd.flags.load(); + enemy.total_damage = cmd.total_damage; + l->log.info("E-%hX updated to damage=%hu game_flags=%08" PRIX32, cmd.enemy_index.load(), enemy.total_damage, enemy.game_flags); } - G_EnemyHitByPlayer_GC_6x0A sw_cmd = {{{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id}, cmd.enemy_index, cmd.total_damage, cmd.flags.load()}}; + G_UpdateEnemyState_GC_6x0A sw_cmd = {{{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id}, cmd.enemy_index, cmd.total_damage, cmd.flags.load()}}; bool sender_is_be = is_big_endian(c->version()); for (auto lc : l->clients) { if (lc && (lc != c)) { @@ -2609,6 +2611,27 @@ static void on_enemy_hit(shared_ptr c, uint8_t command, uint8_t, void* d } } +static void on_update_object_state(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { + const auto& cmd = check_size_t(data, size); + + if (command_is_private(command)) { + return; + } + auto l = c->require_lobby(); + if (!l->is_game()) { + return; + } + + if (l->map) { + if (cmd.object_index >= l->map->objects.size()) { + return; + } + l->map->objects[cmd.object_index].game_flags = cmd.flags; + } + + forward_subcommand(c, command, flag, data, size); +} + static void on_charge_attack_bb(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { auto l = c->require_lobby(); if (l->base_version != Version::BB_V4) { @@ -3809,8 +3832,8 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x07 */ {0x07, 0x07, 0x07, on_symbol_chat, SDF::ALWAYS_FORWARD_TO_WATCHERS}, /* 6x08 */ {0x08, 0x08, 0x08, on_invalid}, /* 6x09 */ {0x09, 0x09, 0x09, forward_subcommand_m}, - /* 6x0A */ {0x0A, 0x0A, 0x0A, on_enemy_hit}, - /* 6x0B */ {0x0B, 0x0B, 0x0B, on_forward_check_game}, + /* 6x0A */ {0x0A, 0x0A, 0x0A, on_update_enemy_state}, + /* 6x0B */ {0x0B, 0x0B, 0x0B, on_update_object_state}, /* 6x0C */ {0x0C, 0x0C, 0x0C, on_received_condition}, /* 6x0D */ {0x00, 0x00, 0x0D, on_forward_check_game}, /* 6x0E */ {0x00, 0x00, 0x0E, on_forward_check_game}, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a98d4318..551c2539 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2359,6 +2359,45 @@ void send_ep3_change_music(Channel& ch, uint32_t song) { ch.send(0x60, 0x00, cmd); } +static void send_game_join_sync_command( + shared_ptr c, const void* data, size_t size, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc) { + string compressed_data = bc0_compress(data, size); + + StringWriter w; + if (is_pre_v1(c->version())) { + G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E compressed_header; + compressed_header.header.basic_header.subcommand = (c->version() == Version::DC_NTE) ? dc_nte_sc : dc_11_2000_sc; + compressed_header.header.basic_header.size = 0x00; + compressed_header.header.basic_header.unused = 0x0000; + compressed_header.header.size = (compressed_data.size() + sizeof(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E) + 3) & (~3); + compressed_header.decompressed_size = size; + w.put(compressed_header); + } else { + G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E compressed_header; + compressed_header.header.basic_header.subcommand = sc; + compressed_header.header.basic_header.size = 0x00; + compressed_header.header.basic_header.unused = 0x0000; + compressed_header.header.size = (compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E) + 3) & (~3); + compressed_header.decompressed_size = size; + compressed_header.compressed_size = compressed_data.size(); + w.put(compressed_header); + } + w.write(compressed_data); + while (w.size() & 3) { + w.put_u8(0x00); + } + + if (c->game_join_command_queue) { + c->log.info("Client not ready to receive join commands; adding to queue"); + auto& cmd = c->game_join_command_queue->emplace_back(); + cmd.command = 0x6D; + cmd.flag = c->lobby_client_id; + cmd.data = std::move(w.str()); + } else { + send_command(c, 0x6D, c->lobby_client_id, w.str()); + } +} + void send_game_item_state(shared_ptr c) { auto l = c->require_lobby(); auto s = c->require_server_state(); @@ -2406,42 +2445,47 @@ void send_game_item_state(shared_ptr c) { StringWriter decompressed_w; decompressed_w.put(decompressed_header); decompressed_w.write(floor_items_w.str()); + const auto& data = decompressed_w.str(); + send_game_join_sync_command(c, data.data(), data.size(), 0x5E, 0x65, 0x6D); +} - string compressed_data = bc0_compress(decompressed_w.str()); - - StringWriter w; - if (is_pre_v1(c->version())) { - G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E compressed_header; - compressed_header.header.basic_header.subcommand = (c->version() == Version::DC_NTE) ? 0x5E : 0x65; - compressed_header.header.basic_header.size = 0x00; - compressed_header.header.basic_header.unused = 0x0000; - compressed_header.header.size = (compressed_data.size() + sizeof(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E) + 3) & (~3); - compressed_header.decompressed_size = decompressed_w.size(); - w.put(compressed_header); - } else { - G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E compressed_header; - compressed_header.header.basic_header.subcommand = 0x6D; - compressed_header.header.basic_header.size = 0x00; - compressed_header.header.basic_header.unused = 0x0000; - compressed_header.header.size = (compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E) + 3) & (~3); - compressed_header.decompressed_size = decompressed_w.size(); - compressed_header.compressed_size = compressed_data.size(); - w.put(compressed_header); +void send_game_enemy_state(shared_ptr c) { + auto l = c->require_lobby(); + if (!l->map) { + return; } - w.write(compressed_data); - while (w.size() & 3) { - w.put_u8(0x00); + auto s = c->require_server_state(); + + vector entries; + entries.reserve(l->map->enemies.size()); + for (size_t z = 0; z < l->map->enemies.size(); z++) { + const auto& enemy = l->map->enemies[z]; + auto& entry = entries.emplace_back(); + entry.flags = enemy.game_flags; + entry.item_drop_id = (enemy.state_flags & Map::Enemy::Flag::ITEM_DROPPED) ? 0xFFFF : (0xCA0 + z); + entry.total_damage = enemy.total_damage; } - if (c->game_join_command_queue) { - c->log.info("Client not ready to receive join commands; adding to queue"); - auto& cmd = c->game_join_command_queue->emplace_back(); - cmd.command = 0x6D; - cmd.flag = c->lobby_client_id; - cmd.data = std::move(w.str()); - } else { - send_command(c, 0x6D, c->lobby_client_id, w.str()); + send_game_join_sync_command(c, entries.data(), entries.size() * sizeof(entries[0]), 0x5C, 0x63, 0x6B); +} + +void send_game_object_state(shared_ptr c) { + auto l = c->require_lobby(); + if (!l->map) { + return; } + auto s = c->require_server_state(); + + vector entries; + entries.reserve(l->map->objects.size()); + for (size_t z = 0; z < l->map->objects.size(); z++) { + const auto& obj = l->map->objects[z]; + auto& entry = entries.emplace_back(); + entry.flags = obj.game_flags; + entry.item_drop_id = (obj.item_drop_checked) ? 0xFFFF : (0x100 + z); + } + + send_game_join_sync_command(c, entries.data(), entries.size() * sizeof(entries[0]), 0x5D, 0x64, 0x6C); } void send_game_flag_state(shared_ptr c) { diff --git a/src/SendCommands.hh b/src/SendCommands.hh index b32ac81c..904e4bd4 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -305,6 +305,8 @@ void send_ep3_change_music(Channel& ch, uint32_t song); void send_revive_player(std::shared_ptr c); void send_game_item_state(std::shared_ptr c); +void send_game_enemy_state(std::shared_ptr c); +void send_game_object_state(std::shared_ptr c); void send_game_flag_state(std::shared_ptr c); void send_drop_item_to_channel(std::shared_ptr s, Channel& ch, const ItemData& item, bool from_enemy, uint8_t floor, float x, float z, uint16_t request_id);