diff --git a/README.md b/README.md index 59984059..8575c32a 100644 --- a/README.md +++ b/README.md @@ -431,8 +431,8 @@ Some commands only work on the game server and not on the proxy server. The chat * The rest of the commands in this section are enabled on the game server. (They are always enabled on the proxy server.) * `$quest ` (game server only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present. * `$qcall `: Call a quest function on your client. - * `$qcheck ` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled. - * `$qset ` or `$qclear `: Set or clear a quest flag for everyone in the game. + * `$qcheck ` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only). + * `$qset ` or `$qclear `: Set or clear a quest flag for everyone in the game. If you're in the lobby and on BB, set or clear the saved value of a quest flag in your character file. * `$qgread ` (game server only): Get the value of a quest counter ("global flag"). This command can be used without debug mode enabled. * `$qgwrite ` (game server only): Set the value of a quest counter ("global flag") for yourself. * `$qsync `: Set a quest register's value for yourself only. `` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `` is parsed as a floating-point value instead of as an integer. @@ -468,7 +468,7 @@ Some commands only work on the game server and not on the proxy server. The chat * `$minlevel `: Sets the minimum level for players to join the current game. * `$password `: Sets the game's join password. To unlock the game, run `$password` with nothing after it. * `$dropmode [mode]`: Changes the way item drops behave in the current game. `mode` can be `none`, `client`, `shared`, `private`, or `duplicate`. If `mode` is not given, tells you the current drop mode without changing it. See the "Item tables and drop modes" section for more information. - * `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies and objects on the map will be reset when the last player leaves, but dropped items will not be deleted. If the game is empty for too long (15 minutes by default), it is then deleted. + * `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies on the map will be reset when the last player leaves, but dropped items will not be deleted. If the game is empty for too long (15 minutes by default), it is then deleted. * Episode 3 commands (game server only) * `$spec`: Toggles the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they will be sent back to the lobby. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 1e5a76d9..7832353f 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -311,10 +311,22 @@ static void server_command_qcheck(shared_ptr c, const std::string& args) auto l = c->require_lobby(); uint16_t flag_num = stoul(args, nullptr, 0); - send_text_message_printf(c, "$C7Quest flag 0x%hX (%hu)\nis %s on %s", - flag_num, flag_num, - c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set", - name_for_difficulty(l->difficulty)); + if (l->is_game()) { + if (!l->quest_flags_known || l->quest_flags_known->get(l->difficulty, flag_num)) { + send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis %s on %s", + flag_num, flag_num, + c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set", + name_for_difficulty(l->difficulty)); + } else { + send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis unknown on %s", + flag_num, flag_num, name_for_difficulty(l->difficulty)); + } + } else if (c->version() == Version::BB_V4) { + send_text_message_printf(c, "$C7Player: flag 0x%hX (%hu)\nis %s on %s", + flag_num, flag_num, + c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set", + name_for_difficulty(l->difficulty)); + } } static void server_command_qset_qclear(shared_ptr c, const std::string& args, bool should_set) { @@ -330,10 +342,22 @@ static void server_command_qset_qclear(shared_ptr c, const std::string& uint16_t flag_num = stoul(args, nullptr, 0); - if (should_set) { - c->character()->quest_flags.set(l->difficulty, flag_num); - } else { - c->character()->quest_flags.clear(l->difficulty, flag_num); + if (l->is_game()) { + if (l->quest_flags_known) { + l->quest_flags_known->set(l->difficulty, flag_num); + } + if (should_set) { + l->quest_flag_values->set(l->difficulty, flag_num); + } else { + l->quest_flag_values->clear(l->difficulty, flag_num); + } + } else if (c->version() == Version::BB_V4) { + auto p = c->character(); + if (should_set) { + p->quest_flags.set(l->difficulty, flag_num); + } else { + p->quest_flags.clear(l->difficulty, flag_num); + } } if (is_v1_or_v2(c->version())) { diff --git a/src/Client.cc b/src/Client.cc index 205912f7..636db5e9 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -361,9 +361,10 @@ bool Client::evaluate_quest_availability_expression( if (!expr) { return true; } + auto l = this->lobby.lock(); auto p = this->character(); QuestAvailabilityExpression::Env env = { - .flags = &p->quest_flags.data.at(difficulty), + .flags = (l && !l->quest_flags_known) ? &l->quest_flag_values->data.at(difficulty) : &p->quest_flags.data.at(difficulty), .challenge_records = &p->challenge_records, .team = this->team(), .num_players = num_players, diff --git a/src/Client.hh b/src/Client.hh index ff5142e2..6be8deb5 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -34,60 +34,62 @@ public: // TODO: It'd be nice to use a pattern here (e.g. all server-side flags are // in the high bits) but that would require re-recording or manually // rewriting all the tests - CLIENT_SIDE_MASK = 0xFFFFFFFFFC0FFFFB, + CLIENT_SIDE_MASK = 0xFF3CFFFF7C0FFFFB, // Version-related flags - CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002, - LICENSE_WAS_CREATED = 0x0000000000000004, // Server-side only - NO_D6_AFTER_LOBBY = 0x0000000000000100, - NO_D6 = 0x0000000000000200, - FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400, + CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002, + LICENSE_WAS_CREATED = 0x0000000000000004, // Server-side only + NO_D6_AFTER_LOBBY = 0x0000000000000100, + NO_D6 = 0x0000000000000200, + FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400, // Flags describing the behavior for send_function_call - NO_SEND_FUNCTION_CALL = 0x0000000000001000, - ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000, - SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000, - SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000, - USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000, + NO_SEND_FUNCTION_CALL = 0x0000000000001000, + ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000, + SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000, + SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000, + USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000, // State flags - LOADING = 0x0000000000100000, // Server-side only - LOADING_QUEST = 0x0000000000200000, // Server-side only - LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only - LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only - IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only - AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only - SAVE_ENABLED = 0x0000000004000000, - HAS_EP3_CARD_DEFS = 0x0000000008000000, - HAS_EP3_MEDIA_UPDATES = 0x0000000010000000, - USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000, - HAS_GUILD_CARD_NUMBER = 0x0000000040000000, - AT_BANK_COUNTER = 0x0000000080000000, // Server-side only - SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only - SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only - SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000, - SWITCH_ASSIST_ENABLED = 0x0000000100000000, + LOADING = 0x0000000000100000, // Server-side only + LOADING_QUEST = 0x0000000000200000, // Server-side only + LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only + LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only + IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only + AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only + SAVE_ENABLED = 0x0000000004000000, + HAS_EP3_CARD_DEFS = 0x0000000008000000, + HAS_EP3_MEDIA_UPDATES = 0x0000000010000000, + USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000, + HAS_GUILD_CARD_NUMBER = 0x0000000040000000, + AT_BANK_COUNTER = 0x0000000080000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only + SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000, + SWITCH_ASSIST_ENABLED = 0x0000000100000000, // Cheat mode and option flags - INFINITE_HP_ENABLED = 0x0000000200000000, - INFINITE_TP_ENABLED = 0x0000000400000000, - DEBUG_ENABLED = 0x0000000800000000, - ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000, - ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000, + INFINITE_HP_ENABLED = 0x0000000200000000, + INFINITE_TP_ENABLED = 0x0000000400000000, + DEBUG_ENABLED = 0x0000000800000000, + ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000, + ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000, // Proxy option flags - PROXY_SAVE_FILES = 0x0000001000000000, - PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000, - PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000, - PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000, - PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000, - PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000, - PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000, - PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000, - PROXY_RED_NAME_ENABLED = 0x0000200000000000, - PROXY_BLANK_NAME_ENABLED = 0x0000400000000000, - PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000, - PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000, + PROXY_SAVE_FILES = 0x0000001000000000, + PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000, + PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000, + PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000, + PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000, + PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000, + PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000, + PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000, + PROXY_RED_NAME_ENABLED = 0x0000200000000000, + PROXY_BLANK_NAME_ENABLED = 0x0000400000000000, + PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000, + PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000, // clang-format on }; enum class ItemDropNotificationMode { diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 47e7bce5..656b6998 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -4629,9 +4629,9 @@ struct G_UseStarAtomizer_6x66 { parray target_client_ids; } __packed__; -// 6x67: Trigger wave event +// 6x67: Trigger set event -struct G_TriggerWaveEvent_6x67 { +struct G_TriggerSetEvent_6x67 { G_UnusedHeader header; le_uint32_t floor = 0; le_uint32_t event_id = 0; // NOT event index @@ -4773,7 +4773,12 @@ struct G_SyncSetFlagState_6x6E_Decompressed { // 6x6F: Set quest flags (used while loading into game) -struct G_SetQuestFlags_6x6F { +struct G_SetQuestFlagsV1_6x6F { + G_UnusedHeader header; + QuestFlagsV1 quest_flags; +} __packed__; + +struct G_SetQuestFlagsV2V3V4_6x6F { G_UnusedHeader header; QuestFlags quest_flags; } __packed__; @@ -4988,12 +4993,14 @@ struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 { le_uint16_t unused = 0; } __packed__; -// 6x76: Enemy killed +// 6x76: Set entity flags +// This command can only be used to set flags, since the game performs a bitwise +// OR operation instead of a simple assignment. -struct G_EnemyKilled_6x76 { - G_EnemyIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; // Flags of some sort +struct G_SetEntityFlags_6x76 { + G_EnemyIDHeader header; // 1000-3FFF = enemy, 4000-FFFF = object + le_uint16_t floor = 0; + le_uint16_t flags = 0; } __packed__; // 6x77: Sync quest data diff --git a/src/Lobby.cc b/src/Lobby.cc index b4c79e82..b07bc8c4 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -325,11 +325,13 @@ shared_ptr Lobby::load_maps( shared_ptr opt_rand_crypt, const parray& variations, const PrefixedLogger* log) { - auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, true); - auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, false); + auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::ENEMIES); + auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::OBJECTS); + auto event_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::EVENTS); return Lobby::load_maps( enemy_filenames, object_filenames, + event_filenames, version, episode, mode, @@ -346,6 +348,7 @@ shared_ptr Lobby::load_maps( shared_ptr Lobby::load_maps( const vector& enemy_filenames, const vector& object_filenames, + const vector& event_filenames, Version version, Episode episode, GameMode mode, @@ -402,6 +405,21 @@ shared_ptr Lobby::load_maps( } else if (log) { log->info("No objects to load for floor %02zX", floor); } + + const auto& floor_event_filename = event_filenames.at(floor); + if (!floor_event_filename.empty()) { + auto map_data = get_file_data(version, floor_event_filename); + if (map_data) { + map->add_events_from_map_data(floor, map_data->data(), map_data->size()); + if (log) { + log->info("Loaded events map %s for floor %02zX", floor_event_filename.c_str(), floor); + } + } else if (log) { + log->info("Events map %s for floor %02zX cannot be used; skipping", floor_event_filename.c_str(), floor); + } + } else if (log) { + log->info("No events to load for floor %02zX", floor); + } } return map; @@ -464,6 +482,11 @@ void Lobby::load_maps() { string e_str = this->map->enemies[z].str(); this->log.info("(E-%zX) %s", z, e_str.c_str()); } + this->log.info("Generated events list (%zu entries):", this->map->events.size()); + for (size_t z = 0; z < this->map->events.size(); z++) { + string e_str = this->map->events[z].str(); + this->log.info("%s", e_str.c_str()); + } this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)", this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size()); } diff --git a/src/Lobby.hh b/src/Lobby.hh index 0142e4b3..0e557117 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -92,15 +92,15 @@ struct Lobby : public std::enable_shared_from_this { uint32_t min_level; uint32_t max_level; - // Item state + // Game state std::array next_item_id_for_client; uint32_t next_game_item_id; std::vector floor_item_managers; - - // Map state std::shared_ptr rare_enemy_rates; std::shared_ptr map; parray variations; + std::unique_ptr quest_flags_known; // If null, ALL quest flags are known + std::unique_ptr quest_flag_values; // Game config Version base_version; @@ -226,6 +226,7 @@ struct Lobby : public std::enable_shared_from_this { static std::shared_ptr load_maps( const std::vector& enemy_filenames, const std::vector& object_filenames, + const std::vector& event_filenames, Version version, Episode episode, GameMode mode, diff --git a/src/Main.cc b/src/Main.cc index 0a3c9554..4ffc493b 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1127,187 +1127,6 @@ Action a_disassemble_set_data_table( string str = sdt.str(); write_output_data(args, str.data(), str.size(), "txt"); }); -Action a_check_set_data_table( - "check-set-data-tables", nullptr, +[](Arguments&) { - auto s = make_shared(); - s->load_patch_indexes(false); - s->load_set_data_tables(false); - static_game_data_log.min_level = LogLevel::DISABLED; - - auto get_file_data = [&](Version version, const string& filename) -> shared_ptr { - try { - return s->load_map_file(version, filename); - } catch (const cannot_open_file&) { - return nullptr; - } - }; - - auto check_filenames = [&](Version version, const string& sdt_filename, const vector& ns_filenames) -> string { - for (size_t z = 0; z < ns_filenames.size(); z++) { - const auto& ns_filename = ns_filenames[z]; - auto data = get_file_data(version, ns_filename); - if (data) { - if (sdt_filename != ns_filename) { - string ns_filenames_str = join(ns_filenames, ", "); - return string_printf("SDT => %s, NS => [%s]", sdt_filename.c_str(), ns_filenames_str.c_str()); - } - return "OK"; - } - if (!data && (sdt_filename == ns_filename)) { - string ns_filenames_str = join(ns_filenames, ", "); - return string_printf("SDT => %s (missing)", sdt_filename.c_str()); - } - } - if (ns_filenames.empty() && sdt_filename.empty()) { - return "OK (no files)"; - } else if (ns_filenames.empty()) { - auto data = get_file_data(version, sdt_filename); - if (data) { - return string_printf("NS blank, SDT => %s", sdt_filename.c_str()); - } else { - return string_printf("NS blank, SDT => %s (missing)", sdt_filename.c_str()); - } - } else if (sdt_filename.empty()) { - string ns_filenames_str = join(ns_filenames, ", "); - return string_printf("SDT blank, NS => [%s] (all missing)", ns_filenames_str.c_str()); - } else { - string ns_filenames_str = join(ns_filenames, ", "); - return string_printf("SDT => %s (missing), NS => [%s] (all missing)", sdt_filename.c_str(), ns_filenames_str.c_str()); - } - }; - - size_t num_checks = 0; - size_t num_errors = 0; - auto check_table = [&](Version version) { - vector episodes({Episode::EP1}); - if (!is_v1_or_v2(version) || (version == Version::GC_NTE)) { - episodes.emplace_back(Episode::EP2); - if (is_v4(version)) { - episodes.emplace_back(Episode::EP4); - } - } - - vector modes({GameMode::NORMAL}); - if (!is_v1(version)) { - modes.emplace_back(GameMode::BATTLE); - modes.emplace_back(GameMode::CHALLENGE); - } - if (is_v4(version)) { - modes.emplace_back(GameMode::SOLO); - } - - uint8_t max_difficulty = is_v1(version) ? 2 : 3; - - for (Episode episode : episodes) { - for (GameMode mode : modes) { - for (uint8_t difficulty = 0; difficulty <= max_difficulty; difficulty++) { - auto sdt = s->set_data_table(version, episode, mode, difficulty); - auto ns_var_maxes = variation_maxes_deprecated(version, episode, (mode == GameMode::SOLO)); - size_t num_floors; - if (episode == Episode::EP4) { - num_floors = 0x0B; - } else if (episode == Episode::EP2) { - num_floors = 0x10; - } else { - num_floors = 0x0F; - } - for (size_t floor = 0; floor < num_floors; floor++) { - auto sdt_var_avail = sdt->num_available_variations_for_floor(episode, floor); - auto sdt_var_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor); - size_t sdt_var1_max_avail = sdt_var_avail.first - 1; - size_t sdt_var2_max_avail = sdt_var_avail.second - 1; - size_t sdt_var1_max = sdt_var_maxes.first - 1; - size_t sdt_var2_max = sdt_var_maxes.second - 1; - size_t ns_var1_max = ns_var_maxes[floor * 2]; - size_t ns_var2_max = ns_var_maxes[floor * 2 + 1]; - num_checks += 4; - if (sdt_var1_max > sdt_var1_max_avail) { - fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR1:[SDT:%02zX SDTA:%02zX]\n", - name_for_enum(version), - name_for_episode(episode), - name_for_mode(mode), - name_for_difficulty(difficulty), - floor, - sdt_var1_max, - sdt_var1_max_avail); - num_errors++; - } - if (sdt_var2_max > sdt_var2_max_avail) { - fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR2:[SDT:%02zX SDTA:%02zX]\n", - name_for_enum(version), - name_for_episode(episode), - name_for_mode(mode), - name_for_difficulty(difficulty), - floor, - sdt_var2_max, - sdt_var2_max_avail); - num_errors++; - } - if (sdt_var1_max < ns_var1_max) { - fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR1:[SDT:%02zX NS:%02zX]\n", - name_for_enum(version), - name_for_episode(episode), - name_for_mode(mode), - name_for_difficulty(difficulty), - floor, - sdt_var1_max, - ns_var1_max); - num_errors++; - } - if (sdt_var2_max < ns_var2_max) { - fprintf(stdout, "## %-8s %-10s %-10s %-10s %02zX VAR2:[SDT:%02zX NS:%02zX]\n", - name_for_enum(version), - name_for_episode(episode), - name_for_mode(mode), - name_for_difficulty(difficulty), - floor, - sdt_var2_max, - ns_var2_max); - num_errors++; - } - for (size_t var1 = 0; var1 <= ns_var1_max; var1++) { - for (size_t var2 = 0; var2 <= ns_var2_max; var2++) { - auto sdt_enemy_filename = sdt->map_filename_for_variation(floor, var1, var2, episode, mode, true); - auto sdt_object_filename = sdt->map_filename_for_variation(floor, var1, var2, episode, mode, false); - auto ns_enemy_filenames = map_filenames_for_variation_deprecated(floor, var1, var2, version, episode, mode, true); - auto ns_object_filenames = map_filenames_for_variation_deprecated(floor, var1, var2, version, episode, mode, false); - string enemies_error = check_filenames(version, sdt_enemy_filename, ns_enemy_filenames); - string objects_error = check_filenames(version, sdt_object_filename, ns_object_filenames); - num_checks += 2; - num_errors += (enemies_error != "OK") + (objects_error != "OK"); - fprintf(stdout, "%s %-8s %-10s %-10s %-10s %02zX %02zX %02zX E:[%s] O:[%s] E:%-30s O:%-30s\n", - ((enemies_error != "OK") || (objects_error != "OK")) ? "##" : " ", - name_for_enum(version), - name_for_episode(episode), - name_for_mode(mode), - name_for_difficulty(difficulty), - floor, - var1, - var2, - enemies_error.c_str(), - objects_error.c_str(), - sdt_enemy_filename.c_str(), - sdt_object_filename.c_str()); - } - } - } - } - } - } - }; - - check_table(Version::DC_NTE); - check_table(Version::DC_V1_11_2000_PROTOTYPE); - check_table(Version::DC_V1); - check_table(Version::DC_V2); - check_table(Version::PC_NTE); - check_table(Version::PC_V2); - check_table(Version::GC_NTE); - check_table(Version::GC_V3); - check_table(Version::XB_V3); - check_table(Version::BB_V4); - fprintf(stdout, "%zu/%zu errors\n", num_errors, num_checks); - }); Action a_assemble_quest_script( "assemble-quest-script", "\ @@ -2189,6 +2008,122 @@ Action a_find_rare_enemy_seeds( parallel_range(thread_fn, 0, 0x100000000, num_threads, nullptr); }); +Action a_load_maps_test( + "load-maps-test", nullptr, +[](Arguments&) { + using SDT = SetDataTable; + auto s = make_shared("system/config.json"); + s->load_config_early(); + s->clear_map_file_caches(); + s->load_patch_indexes(false); + s->load_set_data_tables(false); + s->load_quest_index(false); + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + Version v = static_cast(v_s); + if (is_ep3(v)) { + continue; + } + const array episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; + for (Episode episode : episodes) { + if (episode == Episode::EP4 && !is_v4(v)) { + continue; + } + if (episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) { + continue; + } + const array modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO}; + for (GameMode mode : modes) { + if ((mode == GameMode::BATTLE || mode == GameMode::CHALLENGE) && is_v1(v)) { + continue; + } + if (mode == GameMode::SOLO && !is_v4(v)) { + continue; + } + for (uint8_t difficulty = 0; difficulty < 4; difficulty++) { + if (difficulty == 3 && is_v1(v)) { + continue; + } + auto sdt = s->set_data_table(v, episode, mode, difficulty); + for (uint8_t floor = 0; floor < 0x12; floor++) { + auto variation_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor); + for (size_t var1 = 0; var1 < variation_maxes.first; var1++) { + for (size_t var2 = 0; var2 < variation_maxes.second; var2++) { + auto enemies_filename = sdt->map_filename_for_variation( + floor, var1, var2, episode, mode, SDT::FilenameType::ENEMIES); + auto objects_filename = sdt->map_filename_for_variation( + floor, var1, var2, episode, mode, SDT::FilenameType::OBJECTS); + auto events_filename = sdt->map_filename_for_variation( + floor, var1, var2, episode, mode, SDT::FilenameType::EVENTS); + + fprintf(stderr, "... %s %s %s %s %02hhX %zX %zX", + name_for_enum(v), name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), floor, var1, var2); + auto map = make_shared(v, 0, 0, nullptr); + if (!enemies_filename.empty()) { + fprintf(stderr, " [%s => ", enemies_filename.c_str()); + auto map_data = s->load_map_file(v, enemies_filename); + if (map_data) { + map->add_enemies_from_map_data( + episode, difficulty, 0, 0, map_data->data(), map_data->size(), Map::DEFAULT_RARE_ENEMIES); + fprintf(stderr, "%zu enemies, %zu sets]", map->enemies.size(), map->enemy_set_flags.size()); + } else { + fprintf(stderr, "__MISSING__]"); + } + } + if (!objects_filename.empty()) { + fprintf(stderr, " [%s => ", objects_filename.c_str()); + auto map_data = s->load_map_file(v, objects_filename); + if (map_data) { + map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); + fprintf(stderr, "%zu objects]", map->objects.size()); + } else { + fprintf(stderr, "__MISSING__]"); + } + } + if (!events_filename.empty()) { + fprintf(stderr, " [%s => ", events_filename.c_str()); + auto map_data = s->load_map_file(v, events_filename); + if (map_data) { + map->add_events_from_map_data(floor, map_data->data(), map_data->size()); + fprintf(stderr, "%zu events, %zu action bytes]", map->events.size(), map->event_action_stream.size()); + } else { + fprintf(stderr, "__MISSING__]"); + } + } + fputc('\n', stderr); + } + } + } + } + } + } + } + + for (const auto& q_it : s->default_quest_index->quests_by_number) { + for (const auto& vq_it : q_it.second->versions) { + auto vq = vq_it.second; + shared_ptr map = Lobby::load_maps( + vq->version, + vq->episode, + 0, + 0, + 0, + Map::DEFAULT_RARE_ENEMIES, + 0, + nullptr, + vq->dat_contents_decompressed); + fprintf(stderr, "... %" PRIu32 " (%s) %s %s %s => %zu enemies (%zu sets), %zu objects, %zu events\n", + vq->quest_number, + vq->name.c_str(), + name_for_episode(vq->episode), + name_for_enum(vq->version), + name_for_language_code(vq->language), + map->enemies.size(), + map->enemy_set_flags.size(), + map->objects.size(), + map->events.size()); + } + } + }); + Action a_parse_object_graph( "parse-object-graph", nullptr, +[](Arguments& args) { uint32_t root_object_address = args.get("root", Arguments::IntFormat::HEX); diff --git a/src/Map.cc b/src/Map.cc index b6210e5c..6e4a5e24 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -675,6 +675,15 @@ string Map::Enemy::str() const { this->state_flags); } +string Map::Event::str() const { + return string_printf("[Map::Event W-%02hhX-%" PRIX32 " flags=%04hX floor=%02hhX action_stream_offset=%" PRIX32 "]", + this->floor, + this->event_id, + this->flags, + this->floor, + this->action_stream_offset); +} + string Map::Object::str() const { return string_printf("[Map::Object source %zX %04hX(%s) @%04hX p1=%g p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]", this->source_index, @@ -1189,9 +1198,6 @@ void Map::add_enemy( case 0x00C7: // TBoss3VoloptHiraisin case 0x0118: add(EnemyType::UNKNOWN); - this->log.warning( - "(Entry %zu, offset %zX in file) Unknown enemy type %04hX", - source_index, source_index * sizeof(EnemyEntry), e.base_type.load()); break; default: @@ -1320,6 +1326,10 @@ void Map::add_random_enemies_from_map_data( } wave_events_segment_r.go(wave_events_header.entries_offset); + size_t action_stream_base_offset = this->event_action_stream.size(); + this->event_action_stream += wave_events_segment_r.pread( + wave_events_header.action_stream_offset, wave_events_segment_r.size() - wave_events_header.action_stream_offset); + const auto& locations_header = locations_segment_r.get(); const auto& definitions_header = definitions_segment_r.get(); auto definitions_r = definitions_segment_r.sub( @@ -1336,6 +1346,7 @@ void Map::add_random_enemies_from_map_data( size_t remaining_waves = random_state->rand_int_biased(1, entry.max_waves); // Trace: at 0080E125 EAX is wave count + le_uint32_t wave_next_event_id = entry.event_id; uint32_t wave_number = entry.wave_number; while (remaining_waves) { remaining_waves--; @@ -1422,17 +1433,60 @@ void Map::add_random_enemies_from_map_data( } } if (remaining_waves) { - // We don't generate the event stream here, but the client does, and in - // doing so, it uses one value from random to determine the delay - // parameter of the event. To keep our state in sync with what the - // client would do, we skip a random value here. - random_state->random.next(); + /* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay); + this->add_event(wave_next_event_id, entry.flags, floor, this->event_action_stream.size()); + this->event_action_stream.push_back(0x0C); + wave_next_event_id = entry.event_id + wave_number + 10000; + this->event_action_stream.append(reinterpret_cast(&wave_next_event_id), sizeof(wave_next_event_id)); + this->event_action_stream.push_back(0x01); wave_number++; } } - // For the same reason as above, we need to skip another random value here. - random_state->random.next(); + /* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay); + this->add_event(wave_next_event_id, entry.flags, floor, action_stream_base_offset + entry.action_stream_offset); + wave_number++; + } +} + +void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset) { + size_t index = this->events.size(); + auto& ev = this->events.emplace_back(); + ev.event_id = event_id; + ev.flags = flags; + ev.floor = floor; + ev.action_stream_offset = action_stream_offset; + uint64_t k = (static_cast(floor) << 32) | event_id; + if (!this->floor_and_event_id_to_index.emplace(k, index).second) { + this->log.warning("Duplicate event ID: W-%02hhX-%" PRIX32, floor, event_id); + } +} + +Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) { + uint64_t k = (static_cast(floor) << 32) | event_id; + return this->events.at(this->floor_and_event_id_to_index.at(k)); +} + +const Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) const { + uint64_t k = (static_cast(floor) << 32) | event_id; + return this->events.at(this->floor_and_event_id_to_index.at(k)); +} + +void Map::add_events_from_map_data(uint8_t floor, const void* data, size_t size) { + StringReader r(data, size); + const auto& header = r.get(); + if (header.format != 0) { + throw runtime_error("events section format is not zero"); + } + + size_t action_stream_base_offset = this->event_action_stream.size(); + this->event_action_stream += r.pread(header.action_stream_offset, r.size() - header.action_stream_offset); + + this->events.reserve(this->events.size() + header.entry_count); + auto events_r = r.sub(header.entries_offset, sizeof(Event1Entry) * header.entry_count); + while (!events_r.eof()) { + const auto& entry = events_r.get(); + this->add_event(entry.event_id, entry.flags, floor, entry.action_stream_offset + action_stream_base_offset); } } @@ -1519,23 +1573,10 @@ void Map::add_entities_from_quest_data( this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size); } - if (floor_sections.enemies != 0xFFFFFFFF) { - const auto& header = r.pget(floor_sections.enemies); - if (header.data_size % sizeof(EnemyEntry)) { - throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size"); - } - this->add_enemies_from_map_data( - episode, - difficulty, - event, - floor, - r.pgetv(floor_sections.enemies + sizeof(header), header.data_size), - header.data_size, - rare_rates); - - } else if ((floor_sections.wave_events != 0xFFFFFFFF) && + if ((floor_sections.wave_events != 0xFFFFFFFF) && (floor_sections.random_enemy_locations != 0xFFFFFFFF) && (floor_sections.random_enemy_definitions != 0xFFFFFFFF)) { + // Challenge Mode random enemy waves const auto& wave_events_header = r.pget(floor_sections.wave_events); const auto& random_enemy_locations_header = r.pget(floor_sections.random_enemy_locations); const auto& random_enemy_definitions_header = r.pget(floor_sections.random_enemy_definitions); @@ -1552,6 +1593,29 @@ void Map::add_entities_from_quest_data( r.sub(floor_sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size), random_state, rare_rates); + + } else { + // Non-Challenge (standard) enemies + if (floor_sections.enemies != 0xFFFFFFFF) { + const auto& header = r.pget(floor_sections.enemies); + if (header.data_size % sizeof(EnemyEntry)) { + throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size"); + } + this->add_enemies_from_map_data( + episode, + difficulty, + event, + floor, + r.pgetv(floor_sections.enemies + sizeof(header), header.data_size), + header.data_size, + rare_rates); + } + + if (floor_sections.wave_events != 0xFFFFFFFF) { + const auto& wave_events_header = r.pget(floor_sections.wave_events); + const void* data = r.pgetv(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size); + this->add_events_from_map_data(floor, data, wave_events_header.data_size); + } } } } @@ -1657,14 +1721,14 @@ parray SetDataTableBase::generate_variations( } vector SetDataTableBase::map_filenames_for_variations( - const parray& variations, Episode episode, GameMode mode, bool is_enemies) const { + const parray& variations, Episode episode, GameMode mode, FilenameType type) const { vector ret; for (uint8_t floor = 0; floor < 0x10; floor++) { ret.emplace_back(this->map_filename_for_variation( - floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, is_enemies)); + floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, type)); } for (uint8_t floor = 0x10; floor < 0x12; floor++) { - ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, is_enemies)); + ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, type)); } return ret; } @@ -1738,8 +1802,8 @@ void SetDataTable::load_table_t(const string& data) { while (!var2_r.eof()) { auto& entry = var2_v.emplace_back(); entry.object_list_basename = r.pget_cstr(var2_r.get()); - entry.enemy_list_basename = r.pget_cstr(var2_r.get()); - entry.event_list_basename = r.pget_cstr(var2_r.get()); + entry.enemy_and_event_list_basename = r.pget_cstr(var2_r.get()); + entry.area_setup_filename = r.pget_cstr(var2_r.get()); } } } @@ -1750,7 +1814,10 @@ pair SetDataTable::num_available_variations_for_floor(Episod if (area == 0xFF) { return make_pair(1, 1); } else { - const auto& e = this->entries.at(area); + if (area >= this->entries.size()) { + return make_pair(1, 1); + } + const auto& e = this->entries[area]; return make_pair(e.size(), e.at(0).size()); } } @@ -1789,7 +1856,7 @@ pair SetDataTable::num_free_roam_variations_for_floor(Episod } string SetDataTable::map_filename_for_variation( - uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const { + uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const { uint8_t area = this->default_area_for_floor(episode, floor); if (area == 0xFF) { return ""; @@ -1800,22 +1867,35 @@ string SetDataTable::map_filename_for_variation( } const auto& entry = this->entries.at(area).at(var1).at(var2); - string filename = is_enemies ? entry.enemy_list_basename : entry.object_list_basename; - filename += (is_enemies ? "e" : "o"); + string filename; + switch (type) { + case FilenameType::OBJECTS: + filename = entry.object_list_basename + "o"; + break; + case FilenameType::ENEMIES: + filename = entry.enemy_and_event_list_basename + "e"; + break; + case FilenameType::EVENTS: + filename = entry.enemy_and_event_list_basename; + break; + default: + throw logic_error("invalid map filename type"); + } + bool is_events = (type == FilenameType::EVENTS); switch ((floor != 0) ? GameMode::NORMAL : mode) { case GameMode::NORMAL: - filename += ".dat"; + filename += is_events ? ".evt" : ".dat"; break; case GameMode::SOLO: - filename += "_s.dat"; + filename += is_events ? "_s.evt" : "_s.dat"; break; case GameMode::CHALLENGE: - filename += "_c1.dat"; + filename += is_events ? "_c1.evt" : "_c1.dat"; break; case GameMode::BATTLE: - filename += "_d.dat"; + filename += is_events ? "_d.evt" : "_d.dat"; break; default: throw logic_error("invalid game mode"); @@ -1826,14 +1906,15 @@ string SetDataTable::map_filename_for_variation( string SetDataTable::str() const { vector lines; - lines.emplace_back(string_printf("FL/V1/V2 => ----------------------OBJECT -----------------------ENEMY -----------------------EVENT\n")); + lines.emplace_back(string_printf("FL/V1/V2 => ----------------------OBJECT -----------------ENEMY+EVENT -----------------------SETUP\n")); for (size_t a = 0; a < this->entries.size(); a++) { const auto& v1_v = this->entries[a]; for (size_t v1 = 0; v1 < v1_v.size(); v1++) { const auto& v2_v = v1_v[v1]; for (size_t v2 = 0; v2 < v2_v.size(); v2++) { const auto& e = v2_v[v2]; - lines.emplace_back(string_printf("%02zX/%02zX/%02zX => %28s %28s %28s\n", a, v1, v2, e.object_list_basename.c_str(), e.enemy_list_basename.c_str(), e.event_list_basename.c_str())); + lines.emplace_back(string_printf("%02zX/%02zX/%02zX => %28s %28s %28s\n", + a, v1, v2, e.object_list_basename.c_str(), e.enemy_and_event_list_basename.c_str(), e.area_setup_filename.c_str())); } } } @@ -1854,7 +1935,7 @@ struct AreaMapFileInfo { variation2_values(variation2_values) {} }; -const array>, 0x10> SetDataTableDCNTE::NAMES = {{ +const array>, 0x12> SetDataTableDCNTE::NAMES = {{ /* 00 */ {{"map_city00_00"}}, /* 01 */ {{"map_forest01_00", "map_forest01_01"}}, /* 02 */ {{"map_forest02_00", "map_forest02_03"}}, @@ -1871,12 +1952,15 @@ const array>, 0x10> SetDataTableDCNTE::NAMES = {{ /* 0D */ {{"map_boss03"}}, /* 0E */ {{"map_boss04"}}, /* 0F */ {{"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}}, + /* 10 */ {}, + /* 11 */ {}, }}; SetDataTableDCNTE::SetDataTableDCNTE() : SetDataTableBase(Version::DC_NTE) {} pair SetDataTableDCNTE::num_available_variations_for_floor(Episode, uint8_t floor) const { - return make_pair(this->NAMES[floor].size(), this->NAMES[floor][0].size()); + const auto& floor_names = this->NAMES.at(floor); + return make_pair(floor_names.size(), floor_names.empty() ? 0 : this->NAMES.at(floor)[0].size()); } pair SetDataTableDCNTE::num_free_roam_variations_for_floor(Episode episode, bool, uint8_t floor) const { @@ -1884,14 +1968,29 @@ pair SetDataTableDCNTE::num_free_roam_variations_for_floor(E } string SetDataTableDCNTE::map_filename_for_variation( - uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, bool is_enemies) const { - if (floor >= this->NAMES.size()) { + uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, FilenameType type) const { + try { + string basename = this->NAMES.at(floor).at(var1).at(var2); + switch (type) { + case FilenameType::ENEMIES: + basename += "e.dat"; + break; + case FilenameType::OBJECTS: + basename += "o.dat"; + break; + case FilenameType::EVENTS: + basename += ".evt"; + break; + default: + throw logic_error("invalid map filename type"); + } + return basename; + } catch (const out_of_range&) { return ""; } - return this->NAMES.at(floor).at(var1).at(var2) + (is_enemies ? "e.dat" : "o.dat"); } -const array>, 0x10> SetDataTableDC112000::NAMES = {{ +const array>, 0x12> SetDataTableDC112000::NAMES = {{ /* 00 */ {{"map_city00_00"}}, /* 01 */ {{"map_forest01_00", "map_forest01_01", "map_forest01_02", "map_forest01_03", "map_forest01_04"}}, /* 02 */ {{"map_forest02_00", "map_forest02_01", "map_forest02_02", "map_forest02_03", "map_forest02_04"}}, @@ -1908,12 +2007,15 @@ const array>, 0x10> SetDataTableDC112000::NAMES = {{ /* 0D */ {{"map_boss03"}}, /* 0E */ {{"map_boss04"}}, /* 0F */ {{"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}, {"map_visuallobby"}}, + /* 10 */ {}, + /* 11 */ {}, }}; SetDataTableDC112000::SetDataTableDC112000() : SetDataTableBase(Version::DC_V1_11_2000_PROTOTYPE) {} pair SetDataTableDC112000::num_available_variations_for_floor(Episode, uint8_t floor) const { - return make_pair(this->NAMES[floor].size(), this->NAMES[floor][0].size()); + const auto& floor_names = this->NAMES.at(floor); + return make_pair(floor_names.size(), floor_names.empty() ? 0 : this->NAMES.at(floor)[0].size()); } pair SetDataTableDC112000::num_free_roam_variations_for_floor(Episode episode, bool, uint8_t floor) const { @@ -1921,11 +2023,25 @@ pair SetDataTableDC112000::num_free_roam_variations_for_floo } string SetDataTableDC112000::map_filename_for_variation( - uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, bool is_enemies) const { + uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, FilenameType type) const { if (floor >= this->NAMES.size()) { return ""; } - return this->NAMES.at(floor).at(var1).at(var2) + (is_enemies ? "e.dat" : "o.dat"); + string basename = this->NAMES.at(floor).at(var1).at(var2); + switch (type) { + case FilenameType::ENEMIES: + basename += "e.dat"; + break; + case FilenameType::OBJECTS: + basename += "o.dat"; + break; + case FilenameType::EVENTS: + basename += ".evt"; + break; + default: + throw logic_error("invalid map filename type"); + } + return basename; } static const vector map_file_info_dc_nte = { diff --git a/src/Map.hh b/src/Map.hh index 1d2e812c..9e9c761c 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -94,7 +94,7 @@ struct Map { } __attribute__((packed)); struct EventsSectionHeader { // Section type 3 (WAVE_EVENTS) - /* 00 */ le_uint32_t footer_offset; + /* 00 */ le_uint32_t action_stream_offset; /* 04 */ le_uint32_t entries_offset; /* 08 */ le_uint32_t entry_count; /* 0C */ be_uint32_t format; // 0 or 'evt2' @@ -248,6 +248,15 @@ struct Map { std::string str() const; } __attribute__((packed)); + struct Event { + uint32_t event_id; + uint16_t flags; + uint8_t floor; + uint32_t action_stream_offset; + + std::string str() const; + } __attribute__((packed)); + struct DATParserRandomState { PSOV2Encryption random; PSOV2Encryption location_table_random; @@ -297,6 +306,11 @@ struct Map { std::shared_ptr random_state, std::shared_ptr rare_rates = DEFAULT_RARE_ENEMIES); + void add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset); + Event& get_event(uint8_t floor, uint32_t event_id); + const Event& get_event(uint8_t floor, uint32_t event_id) const; + void add_events_from_map_data(uint8_t floor, const void* data, size_t size); + struct DATSectionsForFloor { uint32_t objects = 0xFFFFFFFF; uint32_t enemies = 0xFFFFFFFF; @@ -327,6 +341,9 @@ struct Map { std::vector enemies; std::vector enemy_set_flags; std::vector rare_enemy_indexes; + std::vector events; + std::string event_action_stream; + std::unordered_map floor_and_event_id_to_index; }; class SetDataTableBase { @@ -340,10 +357,16 @@ public: virtual std::pair num_available_variations_for_floor(Episode episode, uint8_t floor) const = 0; virtual std::pair num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const = 0; + enum class FilenameType { + OBJECTS = 0, + ENEMIES, + EVENTS, + }; + virtual std::string map_filename_for_variation( - uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const = 0; + uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const = 0; std::vector map_filenames_for_variations( - const parray& variations, Episode episode, GameMode mode, bool is_enemies) const; + const parray& variations, Episode episode, GameMode mode, FilenameType type) const; uint8_t default_area_for_floor(Episode episode, uint8_t floor) const; @@ -357,8 +380,8 @@ class SetDataTable : public SetDataTableBase { public: struct SetEntry { std::string object_list_basename; - std::string enemy_list_basename; - std::string event_list_basename; + std::string enemy_and_event_list_basename; + std::string area_setup_filename; }; SetDataTable(Version version, const std::string& data); @@ -367,7 +390,7 @@ public: virtual std::pair num_available_variations_for_floor(Episode episode, uint8_t floor) const; virtual std::pair num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const; virtual std::string map_filename_for_variation( - uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const; + uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const; std::string str() const; @@ -388,10 +411,10 @@ public: virtual std::pair num_available_variations_for_floor(Episode episode, uint8_t floor) const; virtual std::pair num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const; virtual std::string map_filename_for_variation( - uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const; + uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const; private: - static const std::array>, 0x10> NAMES; + static const std::array>, 0x12> NAMES; }; class SetDataTableDC112000 : public SetDataTableBase { @@ -402,10 +425,10 @@ public: virtual std::pair num_available_variations_for_floor(Episode episode, uint8_t floor) const; virtual std::pair num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const; virtual std::string map_filename_for_variation( - uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const; + uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const; private: - static const std::array>, 0x10> NAMES; + static const std::array>, 0x12> NAMES; }; void generate_variations_deprecated( diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index 7e6e9ef0..e0cc481c 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -697,6 +697,21 @@ void PlayerBank::assign_ids(uint32_t base_id) { } } +QuestFlagsV1& QuestFlagsV1::operator=(const QuestFlags& other) { + this->data[0] = other.data[0]; + this->data[1] = other.data[1]; + this->data[2] = other.data[2]; + return *this; +} + +QuestFlagsV1::operator QuestFlags() const { + QuestFlags ret; + ret.data[0] = this->data[0]; + ret.data[1] = this->data[1]; + ret.data[2] = this->data[2]; + return ret; +} + BattleRules::BattleRules(const JSON& json) { static const JSON empty_list = JSON::list(); diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index b34c921f..33a88e09 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -558,6 +558,13 @@ struct QuestFlags { } } __attribute__((packed)); +struct QuestFlagsV1 { + parray data; + + QuestFlagsV1& operator=(const QuestFlags& other); + operator QuestFlags() const; +} __attribute__((packed)); + struct BattleRules { enum class TechDiskMode : uint8_t { ALLOW = 0, diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 6095acd8..981e9782 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -365,15 +365,24 @@ static void on_1D(shared_ptr c, uint16_t, uint32_t, string&) { c->game_join_command_queue.reset(); } - if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE)) { - c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE); - if (!is_ep3(c->version())) { - send_game_item_state(c); + if (!is_ep3(c->version())) { + if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE)) { + c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE); + send_game_item_state(c); // 6x6D + } + if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE)) { + c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE); + send_game_object_state(c); // 6x6C + } + if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE)) { + c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE); + send_game_enemy_state(c); // 6x6B + send_game_set_state(c); // 6x6E } } if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE)) { c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); - send_game_flag_state(c); + send_game_flag_state(c); // 6x6F } } @@ -2501,13 +2510,24 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { } if (game->is_game()) { c->config.set_flag(Client::Flag::LOADING); - // If no one was in the game before, then there's no leader to send the - // item state - send it to the joining player (who is now the leader) + // If no one was in the game before, then there's no leader to send + // the game state - send it to the joining player (who is now the + // leader) if (game->count_clients() == 1) { - // No one was in the game before, so the object and enemy state is lost; - // regenerate it as if the game was just created - game->load_maps(); c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE); + // TODO: Eventually, we want to send the enemy and set states too, + // but currently this doesn't work well. Instead, we reset their + // flags so it's as if they were never defeated. + // c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE); + if (game->map) { + for (auto& enemy : game->map->enemies) { + enemy.game_flags = 0; + enemy.total_damage = 0; + enemy.state_flags = 0; + } + } + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE); + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); } } break; @@ -4301,6 +4321,15 @@ shared_ptr create_game_generic( game->map = make_shared(game->base_version, game->lobby_id, game->random_seed, game->opt_rand_crypt); } + // The game's quest flags are inherited from the creator, if known + if (c->version() == Version::BB_V4) { + game->quest_flag_values = make_unique(p->quest_flags); + game->quest_flags_known = nullptr; + } else { + game->quest_flag_values = make_unique(); + game->quest_flags_known = make_unique(); + } + return game; } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index e7164591..65159bac 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -328,19 +328,19 @@ static shared_ptr get_sync_target(shared_ptr sender_c, uint8_t c return nullptr; } -static void on_forward_sync_joining_player_state(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { +static void on_sync_joining_player_compressed_state(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { auto target = get_sync_target(c, command, flag, false); if (!target) { return; } - uint8_t subcommand; + uint8_t orig_subcommand_number; size_t decompressed_size; size_t compressed_size; const void* compressed_data; if (is_pre_v1(c->version())) { const auto& cmd = check_size_t(data, size, 0xFFFF); - subcommand = cmd.header.basic_header.subcommand; + orig_subcommand_number = cmd.header.basic_header.subcommand; decompressed_size = cmd.decompressed_size; compressed_size = size - sizeof(cmd); compressed_data = reinterpret_cast(data) + sizeof(cmd); @@ -349,117 +349,276 @@ static void on_forward_sync_joining_player_state(shared_ptr c, uint8_t c if (cmd.compressed_size > size - sizeof(cmd)) { throw runtime_error("compressed end offset is beyond end of command"); } - subcommand = cmd.header.basic_header.subcommand; + orig_subcommand_number = cmd.header.basic_header.subcommand; decompressed_size = cmd.decompressed_size; compressed_size = cmd.compressed_size; compressed_data = reinterpret_cast(data) + sizeof(cmd); } + const auto* subcommand_def = def_for_subcommand(c->version(), orig_subcommand_number); + if (!subcommand_def) { + throw runtime_error("unknown sync subcommand"); + } + + string decompressed = bc0_decompress(compressed_data, compressed_size); if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - string decompressed = bc0_decompress(compressed_data, compressed_size); c->log.info("Decompressed sync data (%zX -> %zX bytes; expected %zX):", compressed_size, decompressed.size(), decompressed_size); print_data(stderr, decompressed); } - if (is_pre_v1(c->version()) == is_pre_v1(target->version())) { - on_forward_check_game_loading(c, command, flag, data, size); + switch (subcommand_def->final_subcommand) { + case 0x6B: { + auto l = c->require_lobby(); + if (l->map) { + l->log.info("Checking client enemy state against server state"); + StringReader r(decompressed); + size_t count = r.size() / sizeof(G_SyncEnemyState_6x6B_Entry_Decompressed); + if (count != l->map->enemies.size()) { + l->log.warning("Enemy count from client (%zu) does not match enemy count from map (%zu)", + count, l->map->enemies.size()); + } else { + l->log.info("Enemy count from client matches enemy count from map (%zu)", l->map->enemies.size()); + } - } else if (is_pre_v1(target->version())) { - StringWriter w; - uint32_t cmd_size = ((compressed_size + sizeof(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3); - w.put(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E{ - .header = {{subcommand, 0x00, 0x0000}, cmd_size}, - .decompressed_size = decompressed_size, - }); - w.write(compressed_data, compressed_size); - while (w.size() & 3) { - w.put_u8(0); - } - const string& data_to_send = w.str(); - forward_subcommand(c, command, flag, data_to_send.data(), data_to_send.size()); + // TODO: We should UPDATE our view of the flags here, not just check them + for (size_t z = 0; z < min(count, l->map->enemies.size()); z++) { + const auto& entry = r.get(); + if (l->map->enemies[z].game_flags != entry.flags) { + l->log.warning("(E-%zX) Flags from client (%08" PRIX32 ") do not match game flags from map (%08" PRIX32 ")", + z, entry.flags.load(), l->map->enemies[z].game_flags); + } + if (l->map->enemies[z].total_damage != entry.total_damage) { + l->log.warning("(E-%zX) Total damage from client (%hu) does not match total damage from map (%hu)", + z, entry.total_damage.load(), l->map->enemies[z].total_damage); + } + } + } - } else { - StringWriter w; - uint32_t cmd_size = ((compressed_size + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3); - w.put(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E{ - .header = {{subcommand, 0x00, 0x0000}, cmd_size}, - .decompressed_size = decompressed_size, - .compressed_size = compressed_size, - }); - w.write(compressed_data, compressed_size); - while (w.size() & 3) { - w.put_u8(0); + send_game_join_sync_command_compressed( + target, + compressed_data, + compressed_size, + decompressed_size, + subcommand_def->nte_subcommand, + subcommand_def->proto_subcommand, + subcommand_def->final_subcommand); + break; } - const string& data_to_send = w.str(); - forward_subcommand(c, command, flag, data_to_send.data(), data_to_send.size()); + + case 0x6C: { + auto l = c->require_lobby(); + if (l->map) { + l->log.info("Checking client object state against server state"); + StringReader r(decompressed); + size_t count = r.size() / sizeof(G_SyncObjectState_6x6C_Entry_Decompressed); + if (count > l->map->objects.size()) { + l->log.warning("Object count from client (%zu) exceeds object count from map (%zu)", + count, l->map->objects.size()); + } else if (count < l->map->objects.size()) { + // This is normal because we load objects for inaccessible maps (e.g. lobby) + l->log.info("Object count from client (%zu) is less than object count from map (%zu) (this is normal)", + count, l->map->objects.size()); + } else { + l->log.info("Object count from client matches object count from map (%zu)", l->map->objects.size()); + } + + // TODO: We should UPDATE our view of the flags here, not just check them + for (size_t z = 0; z < min(count, l->map->enemies.size()); z++) { + const auto& entry = r.get(); + if (l->map->objects[z].game_flags != entry.flags) { + l->log.warning("(K-%zX) Flags from client (%04hX) do not match game flags from map (%04hX)", + z, entry.flags.load(), l->map->objects[z].game_flags); + } + } + } + + send_game_join_sync_command_compressed( + target, + compressed_data, + compressed_size, + decompressed_size, + subcommand_def->nte_subcommand, + subcommand_def->proto_subcommand, + subcommand_def->final_subcommand); + break; + } + + case 0x6D: { + if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) { + throw runtime_error(string_printf( + "decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)", + decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed))); + } + auto* decompressed_cmd = reinterpret_cast(decompressed.data()); + + size_t num_floor_items = 0; + for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) { + num_floor_items += decompressed_cmd->floor_item_count_per_floor[z]; + } + + size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem); + if (decompressed.size() < required_size) { + throw runtime_error(string_printf( + "decompressed 6x6D data (0x%zX bytes) is too short for all floor items (0x%zX bytes)", + decompressed.size(), required_size)); + } + + auto l = c->require_lobby(); + size_t target_num_items = target->character()->inventory.num_items; + for (size_t z = 0; z < 12; z++) { + uint32_t client_next_id = decompressed_cmd->next_item_id_per_player[z]; + uint32_t server_next_id = l->next_item_id_for_client[z]; + if (client_next_id == server_next_id) { + l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value", z, l->next_item_id_for_client[z]); + } else if ((z == target->lobby_client_id) && (client_next_id == server_next_id - target_num_items)) { + l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value before inventory item ID assignment (%08" PRIX32 ")", z, l->next_item_id_for_client[z], static_cast(server_next_id - target_num_items)); + } else { + l->log.warning("Next item ID for player %zu (%08" PRIX32 ") does not match expected value (%08" PRIX32 ")", + z, decompressed_cmd->next_item_id_per_player[z].load(), l->next_item_id_for_client[z]); + } + } + + // The leader's item state is never forwarded since the leader may be able + // to see items that the joining player should not see. We always generate + // a new item state for the joining player instead. + send_game_item_state(target); + break; + } + + case 0x6E: { + StringReader r(decompressed); + const auto& dec_header = r.get(); + if (dec_header.total_size != dec_header.entity_set_flags_size + dec_header.event_set_flags_size + dec_header.unused_size) { + throw runtime_error("incorrect size fields in 6x6E header"); + } + + auto l = c->require_lobby(); + if (l->map) { + l->log.info("Checking client set flag state against server state"); + + StringReader set_flags_r = r.sub(r.where(), dec_header.entity_set_flags_size); + const auto& set_flags_header = set_flags_r.get(); + + if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + c->log.info("Set flags data:"); + print_data(stderr, set_flags_r.all()); + } + + if (set_flags_header.num_object_sets > l->map->objects.size()) { + l->log.warning("Object set count from client (%hu) exceeds object count from map (%zu)", + set_flags_header.num_object_sets.load(), l->map->objects.size()); + } else if (set_flags_header.num_object_sets < l->map->objects.size()) { + // This is normal because we load objects for inaccessible maps (e.g. lobby) + l->log.info("Object set count from client (%hu) is less than object count from map (%zu) (this is normal)", + set_flags_header.num_object_sets.load(), l->map->objects.size()); + } else { + l->log.info("Object set count from client matches object count from map (%zu)", l->map->objects.size()); + } + for (size_t z = 0; z < min(set_flags_header.num_object_sets, l->map->objects.size()); z++) { + uint16_t flags = set_flags_r.get_u16l(); + if (flags != l->map->objects[z].set_flags) { + l->log.warning("(K-%zX) Set flags from client (%04hX) do not match flags from map (%04hX)", + z, flags, l->map->objects[z].set_flags); + } + } + + set_flags_r.go(sizeof(set_flags_header) + set_flags_header.num_object_sets * sizeof(le_uint16_t)); + if (set_flags_header.num_enemy_sets != l->map->enemy_set_flags.size()) { + l->log.warning("Enemy set count from client (%hu) does not match count from map (%zu)", + set_flags_header.num_enemy_sets.load(), l->map->enemy_set_flags.size()); + } else { + l->log.info("Enemy set count from client matches count from map (%zu)", l->map->enemy_set_flags.size()); + } + for (size_t z = 0; z < min(set_flags_header.num_enemy_sets, l->map->enemy_set_flags.size()); z++) { + uint16_t flags = set_flags_r.get_u16l(); + if (flags != l->map->enemy_set_flags[z]) { + l->log.warning("(S-%zX) Set flags from client (%04hX) do not match flags from map (%04hX)", + z, flags, l->map->enemy_set_flags[z]); + } + } + + StringReader event_set_flags_r = r.sub(r.where() + dec_header.entity_set_flags_size, dec_header.event_set_flags_size); + if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + c->log.info("Event flags data:"); + print_data(stderr, event_set_flags_r.all()); + } + size_t num_event_flags = event_set_flags_r.size() / sizeof(le_uint16_t); + if (num_event_flags != l->map->events.size()) { + l->log.warning("Event count from client (%zu) does not match count from map (%zu)", + num_event_flags, l->map->events.size()); + } else { + l->log.info("Event count from client matches count from map (%zu)", l->map->events.size()); + } + for (size_t z = 0; z < min(num_event_flags, l->map->events.size()); z++) { + uint16_t flags = event_set_flags_r.get_u16l(); + const auto& event = l->map->events[z]; + if (flags != event.flags) { + l->log.warning("(W-%02hhX-%" PRIX32 ") Event flags from client (%04hX) do not match flags from map (%04hX)", + event.floor, event.event_id, flags, event.flags); + } + } + } + + size_t expected_unused_size = is_v1(c->version()) ? 0x200 : 0x240; + size_t target_unused_size = is_v1(target->version()) ? 0x200 : 0x240; + if (dec_header.unused_size != expected_unused_size) { + l->log.warning("Unused data size (0x%" PRIX32 ") does not match expected size (0x%zX)", + dec_header.unused_size.load(), expected_unused_size); + } + if (dec_header.unused_size != target_unused_size) { + l->log.info("Resizing unused data from 0x%" PRIX32 " bytes to 0x%zX bytes", + dec_header.unused_size.load(), target_unused_size); + if (dec_header.unused_size >= decompressed.size()) { + throw runtime_error("unused size is too large"); + } + decompressed.resize(decompressed.size() - dec_header.unused_size.load() + target_unused_size, '\0'); + auto* wdec_header = reinterpret_cast(decompressed.data()); + wdec_header->unused_size = target_unused_size; + wdec_header->total_size = wdec_header->entity_set_flags_size + wdec_header->event_set_flags_size + wdec_header->unused_size; + } + + send_game_join_sync_command_compressed( + target, + compressed_data, + compressed_size, + decompressed_size, + subcommand_def->nte_subcommand, + subcommand_def->proto_subcommand, + subcommand_def->final_subcommand); + break; + } + + default: + throw logic_error("invalid compressed sync state subcommand"); } } -static void on_sync_joining_player_item_state(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { - auto target = get_sync_target(c, command, flag, false); - if (!target) { +template +static void on_sync_joining_player_quest_flags_t(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; } - const auto& l = c->require_lobby(); - string decompressed; - size_t compressed_size; - size_t decompressed_size; - if (is_pre_v1(c->version())) { - const auto& cmd = check_size_t(data, size, 0xFFFF); - compressed_size = size - sizeof(cmd); - decompressed_size = cmd.decompressed_size; - decompressed = bc0_decompress(reinterpret_cast(data) + sizeof(cmd), compressed_size); + auto l = c->require_lobby(); + if (l->is_game() && l->any_client_loading() && (l->leader_id == c->lobby_client_id)) { + l->quest_flags_known = nullptr; // All quest flags are now known + l->quest_flag_values = make_unique(cmd.quest_flags); + auto target = l->clients.at(flag); + if (target) { + send_game_flag_state(target); + } + } +} + +static void on_sync_joining_player_quest_flags(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { + if (is_v1(c->version())) { + on_sync_joining_player_quest_flags_t(c, command, flag, data, size); } else { - const auto& cmd = check_size_t(data, size, 0xFFFF); - compressed_size = cmd.compressed_size; - decompressed_size = cmd.decompressed_size; - if (compressed_size > size - sizeof(cmd)) { - throw runtime_error("compressed end offset is beyond end of command"); - } - decompressed = bc0_decompress(reinterpret_cast(data) + sizeof(cmd), cmd.compressed_size); + on_sync_joining_player_quest_flags_t(c, command, flag, data, size); } - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - c->log.info("Decompressed item sync data (%zX -> %zX bytes; expected %zX):", - compressed_size, decompressed.size(), decompressed_size); - print_data(stderr, decompressed); - } - - if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) { - throw runtime_error(string_printf( - "decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)", - decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed))); - } - auto* decompressed_cmd = reinterpret_cast(decompressed.data()); - - size_t num_floor_items = 0; - for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) { - num_floor_items += decompressed_cmd->floor_item_count_per_floor[z]; - } - - size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem); - if (decompressed.size() < required_size) { - throw runtime_error(string_printf( - "decompressed 6x6D data (0x%zX bytes) is too short for all floor items (0x%zX bytes)", - decompressed.size(), required_size)); - } - - size_t target_num_items = target->character()->inventory.num_items; - for (size_t z = 0; z < 12; z++) { - uint32_t client_next_id = decompressed_cmd->next_item_id_per_player[z]; - uint32_t server_next_id = l->next_item_id_for_client[z]; - if (client_next_id == server_next_id) { - l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value", z, l->next_item_id_for_client[z]); - } else if ((z == target->lobby_client_id) && (client_next_id == server_next_id - target_num_items)) { - l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value before inventory item ID assignment (%08" PRIX32 ")", z, l->next_item_id_for_client[z], static_cast(server_next_id - target_num_items)); - } else { - l->log.warning("Next item ID for player %zu (%08" PRIX32 ") does not match expected value (%08" PRIX32 ")", - z, decompressed_cmd->next_item_id_per_player[z].load(), l->next_item_id_for_client[z]); - } - } - - send_game_item_state(target); } class Parsed6x70Data { @@ -2404,39 +2563,50 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla return; } - uint16_t flag_index, difficulty, action; + uint16_t flag_num, difficulty, action; if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) { const auto& cmd = check_size_t(data, size); - flag_index = cmd.flag; + flag_num = cmd.flag; action = cmd.action; difficulty = l->difficulty; } else { const auto& cmd = check_size_t(data, size); - flag_index = cmd.flag; + flag_num = cmd.flag; action = cmd.action; difficulty = cmd.difficulty; } - if ((flag_index >= 0x400) || (difficulty > 3) || (action > 1)) { + // The client explicitly checks action for both 0 and 1 - any other value + // means no operation is performed. + if ((flag_num >= 0x400) || (difficulty > 3) || (action > 1)) { return; } + bool should_set = (action == 0); - // TODO: Should we allow overlays here? - auto p = c->character(true, false); - - auto s = c->require_server_state(); - if (s->quest_flag_persist_mask.get(flag_index)) { - // The client explicitly checks for both 0 and 1 - any other value means no - // operation is performed. - if (action == 0) { - c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_index); - p->quest_flags.set(difficulty, flag_index); - } else if (action == 1) { - c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_index); - p->quest_flags.clear(difficulty, flag_index); - } + if (l->quest_flags_known) { + l->quest_flags_known->set(difficulty, flag_num); + } + if (should_set) { + l->quest_flag_values->set(difficulty, flag_num); } else { - c->log.info("Quest flag %s:%03hX cannot be modified", name_for_difficulty(difficulty), flag_index); + l->quest_flag_values->clear(difficulty, flag_num); + } + + if (c->version() == Version::BB_V4) { + auto s = c->require_server_state(); + // TODO: Should we allow overlays here? + auto p = c->character(true, false); + if (s->quest_flag_persist_mask.get(flag_num)) { + if (should_set) { + c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num); + p->quest_flags.set(difficulty, flag_num); + } else { + c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num); + p->quest_flags.clear(difficulty, flag_num); + } + } else { + c->log.info("Quest flag %s:%03hX cannot be modified", name_for_difficulty(difficulty), flag_num); + } } forward_subcommand(c, command, flag, data, size); @@ -2448,12 +2618,12 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla // On Normal, Dark Falz does not have a third phase, so send the drop // request after the end of the second phase. On all other difficulty // levels, send it after the third phase. - if ((difficulty == 0) && (flag_index == 0x0035)) { + if ((difficulty == 0) && (flag_num == 0x0035)) { boss_enemy_type = EnemyType::DARK_FALZ_2; - } else if ((difficulty != 0) && (flag_index == 0x0037)) { + } else if ((difficulty != 0) && (flag_num == 0x0037)) { boss_enemy_type = EnemyType::DARK_FALZ_3; } - } else if (is_ep2 && (flag_index == 0x0057) && (c->floor == 0x0D)) { + } else if (is_ep2 && (flag_num == 0x0057) && (c->floor == 0x0D)) { boss_enemy_type = EnemyType::OLGA_FLOW_2; } @@ -2494,6 +2664,52 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla } } +static void on_set_entity_flag(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { + auto l = c->require_lobby(); + if (!l->is_game()) { + return; + } + + const auto& cmd = check_size_t(data, size); + if (l->map) { + if (cmd.header.enemy_id >= 0x1000 && cmd.header.enemy_id < 0x4000) { + try { + l->map->enemies.at(cmd.header.enemy_id - 0x1000).game_flags |= cmd.flags; + } catch (const out_of_range&) { + l->log.warning("Flag update refers to missing enemy"); + } + } else if (cmd.header.enemy_id >= 0x4000) { + try { + l->map->objects.at(cmd.header.enemy_id - 0x4000).game_flags |= cmd.flags; + } catch (const out_of_range&) { + l->log.warning("Flag update refers to missing object"); + } + } + } + + forward_subcommand(c, command, flag, data, size); +} + +static void on_trigger_set_event(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { + auto l = c->require_lobby(); + if (!l->is_game()) { + return; + } + + const auto& cmd = check_size_t(data, size); + if (l->map) { + // TODO: The game's logic is significantly more complex than this. Do we + // need to do anything fancy here? + try { + l->map->get_event(cmd.floor, cmd.event_id).flags |= 0x04; + } catch (const out_of_range&) { + l->log.warning("Client triggered missing event W-%02" PRIX32 "-%" PRIX32, cmd.floor.load(), cmd.event_id.load()); + } + } + + forward_subcommand(c, command, flag, data, size); +} + static void on_battle_scores(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { const auto& cmd = check_size_t>(data, size); @@ -3935,22 +4151,22 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x64 */ {0x56, 0x5D, 0x64, on_forward_check_game}, /* 6x65 */ {0x57, 0x5E, 0x65, on_forward_check_game}, /* 6x66 */ {0x00, 0x00, 0x66, on_forward_check_game}, - /* 6x67 */ {0x58, 0x5F, 0x67, on_forward_check_game}, + /* 6x67 */ {0x58, 0x5F, 0x67, on_trigger_set_event}, /* 6x68 */ {0x59, 0x60, 0x68, on_forward_check_game}, /* 6x69 */ {0x5A, 0x61, 0x69, on_npc_control}, /* 6x6A */ {0x5B, 0x62, 0x6A, on_forward_check_game}, - /* 6x6B */ {0x5C, 0x63, 0x6B, on_forward_sync_joining_player_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6C */ {0x5D, 0x64, 0x6C, on_forward_sync_joining_player_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_item_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6E */ {0x5F, 0x66, 0x6E, on_forward_sync_joining_player_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6F */ {0x00, 0x00, 0x6F, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE}, + /* 6x6B */ {0x5C, 0x63, 0x6B, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, + /* 6x6C */ {0x5D, 0x64, 0x6C, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, + /* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, + /* 6x6E */ {0x5F, 0x66, 0x6E, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, + /* 6x6F */ {0x00, 0x00, 0x6F, on_sync_joining_player_quest_flags, SDF::USE_JOIN_COMMAND_QUEUE}, /* 6x70 */ {0x60, 0x67, 0x70, on_sync_joining_player_disp_and_inventory, SDF::USE_JOIN_COMMAND_QUEUE}, /* 6x71 */ {0x00, 0x00, 0x71, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE}, /* 6x72 */ {0x61, 0x68, 0x72, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE}, /* 6x73 */ {0x00, 0x00, 0x73, on_forward_check_game_quest}, /* 6x74 */ {0x62, 0x69, 0x74, on_word_select, SDF::ALWAYS_FORWARD_TO_WATCHERS}, /* 6x75 */ {0x00, 0x00, 0x75, on_set_quest_flag}, - /* 6x76 */ {0x00, 0x00, 0x76, on_forward_check_game}, + /* 6x76 */ {0x00, 0x00, 0x76, on_set_entity_flag}, /* 6x77 */ {0x00, 0x00, 0x77, on_forward_check_game}, /* 6x78 */ {0x00, 0x00, 0x78, forward_subcommand_m}, /* 6x79 */ {0x00, 0x00, 0x79, on_forward_check_lobby}, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a8b0c73e..5f3f8319 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2396,30 +2396,44 @@ void send_ep3_change_music(Channel& ch, uint32_t song) { ch.send(0x60, 0x00, cmd); } -static void send_game_join_sync_command( +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); + send_game_join_sync_command_compressed(c, compressed_data.data(), compressed_data.size(), size, dc_nte_sc, dc_11_2000_sc, sc); +} +void send_game_join_sync_command(shared_ptr c, const string& data, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc) { + send_game_join_sync_command(c, data.data(), data.size(), dc_nte_sc, dc_11_2000_sc, sc); +} + +void send_game_join_sync_command_compressed( + shared_ptr c, + const void* data, + size_t size, + size_t decompressed_size, + uint8_t dc_nte_sc, + uint8_t dc_11_2000_sc, + uint8_t sc) { 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; + compressed_header.header.size = (size + sizeof(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E) + 3) & (~3); + compressed_header.decompressed_size = decompressed_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(); + compressed_header.header.size = (size + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E) + 3) & (~3); + compressed_header.decompressed_size = decompressed_size; + compressed_header.compressed_size = size; w.put(compressed_header); } - w.write(compressed_data); + w.write(data, size); while (w.size() & 3) { w.put_u8(0x00); } @@ -2525,31 +2539,119 @@ void send_game_object_state(shared_ptr c) { send_game_join_sync_command(c, entries.data(), entries.size() * sizeof(entries[0]), 0x5D, 0x64, 0x6C); } -void send_game_flag_state(shared_ptr c) { +void send_game_set_state(shared_ptr c) { + auto l = c->require_lobby(); + if (!l->map) { + return; + } + + size_t num_object_sets = l->map->objects.size(); + size_t num_enemy_sets = l->map->enemy_set_flags.size(); + + G_SyncSetFlagState_6x6E_Decompressed::EntitySetFlags entity_set_flags_header; + entity_set_flags_header.object_set_flags_offset = sizeof(entity_set_flags_header); + entity_set_flags_header.num_object_sets = num_object_sets; + entity_set_flags_header.enemy_set_flags_offset = sizeof(entity_set_flags_header) + num_object_sets * sizeof(le_uint16_t); + entity_set_flags_header.num_enemy_sets = num_enemy_sets; + + G_SyncSetFlagState_6x6E_Decompressed header; + header.entity_set_flags_size = sizeof(entity_set_flags_header) + (num_object_sets + num_enemy_sets) * sizeof(le_uint16_t); + header.event_set_flags_size = sizeof(le_uint16_t) * l->map->events.size(); + header.unused_size = is_v1(c->version()) ? 0x200 : 0x240; + header.total_size = header.entity_set_flags_size + header.event_set_flags_size + header.unused_size; + + StringWriter w; + w.put(header); + w.put(entity_set_flags_header); + for (const auto& obj : l->map->objects) { + w.put_u16l(obj.set_flags); + } + for (uint16_t enemy_set_flags : l->map->enemy_set_flags) { + w.put_u16l(enemy_set_flags); + } + for (const auto& event : l->map->events) { + w.put_u16l(event.flags); + } + w.extend_by(header.unused_size, 0x00); + + send_game_join_sync_command(c, w.str(), 0x5F, 0x66, 0x6E); +} + +template +void send_game_flag_state_t(shared_ptr c) { auto l = c->require_lobby(); - G_SetQuestFlags_6x6F cmd; - cmd.header.subcommand = 0x6F; - cmd.header.size = sizeof(G_SetQuestFlags_6x6F) >> 2; - cmd.header.unused = 0x0000; - cmd.quest_flags = c->character()->quest_flags; - - for (const auto& lc : l->clients) { - if (!lc) { - continue; + if (l->quest_flags_known) { // Not all flags known; send multiple 6x75s + StringWriter w; + bool use_v3_cmd = !is_v1_or_v2(c->version()) || (c->version() == Version::GC_NTE); + for (uint8_t difficulty = 0; difficulty < 4; difficulty++) { + if ((difficulty != l->difficulty) && !use_v3_cmd) { + continue; + } + const auto& diff_flags = l->quest_flag_values->data.at(difficulty); + const auto& diff_known_flags = l->quest_flags_known->data.at(difficulty); + for (uint8_t z = 0; z < diff_known_flags.data.size(); z++) { + uint8_t known_flags = diff_known_flags.data[z]; + if (!known_flags) { + continue; + } + uint8_t flag_values = diff_flags.data[z]; + for (uint8_t sh = 0; sh < 8; sh++) { + if ((known_flags << sh) & 0x80) { + uint16_t flag_num = ((z << 3) | sh); + if (use_v3_cmd) { + w.put(G_UpdateQuestFlag_V3_BB_6x75{ + {{0x75, 0x03, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)}, difficulty, 0}); + } else { + w.put(G_UpdateQuestFlag_DC_PC_6x75{ + {0x75, 0x02, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)}); + } + } + } + } } - if (lc->game_join_command_queue) { - lc->log.info("Client not ready to receive join commands; adding to queue"); - auto& cmd = lc->game_join_command_queue->emplace_back(); - cmd.command = 0x0060; - cmd.flag = 0x00000000; + + if (w.size() > 0) { + 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 = 0x006D; + cmd.flag = c->lobby_client_id; + cmd.data = std::move(w.str()); + } else { + send_command(c, 0x6D, c->lobby_client_id, w.str()); + } + } + + } else { // All flags known; send 6x6F + CmdT cmd; + cmd.header.subcommand = 0x6F; + cmd.header.size = sizeof(CmdT) >> 2; + cmd.header.unused = 0x0000; + cmd.quest_flags = (l && !l->quest_flags_known) ? *l->quest_flag_values : c->character()->quest_flags; + + 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 = 0x0062; + cmd.flag = c->lobby_client_id; cmd.data.assign(reinterpret_cast(&cmd), sizeof(cmd)); } else { - send_command_t(lc, 0x60, 0x00, cmd); + send_command_t(c, 0x62, c->lobby_client_id, cmd); } } } +void send_game_flag_state(shared_ptr c) { + // DC NTE and 11/2000 don't have this command at all; v1 has it but it doesn't + // include flags for Ultimate. + if (!is_v1(c->version())) { + send_game_flag_state_t(c); + } else if (!is_pre_v1(c->version())) { + send_game_flag_state_t(c); + } +} + void send_drop_item_to_channel(shared_ptr s, Channel& ch, const ItemData& item, bool from_enemy, uint8_t floor, float x, float z, uint16_t entity_id) { uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x51, 0x58, 0x5F); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 8d8d51d0..e402fe14 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -309,9 +309,22 @@ void send_warp(std::shared_ptr l, uint32_t floor, bool is_private); void send_ep3_change_music(Channel& ch, uint32_t song); void send_revive_player(std::shared_ptr c); +void send_game_join_sync_command( + std::shared_ptr c, const void* data, size_t size, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc); +void send_game_join_sync_command( + std::shared_ptr c, const std::string& data, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc); +void send_game_join_sync_command_compressed( + std::shared_ptr c, + const void* data, + size_t size, + size_t decompressed_size, + uint8_t dc_nte_sc, + uint8_t dc_11_2000_sc, + uint8_t sc); 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_set_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); diff --git a/src/Text.cc b/src/Text.cc index 27174926..f37260bc 100644 --- a/src/Text.cc +++ b/src/Text.cc @@ -35,7 +35,12 @@ TextTranscoder::Result TextTranscoder::operator()( const void* orig_src = src; while (src_bytes > 0) { size_t src_bytes_before = src_bytes; - size_t ret = iconv(this->ic, reinterpret_cast(const_cast(&src)), &src_bytes, reinterpret_cast(&dest), &dest_bytes); + size_t ret = iconv( + this->ic, + reinterpret_cast(const_cast(&src)), + &src_bytes, + reinterpret_cast(&dest), + &dest_bytes); size_t bytes_read = reinterpret_cast(src) - reinterpret_cast(orig_src); if (ret == this->FAILURE_RESULT) { @@ -64,7 +69,6 @@ TextTranscoder::Result TextTranscoder::operator()( default: throw runtime_error("transcoding failed: " + string_for_error(errno)); } - } else if (src_bytes_before == src_bytes) { throw runtime_error("could not transcode any characters"); } diff --git a/system/maps/pc-nte/map_ancient01_00_00.evt b/system/maps/pc-nte/map_ancient01_00_00.evt index 265c49f0..3229e7e9 100755 Binary files a/system/maps/pc-nte/map_ancient01_00_00.evt and b/system/maps/pc-nte/map_ancient01_00_00.evt differ diff --git a/system/maps/pc-nte/map_ancient01_00_01.evt b/system/maps/pc-nte/map_ancient01_00_01.evt index 8cf0a2ea..f584d6d6 100755 Binary files a/system/maps/pc-nte/map_ancient01_00_01.evt and b/system/maps/pc-nte/map_ancient01_00_01.evt differ diff --git a/system/maps/pc-nte/map_ancient01_01_00.evt b/system/maps/pc-nte/map_ancient01_01_00.evt index 3d1cd26a..1b640688 100755 Binary files a/system/maps/pc-nte/map_ancient01_01_00.evt and b/system/maps/pc-nte/map_ancient01_01_00.evt differ diff --git a/system/maps/pc-nte/map_ancient01_01_01.evt b/system/maps/pc-nte/map_ancient01_01_01.evt index 06edc4c0..8fca51b7 100755 Binary files a/system/maps/pc-nte/map_ancient01_01_01.evt and b/system/maps/pc-nte/map_ancient01_01_01.evt differ diff --git a/system/maps/pc-nte/map_ancient01_02_00.evt b/system/maps/pc-nte/map_ancient01_02_00.evt index 1f5582e7..213eb25f 100755 Binary files a/system/maps/pc-nte/map_ancient01_02_00.evt and b/system/maps/pc-nte/map_ancient01_02_00.evt differ diff --git a/system/maps/pc-nte/map_ancient01_02_01.evt b/system/maps/pc-nte/map_ancient01_02_01.evt index b8737490..81b31707 100755 Binary files a/system/maps/pc-nte/map_ancient01_02_01.evt and b/system/maps/pc-nte/map_ancient01_02_01.evt differ diff --git a/system/maps/pc-nte/map_ancient02_00_00.evt b/system/maps/pc-nte/map_ancient02_00_00.evt index fde28a7b..8403908c 100755 Binary files a/system/maps/pc-nte/map_ancient02_00_00.evt and b/system/maps/pc-nte/map_ancient02_00_00.evt differ diff --git a/system/maps/pc-nte/map_ancient02_00_01.evt b/system/maps/pc-nte/map_ancient02_00_01.evt index f4cb1812..5f739a99 100755 Binary files a/system/maps/pc-nte/map_ancient02_00_01.evt and b/system/maps/pc-nte/map_ancient02_00_01.evt differ diff --git a/system/maps/pc-nte/map_ancient02_01_00.evt b/system/maps/pc-nte/map_ancient02_01_00.evt index c6196ed0..582b818d 100755 Binary files a/system/maps/pc-nte/map_ancient02_01_00.evt and b/system/maps/pc-nte/map_ancient02_01_00.evt differ diff --git a/system/maps/pc-nte/map_ancient02_01_01.evt b/system/maps/pc-nte/map_ancient02_01_01.evt index 91b4e645..ee562722 100755 Binary files a/system/maps/pc-nte/map_ancient02_01_01.evt and b/system/maps/pc-nte/map_ancient02_01_01.evt differ diff --git a/system/maps/pc-nte/map_ancient02_02_00.evt b/system/maps/pc-nte/map_ancient02_02_00.evt index bdb7ee14..8a7f2961 100755 Binary files a/system/maps/pc-nte/map_ancient02_02_00.evt and b/system/maps/pc-nte/map_ancient02_02_00.evt differ diff --git a/system/maps/pc-nte/map_ancient02_02_01.evt b/system/maps/pc-nte/map_ancient02_02_01.evt index 105d20d7..032a895a 100755 Binary files a/system/maps/pc-nte/map_ancient02_02_01.evt and b/system/maps/pc-nte/map_ancient02_02_01.evt differ diff --git a/system/maps/pc-nte/map_ancient03_00_00.evt b/system/maps/pc-nte/map_ancient03_00_00.evt index bb26c03c..f8a7484d 100755 Binary files a/system/maps/pc-nte/map_ancient03_00_00.evt and b/system/maps/pc-nte/map_ancient03_00_00.evt differ diff --git a/system/maps/pc-nte/map_ancient03_00_01.evt b/system/maps/pc-nte/map_ancient03_00_01.evt index 9c7ed0c1..a3b0a7be 100755 Binary files a/system/maps/pc-nte/map_ancient03_00_01.evt and b/system/maps/pc-nte/map_ancient03_00_01.evt differ diff --git a/system/maps/pc-nte/map_ancient03_01_00.evt b/system/maps/pc-nte/map_ancient03_01_00.evt index d1f6ed15..99056857 100755 Binary files a/system/maps/pc-nte/map_ancient03_01_00.evt and b/system/maps/pc-nte/map_ancient03_01_00.evt differ diff --git a/system/maps/pc-nte/map_ancient03_01_01.evt b/system/maps/pc-nte/map_ancient03_01_01.evt index 22420c8f..acf862df 100755 Binary files a/system/maps/pc-nte/map_ancient03_01_01.evt and b/system/maps/pc-nte/map_ancient03_01_01.evt differ diff --git a/system/maps/pc-nte/map_ancient03_02_00.evt b/system/maps/pc-nte/map_ancient03_02_00.evt index df779344..00587afa 100755 Binary files a/system/maps/pc-nte/map_ancient03_02_00.evt and b/system/maps/pc-nte/map_ancient03_02_00.evt differ diff --git a/system/maps/pc-nte/map_ancient03_02_01.evt b/system/maps/pc-nte/map_ancient03_02_01.evt index 80a07c08..3d7ab61a 100755 Binary files a/system/maps/pc-nte/map_ancient03_02_01.evt and b/system/maps/pc-nte/map_ancient03_02_01.evt differ diff --git a/system/maps/pc-nte/map_boss01.evt b/system/maps/pc-nte/map_boss01.evt index edf041a8..5624f167 100755 Binary files a/system/maps/pc-nte/map_boss01.evt and b/system/maps/pc-nte/map_boss01.evt differ diff --git a/system/maps/pc-nte/map_boss01gj.evt b/system/maps/pc-nte/map_boss01gj.evt index a8571544..ab7be9b1 100755 Binary files a/system/maps/pc-nte/map_boss01gj.evt and b/system/maps/pc-nte/map_boss01gj.evt differ diff --git a/system/maps/pc-nte/map_boss02.evt b/system/maps/pc-nte/map_boss02.evt index edf041a8..5624f167 100755 Binary files a/system/maps/pc-nte/map_boss02.evt and b/system/maps/pc-nte/map_boss02.evt differ diff --git a/system/maps/pc-nte/map_boss04.evt b/system/maps/pc-nte/map_boss04.evt index 24655c90..9d2f089f 100755 Binary files a/system/maps/pc-nte/map_boss04.evt and b/system/maps/pc-nte/map_boss04.evt differ diff --git a/system/maps/pc-nte/map_cave01_00_00.evt b/system/maps/pc-nte/map_cave01_00_00.evt index bd9119b4..c0d42f83 100755 Binary files a/system/maps/pc-nte/map_cave01_00_00.evt and b/system/maps/pc-nte/map_cave01_00_00.evt differ diff --git a/system/maps/pc-nte/map_cave01_00_01.evt b/system/maps/pc-nte/map_cave01_00_01.evt index 3ecefa54..1ae5db1a 100755 Binary files a/system/maps/pc-nte/map_cave01_00_01.evt and b/system/maps/pc-nte/map_cave01_00_01.evt differ diff --git a/system/maps/pc-nte/map_cave01_01_00.evt b/system/maps/pc-nte/map_cave01_01_00.evt index 9b9252df..b856fba8 100755 Binary files a/system/maps/pc-nte/map_cave01_01_00.evt and b/system/maps/pc-nte/map_cave01_01_00.evt differ diff --git a/system/maps/pc-nte/map_cave01_01_01.evt b/system/maps/pc-nte/map_cave01_01_01.evt index 5ff78d62..55011814 100755 Binary files a/system/maps/pc-nte/map_cave01_01_01.evt and b/system/maps/pc-nte/map_cave01_01_01.evt differ diff --git a/system/maps/pc-nte/map_cave01_02_00.evt b/system/maps/pc-nte/map_cave01_02_00.evt index 8d491306..d79c8b27 100755 Binary files a/system/maps/pc-nte/map_cave01_02_00.evt and b/system/maps/pc-nte/map_cave01_02_00.evt differ diff --git a/system/maps/pc-nte/map_cave01_02_01.evt b/system/maps/pc-nte/map_cave01_02_01.evt index 2a740129..029e21d9 100755 Binary files a/system/maps/pc-nte/map_cave01_02_01.evt and b/system/maps/pc-nte/map_cave01_02_01.evt differ diff --git a/system/maps/pc-nte/map_cave02_00_00.evt b/system/maps/pc-nte/map_cave02_00_00.evt index 28f63265..88a3f7e7 100755 Binary files a/system/maps/pc-nte/map_cave02_00_00.evt and b/system/maps/pc-nte/map_cave02_00_00.evt differ diff --git a/system/maps/pc-nte/map_cave02_00_01.evt b/system/maps/pc-nte/map_cave02_00_01.evt index bc2ce76a..3490657a 100755 Binary files a/system/maps/pc-nte/map_cave02_00_01.evt and b/system/maps/pc-nte/map_cave02_00_01.evt differ diff --git a/system/maps/pc-nte/map_cave02_01_00.evt b/system/maps/pc-nte/map_cave02_01_00.evt index a060d694..89125236 100755 Binary files a/system/maps/pc-nte/map_cave02_01_00.evt and b/system/maps/pc-nte/map_cave02_01_00.evt differ diff --git a/system/maps/pc-nte/map_cave02_01_01.evt b/system/maps/pc-nte/map_cave02_01_01.evt index c7364346..d04cbccc 100755 Binary files a/system/maps/pc-nte/map_cave02_01_01.evt and b/system/maps/pc-nte/map_cave02_01_01.evt differ diff --git a/system/maps/pc-nte/map_cave02_02_00.evt b/system/maps/pc-nte/map_cave02_02_00.evt index f024c5d8..9bca2c5e 100755 Binary files a/system/maps/pc-nte/map_cave02_02_00.evt and b/system/maps/pc-nte/map_cave02_02_00.evt differ diff --git a/system/maps/pc-nte/map_cave02_02_01.evt b/system/maps/pc-nte/map_cave02_02_01.evt index c7e8f1cc..16a869d3 100755 Binary files a/system/maps/pc-nte/map_cave02_02_01.evt and b/system/maps/pc-nte/map_cave02_02_01.evt differ diff --git a/system/maps/pc-nte/map_cave03_00_00.evt b/system/maps/pc-nte/map_cave03_00_00.evt index 4639d351..6ecdfcf1 100755 Binary files a/system/maps/pc-nte/map_cave03_00_00.evt and b/system/maps/pc-nte/map_cave03_00_00.evt differ diff --git a/system/maps/pc-nte/map_cave03_00_01.evt b/system/maps/pc-nte/map_cave03_00_01.evt index 77e7b9de..8e9089fb 100755 Binary files a/system/maps/pc-nte/map_cave03_00_01.evt and b/system/maps/pc-nte/map_cave03_00_01.evt differ diff --git a/system/maps/pc-nte/map_cave03_01_00.evt b/system/maps/pc-nte/map_cave03_01_00.evt index fea278f3..3496716c 100755 Binary files a/system/maps/pc-nte/map_cave03_01_00.evt and b/system/maps/pc-nte/map_cave03_01_00.evt differ diff --git a/system/maps/pc-nte/map_cave03_01_01.evt b/system/maps/pc-nte/map_cave03_01_01.evt index 086d9945..be1aff89 100755 Binary files a/system/maps/pc-nte/map_cave03_01_01.evt and b/system/maps/pc-nte/map_cave03_01_01.evt differ diff --git a/system/maps/pc-nte/map_cave03_02_00.evt b/system/maps/pc-nte/map_cave03_02_00.evt index 57639e49..cb0de832 100755 Binary files a/system/maps/pc-nte/map_cave03_02_00.evt and b/system/maps/pc-nte/map_cave03_02_00.evt differ diff --git a/system/maps/pc-nte/map_cave03_02_01.evt b/system/maps/pc-nte/map_cave03_02_01.evt index b31b88b4..bb7e009d 100755 Binary files a/system/maps/pc-nte/map_cave03_02_01.evt and b/system/maps/pc-nte/map_cave03_02_01.evt differ diff --git a/system/maps/pc-nte/map_forest01_00.evt b/system/maps/pc-nte/map_forest01_00.evt index b407b730..13277350 100755 Binary files a/system/maps/pc-nte/map_forest01_00.evt and b/system/maps/pc-nte/map_forest01_00.evt differ diff --git a/system/maps/pc-nte/map_forest01_01.evt b/system/maps/pc-nte/map_forest01_01.evt index a9b2ac36..0404cd9a 100755 Binary files a/system/maps/pc-nte/map_forest01_01.evt and b/system/maps/pc-nte/map_forest01_01.evt differ diff --git a/system/maps/pc-nte/map_forest01_02.evt b/system/maps/pc-nte/map_forest01_02.evt index 59439e44..fc2b673c 100755 Binary files a/system/maps/pc-nte/map_forest01_02.evt and b/system/maps/pc-nte/map_forest01_02.evt differ diff --git a/system/maps/pc-nte/map_forest01_03.evt b/system/maps/pc-nte/map_forest01_03.evt index c9612052..dc64c5a8 100755 Binary files a/system/maps/pc-nte/map_forest01_03.evt and b/system/maps/pc-nte/map_forest01_03.evt differ diff --git a/system/maps/pc-nte/map_forest01_04.evt b/system/maps/pc-nte/map_forest01_04.evt index 1724cb38..92032617 100755 Binary files a/system/maps/pc-nte/map_forest01_04.evt and b/system/maps/pc-nte/map_forest01_04.evt differ diff --git a/system/maps/pc-nte/map_forest01_05.evt b/system/maps/pc-nte/map_forest01_05.evt index 28345b9b..86c4c974 100755 Binary files a/system/maps/pc-nte/map_forest01_05.evt and b/system/maps/pc-nte/map_forest01_05.evt differ diff --git a/system/maps/pc-nte/map_forest01_gj.evt b/system/maps/pc-nte/map_forest01_gj.evt index ff1bdda6..66e136eb 100755 Binary files a/system/maps/pc-nte/map_forest01_gj.evt and b/system/maps/pc-nte/map_forest01_gj.evt differ diff --git a/system/maps/pc-nte/map_forest02_00.evt b/system/maps/pc-nte/map_forest02_00.evt index 66b570d3..d3f265db 100755 Binary files a/system/maps/pc-nte/map_forest02_00.evt and b/system/maps/pc-nte/map_forest02_00.evt differ diff --git a/system/maps/pc-nte/map_forest02_01.evt b/system/maps/pc-nte/map_forest02_01.evt index 2cbe40d6..bad69d35 100755 Binary files a/system/maps/pc-nte/map_forest02_01.evt and b/system/maps/pc-nte/map_forest02_01.evt differ diff --git a/system/maps/pc-nte/map_forest02_02.evt b/system/maps/pc-nte/map_forest02_02.evt index 7d98bff3..9e13c8f4 100755 Binary files a/system/maps/pc-nte/map_forest02_02.evt and b/system/maps/pc-nte/map_forest02_02.evt differ diff --git a/system/maps/pc-nte/map_forest02_03.evt b/system/maps/pc-nte/map_forest02_03.evt index 620e83a0..b05fedde 100755 Binary files a/system/maps/pc-nte/map_forest02_03.evt and b/system/maps/pc-nte/map_forest02_03.evt differ diff --git a/system/maps/pc-nte/map_forest02_04.evt b/system/maps/pc-nte/map_forest02_04.evt index c38095ce..58e206ed 100755 Binary files a/system/maps/pc-nte/map_forest02_04.evt and b/system/maps/pc-nte/map_forest02_04.evt differ diff --git a/system/maps/pc-nte/map_machine01_00_00.evt b/system/maps/pc-nte/map_machine01_00_00.evt index 472e2cc2..db38bd07 100755 Binary files a/system/maps/pc-nte/map_machine01_00_00.evt and b/system/maps/pc-nte/map_machine01_00_00.evt differ diff --git a/system/maps/pc-nte/map_machine01_00_01.evt b/system/maps/pc-nte/map_machine01_00_01.evt index 88fc5341..4e39eefb 100755 Binary files a/system/maps/pc-nte/map_machine01_00_01.evt and b/system/maps/pc-nte/map_machine01_00_01.evt differ diff --git a/system/maps/pc-nte/map_machine01_01_00.evt b/system/maps/pc-nte/map_machine01_01_00.evt index ad0c8f69..f33c9dce 100755 Binary files a/system/maps/pc-nte/map_machine01_01_00.evt and b/system/maps/pc-nte/map_machine01_01_00.evt differ diff --git a/system/maps/pc-nte/map_machine01_01_01.evt b/system/maps/pc-nte/map_machine01_01_01.evt index 9b2236ec..7ab5e462 100755 Binary files a/system/maps/pc-nte/map_machine01_01_01.evt and b/system/maps/pc-nte/map_machine01_01_01.evt differ diff --git a/system/maps/pc-nte/map_machine01_02_00.evt b/system/maps/pc-nte/map_machine01_02_00.evt index f59b0028..6fc4cd7a 100755 Binary files a/system/maps/pc-nte/map_machine01_02_00.evt and b/system/maps/pc-nte/map_machine01_02_00.evt differ diff --git a/system/maps/pc-nte/map_machine01_02_01.evt b/system/maps/pc-nte/map_machine01_02_01.evt index d7271e78..690345c5 100755 Binary files a/system/maps/pc-nte/map_machine01_02_01.evt and b/system/maps/pc-nte/map_machine01_02_01.evt differ diff --git a/system/maps/pc-nte/map_machine02_00_00.evt b/system/maps/pc-nte/map_machine02_00_00.evt index e264b502..aa488768 100755 Binary files a/system/maps/pc-nte/map_machine02_00_00.evt and b/system/maps/pc-nte/map_machine02_00_00.evt differ diff --git a/system/maps/pc-nte/map_machine02_00_01.evt b/system/maps/pc-nte/map_machine02_00_01.evt index b7acb2ad..d6b99a3d 100755 Binary files a/system/maps/pc-nte/map_machine02_00_01.evt and b/system/maps/pc-nte/map_machine02_00_01.evt differ diff --git a/system/maps/pc-nte/map_machine02_01_00.evt b/system/maps/pc-nte/map_machine02_01_00.evt index 4e8aeada..0e9fa0f8 100755 Binary files a/system/maps/pc-nte/map_machine02_01_00.evt and b/system/maps/pc-nte/map_machine02_01_00.evt differ diff --git a/system/maps/pc-nte/map_machine02_01_01.evt b/system/maps/pc-nte/map_machine02_01_01.evt index 97dcb480..70b31795 100755 Binary files a/system/maps/pc-nte/map_machine02_01_01.evt and b/system/maps/pc-nte/map_machine02_01_01.evt differ diff --git a/system/maps/pc-nte/map_machine02_02_00.evt b/system/maps/pc-nte/map_machine02_02_00.evt index 689af887..3bf15662 100755 Binary files a/system/maps/pc-nte/map_machine02_02_00.evt and b/system/maps/pc-nte/map_machine02_02_00.evt differ diff --git a/system/maps/pc-nte/map_machine02_02_01.evt b/system/maps/pc-nte/map_machine02_02_01.evt index f6dbb16b..b532ae53 100755 Binary files a/system/maps/pc-nte/map_machine02_02_01.evt and b/system/maps/pc-nte/map_machine02_02_01.evt differ diff --git a/tests/GC-Episode2PrivateDrops2P.test.txt b/tests/GC-Episode2PrivateDrops2P.test.txt index 4330b23c..0b5f2832 100644 --- a/tests/GC-Episode2PrivateDrops2P.test.txt +++ b/tests/GC-Episode2PrivateDrops2P.test.txt @@ -3185,7 +3185,7 @@ I 94381 2023-12-29 15:36:50 - [Commands] Received from C-2 (Jess) (version=GC_V3 01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0200 | 00 00 00 00 00 00 00 00 | I 94381 2023-12-29 15:36:50 - [Commands] Sending to C-5 (Jonah) (version=GC_V3 command=62 flag=01) -0000 | 62 01 08 02 6F 81 88 98 00 12 CB C0 00 08 00 00 | b o +0000 | 62 01 08 02 6F 81 00 00 00 12 CB C0 00 08 00 00 | b o 0010 | 00 20 00 10 0A 20 00 00 00 00 00 00 00 00 00 00 | 0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | @@ -12489,7 +12489,7 @@ I 94381 2023-12-29 15:42:19 - [Commands] Received from C-2 (Jess) (version=GC_V3 01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0200 | 00 00 00 00 00 00 00 00 | I 94381 2023-12-29 15:42:19 - [Commands] Sending to C-5 (Jonah) (version=GC_V3 command=62 flag=01) -0000 | 62 01 08 02 6F 81 88 98 00 12 CB C0 00 08 00 00 | b o +0000 | 62 01 08 02 6F 81 00 00 00 12 CB C0 00 08 00 00 | b o 0010 | 00 30 00 10 0A 20 00 00 00 00 00 00 00 00 00 00 | 0 0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | diff --git a/tests/GC-ForestGame.test.txt b/tests/GC-ForestGame.test.txt index a87b22b9..de566705 100644 --- a/tests/GC-ForestGame.test.txt +++ b/tests/GC-ForestGame.test.txt @@ -614,6 +614,11 @@ I 49108 2023-05-26 16:18:25 - [Commands] Received from C-2 (Jess) (version=GC co 0010 | 00 00 00 00 | I 49108 2023-05-26 16:18:25 - [Commands] Received from C-2 (Jess) (version=GC command=60 flag=00) 0000 | 60 00 10 00 52 03 00 00 00 00 00 00 00 80 00 00 | ` R +I 49108 2023-05-26 16:18:25 - [Commands] Received from C-2 (Jess) (version=GC command=60 flag=00) +0000 | 06 00 20 00 00 00 00 00 00 00 00 00 09 45 24 76 | E$v +0010 | 61 72 69 61 74 69 6F 6E 73 20 30 30 30 31 30 34 | ariations 000104 +0020 | 32 31 30 31 32 30 32 31 31 30 32 30 32 31 32 31 | 2101202110202121 +0030 | 30 30 30 30 30 30 30 30 30 30 00 00 | 0000000000 I 49108 2023-05-26 16:18:32 - [Commands] Received from C-2 (Jess) (version=GC command=C1 flag=03) 0000 | C1 03 30 00 00 00 00 00 00 00 00 00 09 45 31 31 | 0 E11 0010 | 31 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 11 diff --git a/tests/GC-PoisonRoom.test.txt b/tests/GC-PoisonRoom.test.txt index 2a819480..3a604df9 100644 --- a/tests/GC-PoisonRoom.test.txt +++ b/tests/GC-PoisonRoom.test.txt @@ -25658,7 +25658,7 @@ I 56327 2024-03-03 23:56:43 - [Commands] Received from C-2 (Jess) (version=GC_V3 01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0200 | 00 00 00 00 00 00 00 00 | I 56327 2024-03-03 23:56:43 - [Commands] Sending to C-5 (NO DATA ) (version=GC_V3 command=62 flag=01) -0000 | 62 01 08 02 6F 81 88 98 00 12 CB C0 00 08 00 00 | b o +0000 | 62 01 08 02 6F 81 00 00 00 12 CB C0 00 08 00 00 | b o 0010 | 00 32 00 10 0A 20 00 00 00 00 00 00 00 00 00 00 | 2 0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | diff --git a/tests/PC-DCv1-CrossplayPrivateDrops.test.txt b/tests/PC-DCv1-CrossplayPrivateDrops.test.txt index c457ffa4..e62732f5 100644 --- a/tests/PC-DCv1-CrossplayPrivateDrops.test.txt +++ b/tests/PC-DCv1-CrossplayPrivateDrops.test.txt @@ -2822,7 +2822,7 @@ I 97037 2023-12-29 15:57:06 - [Commands] Received from C-3 (Tali) (version=PC_V2 01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0200 | 00 00 00 00 00 00 00 00 | I 97037 2023-12-29 15:57:06 - [Commands] Sending to C-5 (88888888) (version=DC_V1 command=62 flag=01) -0000 | 62 01 08 02 6F 81 00 00 00 00 00 00 00 00 00 00 | b o +0000 | 62 01 88 01 6F 61 00 00 00 00 00 00 00 00 00 00 | b o 0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | @@ -2846,15 +2846,7 @@ I 97037 2023-12-29 15:57:06 - [Commands] Sending to C-5 (88888888) (version=DC_V 0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -01C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -01D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0200 | 00 00 00 00 00 00 00 00 | +0180 | 00 00 00 00 00 00 00 00 | I 97037 2023-12-29 15:57:06 - [Commands] Received from C-3 (Tali) (version=PC_V2 command=62 flag=01) 0000 | 08 00 62 01 71 01 00 00 | b q I 97037 2023-12-29 15:57:06 - [Commands] Sending to C-5 (88888888) (version=DC_V1 command=62 flag=01) @@ -7036,7 +7028,7 @@ I 97037 2023-12-29 15:59:36 - [Commands] Sending to C-3 (Tali) (version=PC_V2 co 1910 | B0 FD B0 B1 FD B0 B2 FD B0 B3 FD B0 55 B4 FD B0 | U 1920 | B5 FD B0 B6 FD B0 B7 FD B0 55 B8 FD B0 B9 FD B0 | U 1930 | BA FD B0 BB FD B0 55 BC FD B0 BD FD B0 BE FD B0 | U -1940 | BF FD B0 03 C0 08 5F 73 | _s +1940 | BF FD B0 03 C0 08 00 00 | I 97037 2023-12-29 15:59:37 - [Commands] Received from C-5 (88888888) (version=DC_V1 command=6D flag=00) 0000 | 6D 00 B4 03 6E 00 00 00 B0 03 00 00 BA 1D 00 00 | m n 0010 | 9F 03 00 00 FF BA 1D 0A 19 B0 02 00 02 FD 10 EB | @@ -7186,7 +7178,7 @@ I 97037 2023-12-29 15:59:37 - [Commands] Received from C-5 (88888888) (version=D 0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0180 | 00 00 00 00 00 00 00 00 | I 97037 2023-12-29 15:59:37 - [Commands] Sending to C-3 (Tali) (version=PC_V2 command=62 flag=00) -0000 | 88 01 62 00 6F 61 00 00 00 00 08 00 00 00 00 00 | b oa +0000 | 08 02 62 00 6F 81 00 00 00 00 08 00 00 00 00 00 | b oa 0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | @@ -7210,7 +7202,15 @@ I 97037 2023-12-29 15:59:37 - [Commands] Sending to C-3 (Tali) (version=PC_V2 co 0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0180 | 00 00 00 00 00 00 00 00 | +0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0200 | 00 00 00 00 00 00 00 00 | I 97037 2023-12-29 15:59:37 - [Commands] Received from C-5 (88888888) (version=DC_V1 command=62 flag=00) 0000 | 62 00 08 00 71 01 00 00 | b q I 97037 2023-12-29 15:59:37 - [Commands] Sending to C-3 (Tali) (version=PC_V2 command=62 flag=00)