diff --git a/README.md b/README.md index 9c1d75ce..ffee411d 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,7 @@ Some commands only work on the game server and not on the proxy server. The chat * `$next`: Warps yourself to the next floor. * `$item ` (or `$i `): Create an item. `desc` may be a description of the item (e.g. "Hell Saber +5 0/10/25/0/10") or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy server, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions. * `$unset ` (game server only): In an Episode 3 battle, removes one of your set cards from the field. `` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed. + * `$dropmode [mode]` (proxy server): Changes the way item drops behave in the current game, if you are not on BB. Unlike the game server version of this command, using this on the proxy server requires cheats to be enabled. This works by intercepting the drop requests sent to and from the leader. (So, if you are the leader and not using server drop mode on the remote server, it affects the entire game; if not, it affects only items generated by your actions.) `mode` can be `none` (no drops), `default` (normal drops), or `proxy` (use newserv's drop tables instead of the remote server's). If `mode` is not given, tells you the current drop mode without changing it. * Configuration commands * `$event `: Sets the current holiday event in the current lobby. Holiday events are documented in the "Using $event" item in the information menu. If you're on the proxy server, this applies to all lobbies and games you join, but only you will see the new event - other players will not. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 50c5b4bd..749950e8 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -359,7 +359,7 @@ static void proxy_command_qset_qclear(shared_ptr ses ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); } else { - G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, ses->difficulty, 0x0000}; + G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, ses->lobby_difficulty, 0x0000}; ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); } @@ -1705,6 +1705,51 @@ static void server_command_dropmode(shared_ptr c, const std::string& arg } } +static void proxy_command_dropmode(shared_ptr ses, const std::string& args) { + check_cheats_allowed(ses->require_server_state(), ses); + + using DropMode = ProxyServer::LinkedSession::DropMode; + if (args.empty()) { + switch (ses->drop_mode) { + case DropMode::DISABLED: + send_text_message(ses->client_channel, "Drop mode: disabled"); + break; + case DropMode::PASSTHROUGH: + send_text_message(ses->client_channel, "Drop mode: default"); + break; + case DropMode::INTERCEPT: + send_text_message(ses->client_channel, "Drop mode: proxy"); + break; + } + + } else { + DropMode new_mode; + if ((args == "none") || (args == "disabled")) { + new_mode = DropMode::DISABLED; + } else if ((args == "default") || (args == "passthrough")) { + new_mode = DropMode::PASSTHROUGH; + } else if ((args == "proxy") || (args == "intercept")) { + new_mode = DropMode::INTERCEPT; + } else { + send_text_message(ses->client_channel, "Invalid drop mode"); + return; + } + + ses->set_drop_mode(new_mode); + switch (ses->drop_mode) { + case DropMode::DISABLED: + send_text_message(ses->client_channel, "Item drops disabled"); + break; + case DropMode::PASSTHROUGH: + send_text_message(ses->client_channel, "Item drops changed\nto default mode"); + break; + case DropMode::INTERCEPT: + send_text_message(ses->client_channel, "Item drops changed\nto proxy mode"); + break; + } + } +} + static void server_command_item(shared_ptr c, const std::string& args) { auto s = c->require_server_state(); auto l = c->require_lobby(); @@ -2092,7 +2137,7 @@ static const unordered_map chat_commands({ {"$cheat", {server_command_cheat, nullptr}}, {"$debug", {server_command_debug, nullptr}}, {"$dicerange", {server_command_ep3_set_dice_range, nullptr}}, - {"$dropmode", {server_command_dropmode, nullptr}}, + {"$dropmode", {server_command_dropmode, proxy_command_dropmode}}, {"$edit", {server_command_edit, nullptr}}, {"$ep3battledebug", {server_command_enable_ep3_battle_debug_menu, nullptr}}, {"$event", {server_command_lobby_event, proxy_command_lobby_event}}, diff --git a/src/Lobby.cc b/src/Lobby.cc index c56dae97..153c552e 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -269,17 +269,14 @@ shared_ptr Lobby::load_maps( uint32_t lobby_id, shared_ptr rare_rates, shared_ptr random_crypt, - shared_ptr vq) { - if (!vq->dat_contents_decompressed) { - throw runtime_error("quest does not have DAT data"); - } + shared_ptr quest_dat_contents_decompressed) { auto map = make_shared(version, lobby_id, random_crypt); map->add_enemies_and_objects_from_quest_data( episode, difficulty, event, - vq->dat_contents_decompressed->data(), - vq->dat_contents_decompressed->size(), + quest_dat_contents_decompressed->data(), + quest_dat_contents_decompressed->size(), rare_rates); return map; } @@ -377,6 +374,9 @@ void Lobby::load_maps() { } auto vq = this->quest->version(this->base_version, leader_c->language()); + if (!vq->dat_contents_decompressed) { + throw runtime_error("quest does not have DAT data"); + } this->map = this->load_maps( this->base_version, this->episode, @@ -385,7 +385,7 @@ void Lobby::load_maps() { this->lobby_id, rare_rates, this->random_crypt, - vq); + vq->dat_contents_decompressed); } else if (this->mode != GameMode::CHALLENGE) { auto s = this->require_server_state(); diff --git a/src/Lobby.hh b/src/Lobby.hh index 1d629c1d..48e25839 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -203,7 +203,7 @@ struct Lobby : public std::enable_shared_from_this { uint32_t lobby_id, std::shared_ptr rare_rates, std::shared_ptr random_crypt, - std::shared_ptr vq); + std::shared_ptr quest_dat_contents_decompressed); static std::shared_ptr load_maps( Version version, Episode episode, diff --git a/src/Main.cc b/src/Main.cc index 2125334a..b9826d01 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -2128,7 +2128,10 @@ Action a_find_rare_enemy_seeds( shared_ptr map; if (vq) { - map = Lobby::load_maps(version, episode, difficulty, 0, 0, rare_rates, random_crypt, vq); + if (!vq->dat_contents_decompressed) { + throw runtime_error("quest does not have DAT data"); + } + map = Lobby::load_maps(version, episode, difficulty, 0, 0, rare_rates, random_crypt, vq->dat_contents_decompressed); } else { generate_variations_deprecated(variations, random_crypt, version, episode, (mode == GameMode::SOLO)); diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 4ffe9028..dfa5fce3 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -76,18 +76,6 @@ static void forward_command(shared_ptr ses, bool to_ } } -static void check_implemented_subcommand( - shared_ptr ses, const string& data) { - if (data.size() < 4) { - ses->log.warning("Received broadcast/target command with no contents"); - } else { - if (!subcommand_is_implemented(data[0])) { - ses->log.warning("Received subcommand %02hhX which is not implemented on the server", - data[0]); - } - } -} - // Command handlers. These are called to preprocess or react to specific // commands in either direction. The functions have abbreviated names in order // to make the massive table more readable. The functions' names are, in @@ -954,6 +942,73 @@ static HandlerResult S_V3_BB_DA(shared_ptr ses, uint } } +static HandlerResult SC_6x60_6xA2(shared_ptr ses, const string& data) { + if (!ses->is_in_game) { + return HandlerResult::Type::FORWARD; + } + + if (ses->next_drop_item.data1d[0]) { + G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(data.data(), data.size()); + auto s = ses->require_server_state(); + ses->next_drop_item.id = ses->next_item_id++; + bool is_box = (cmd.rt_index == 0x30); + send_drop_item_to_channel(s, ses->server_channel, ses->next_drop_item, !is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + send_drop_item_to_channel(s, ses->client_channel, ses->next_drop_item, !is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + ses->next_drop_item.clear(); + return HandlerResult::Type::SUPPRESS; + } + + using DropMode = ProxyServer::LinkedSession::DropMode; + switch (ses->drop_mode) { + case DropMode::DISABLED: + return HandlerResult::Type::SUPPRESS; + case DropMode::PASSTHROUGH: + return HandlerResult::Type::FORWARD; + case DropMode::INTERCEPT: + if (!ses->item_creator) { + ses->log.warning("Received item drop request in intercept mode, but item creator is missing"); + return HandlerResult::Type::FORWARD; + } + break; + default: + throw logic_error("invalid drop mode"); + } + + G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(data.data(), data.size()); + auto rec = reconcile_drop_request_with_map( + ses->log, ses->client_channel, cmd, ses->version(), ses->lobby_episode, ses->config, ses->map, false); + + ItemCreator::DropResult res; + if (rec.is_box) { + if (rec.ignore_def) { + ses->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); + res = ses->item_creator->on_box_item_drop(cmd.effective_area); + } else { + ses->log.info("Creating item from box %04hX (area %02hX; specialized with %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 ")", + cmd.entity_id.load(), cmd.effective_area, cmd.param3.load(), cmd.param4.load(), cmd.param5.load(), cmd.param6.load()); + res = ses->item_creator->on_specialized_box_item_drop(cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6); + } + } else { + ses->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); + res = ses->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area); + } + + if (res.item.empty()) { + ses->log.info("No item was created"); + } else { + auto s = ses->require_server_state(); + string name = s->describe_item(ses->version(), res.item, false); + ses->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str()); + res.item.id = ses->next_item_id++; + ses->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients", + res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load()); + send_drop_item_to_channel(s, ses->client_channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + send_drop_item_to_channel(s, ses->server_channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + send_item_notification_if_needed(s, ses->client_channel, ses->config, res.item, res.is_from_rare_table); + } + return HandlerResult::Type::SUPPRESS; +} + static HandlerResult S_6x(shared_ptr ses, uint16_t, uint32_t, string& data) { auto s = ses->require_server_state(); @@ -1061,32 +1116,15 @@ static HandlerResult S_6x(shared_ptr ses, uint16_t, const auto& cmd = check_size_t(data, sizeof(G_DropItem_PC_V3_BB_6x5F)); send_item_notification_if_needed(ses->require_server_state(), ses->client_channel, ses->config, cmd.item.item, true); - } else if ((data[0] == 0x60) && ses->next_drop_item.data1d[0] && !is_v4(ses->version())) { - const auto& cmd = check_size_t( - data, sizeof(G_StandardDropItemRequest_PC_V3_BB_6x60)); - ses->next_drop_item.id = ses->next_item_id++; - send_drop_item_to_channel(s, ses->server_channel, ses->next_drop_item, true, cmd.floor, cmd.x, cmd.z, cmd.entity_id); - send_drop_item_to_channel(s, ses->client_channel, ses->next_drop_item, true, cmd.floor, cmd.x, cmd.z, cmd.entity_id); - ses->next_drop_item.clear(); - return HandlerResult::Type::SUPPRESS; - - // Note: This static_cast is required to make compilers not complain that - // the comparison is always false (which even happens in some environments - // if we use -0x5E... apparently char is unsigned on some systems, or - // std::string's char_type isn't char??) - } else if ((static_cast(data[0]) == 0xA2) && ses->next_drop_item.data1d[0] && !is_v4(ses->version())) { - const auto& cmd = check_size_t(data); - ses->next_drop_item.id = ses->next_item_id++; - send_drop_item_to_channel(s, ses->server_channel, ses->next_drop_item, false, cmd.floor, cmd.x, cmd.z, cmd.entity_id); - send_drop_item_to_channel(s, ses->client_channel, ses->next_drop_item, false, cmd.floor, cmd.x, cmd.z, cmd.entity_id); - ses->next_drop_item.clear(); - return HandlerResult::Type::SUPPRESS; + } else if ((data[0] == 0x60) || (static_cast(data[0]) == 0xA2)) { + return SC_6x60_6xA2(ses, data); } else if ((static_cast(data[0]) == 0xB5) && is_ep3(ses->version()) && (data.size() > 4)) { if (data[4] == 0x1A) { return HandlerResult::Type::SUPPRESS; } else if (data[4] == 0x36) { - const auto& cmd = check_size_t(data); + auto& cmd = check_size_t(data); + set_mask_for_ep3_game_command(&cmd, sizeof(cmd), 0); if (ses->is_in_game && (cmd.client_id >= 4)) { return HandlerResult::Type::SUPPRESS; } @@ -1299,6 +1337,20 @@ static HandlerResult S_13_A7(shared_ptr ses, uint16_ } else { ses->log.info("Download complete for file %s", sf->basename.c_str()); } + + if (!sf->is_download && ends_with(sf->basename, ".dat")) { + auto quest_dat_data = make_shared(join(sf->blocks)); + ses->map = Lobby::load_maps( + ses->version(), + ses->lobby_episode, + ses->lobby_difficulty, + ses->lobby_event, + ses->id, + Map::DEFAULT_RARE_ENEMIES, + make_shared(ses->lobby_random_seed), + quest_dat_data); + } + ses->saving_files.erase(cmd.filename.decode()); } @@ -1430,7 +1482,13 @@ static HandlerResult S_65_67_68_EB(shared_ptr ses, u ses->is_in_game = false; ses->is_in_quest = false; ses->floor = 0x0F; - ses->difficulty = 0; + ses->lobby_difficulty = 0; + ses->lobby_section_id = 0; + ses->lobby_mode = GameMode::NORMAL; + ses->lobby_episode = Episode::EP1; + ses->lobby_random_seed = 0; + ses->item_creator.reset(); + ses->map.reset(); // This command can cause the client to no longer send D6 responses when // 1A/D5 large message boxes are closed. newserv keeps track of this @@ -1448,6 +1506,7 @@ static HandlerResult S_65_67_68_EB(shared_ptr ses, u size_t num_replacements = 0; ses->lobby_client_id = cmd.lobby_flags.client_id; + ses->lobby_event = cmd.lobby_flags.event; update_leader_id(ses, cmd.lobby_flags.leader_id); for (size_t x = 0; x < flag; x++) { auto& entry = cmd.entries[x]; @@ -1496,6 +1555,50 @@ constexpr on_command_t S_P_65_67_68 = &S_65_67_68_EB; constexpr on_command_t S_X_65_67_68 = &S_65_67_68_EB; constexpr on_command_t S_B_65_67_68 = &S_65_67_68_EB; +template +Episode get_episode(const CmdT&) { + return Episode::EP1; +} +template <> +Episode get_episode(const S_JoinGame_GC_64& cmd) { + switch (cmd.episode) { + case 1: + return Episode::EP1; + case 2: + return Episode::EP2; + default: + return Episode::NONE; + } +} +template <> +Episode get_episode(const S_JoinGame_XB_64& cmd) { + switch (cmd.episode) { + case 1: + return Episode::EP1; + case 2: + return Episode::EP2; + default: + return Episode::NONE; + } +} +template <> +Episode get_episode(const S_JoinGame_BB_64& cmd) { + switch (cmd.episode) { + case 1: + return Episode::EP1; + case 2: + return Episode::EP2; + case 3: + return Episode::EP4; + default: + return Episode::NONE; + } +} +template <> +Episode get_episode(const S_JoinGame_Ep3_64&) { + return Episode::EP3; +} + template static HandlerResult S_64(shared_ptr ses, uint16_t, uint32_t flag, string& data) { CmdT* cmd; @@ -1515,7 +1618,41 @@ static HandlerResult S_64(shared_ptr ses, uint16_t, ses->floor = 0; ses->is_in_game = true; ses->is_in_quest = false; - ses->difficulty = cmd->difficulty; + ses->lobby_event = cmd->event; + ses->lobby_difficulty = cmd->difficulty; + ses->lobby_section_id = cmd->section_id; + // We only need the game mode for overriding drops, and SOLO behaves the same + // as NORMAL in that regard, so we can conveniently ignore SOLO here + if (cmd->battle_mode) { + ses->lobby_mode = GameMode::BATTLE; + } else if (cmd->challenge_mode) { + ses->lobby_mode = GameMode::CHALLENGE; + } else { + ses->lobby_mode = GameMode::NORMAL; + } + ses->lobby_random_seed = cmd->rare_seed; + if (cmd_ep3) { + ses->lobby_episode = Episode::EP3; + } else { + ses->lobby_episode = get_episode(*cmd); + } + + // Recreate the item creator if needed, and load maps + auto s = ses->require_server_state(); + ses->set_drop_mode(ses->drop_mode); + ses->map = Lobby::load_maps( + ses->version(), + ses->lobby_episode, + ses->lobby_mode, + ses->lobby_difficulty, + ses->lobby_event, + ses->id, + s->set_data_table(ses->version(), ses->lobby_episode, ses->lobby_mode, ses->lobby_difficulty), + bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2), + Map::DEFAULT_RARE_ENEMIES, + make_shared(ses->lobby_random_seed), + cmd->variations, + &ses->log); bool modified = false; @@ -1569,7 +1706,14 @@ static HandlerResult S_E8(shared_ptr ses, uint16_t, ses->floor = 0; ses->is_in_game = true; ses->is_in_quest = false; - ses->difficulty = 0; + ses->lobby_event = cmd.event; + ses->lobby_difficulty = 0; + ses->lobby_section_id = cmd.section_id; + ses->lobby_mode = GameMode::NORMAL; + ses->lobby_random_seed = 0; + ses->lobby_episode = Episode::EP3; + ses->item_creator.reset(); + ses->map.reset(); bool modified = false; @@ -1649,7 +1793,15 @@ static HandlerResult C_98(shared_ptr ses, uint16_t c ses->floor = 0x0F; ses->is_in_game = false; ses->is_in_quest = false; - ses->difficulty = 0; + ses->lobby_event = 0; + ses->lobby_difficulty = 0; + ses->lobby_section_id = 0; + ses->lobby_episode = Episode::EP1; + ses->lobby_mode = GameMode::NORMAL; + ses->lobby_random_seed = 0; + ses->item_creator.reset(); + ses->map.reset(); + if (is_v3(ses->version()) || is_v4(ses->version())) { return C_GXB_61(ses, command, flag, data); } else { @@ -1765,44 +1917,6 @@ static HandlerResult C_6x(shared_ptr ses, uint16_t c } } } - - if (!data.empty()) { - if (data[0] == 0x21) { - const auto& cmd = check_size_t(data); - ses->floor = cmd.floor; - - } else if (data[0] == 0x0C) { - if (is_v1_or_v2(ses->version()) && ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) { - send_remove_conditions(ses->client_channel, ses->lobby_client_id); - send_remove_conditions(ses->server_channel, ses->lobby_client_id); - } - } else if (data[0] == 0x2F || data[0] == 0x4B || data[0] == 0x4C) { - if (ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) { - send_player_stats_change(ses->client_channel, - ses->lobby_client_id, PlayerStatsChange::ADD_HP, 2550); - send_player_stats_change(ses->server_channel, - ses->lobby_client_id, PlayerStatsChange::ADD_HP, 2550); - } - } else if (data[0] == 0x3E) { - C_6x_movement(ses, data); - } else if (data[0] == 0x3F) { - C_6x_movement(ses, data); - } else if (data[0] == 0x40) { - C_6x_movement(ses, data); - } else if (data[0] == 0x42) { - C_6x_movement(ses, data); - } else if (data[0] == 0x48) { - if (ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED)) { - send_player_stats_change(ses->client_channel, - ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255); - send_player_stats_change(ses->server_channel, - ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255); - } - } else if (data[0] == 0x5F) { - const auto& cmd = check_size_t(data, sizeof(G_DropItem_PC_V3_BB_6x5F)); - send_item_notification_if_needed(ses->require_server_state(), ses->client_channel, ses->config, cmd.item.item, true); - } - } return C_6x(ses, command, flag, data); } @@ -1814,19 +1928,64 @@ constexpr on_command_t C_B_6x = &C_6x; template <> HandlerResult C_6x(shared_ptr ses, uint16_t, uint32_t, string& data) { - check_implemented_subcommand(ses, data); - - if (!data.empty() && (data[0] == 0x05) && ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) { - auto& cmd = check_size_t(data); - if (cmd.flags && cmd.header.object_id != 0xFFFF) { - if (ses->last_switch_enabled_command.header.subcommand == 0x05) { - ses->log.info("Switch assist: replaying previous enable command"); - ses->server_channel.send(0x60, 0x00, &ses->last_switch_enabled_command, - sizeof(ses->last_switch_enabled_command)); - ses->client_channel.send(0x60, 0x00, &ses->last_switch_enabled_command, - sizeof(ses->last_switch_enabled_command)); + if (!data.empty()) { + if ((data[0] == 0x05) && ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) { + auto& cmd = check_size_t(data); + if (cmd.flags && cmd.header.object_id != 0xFFFF) { + if (ses->last_switch_enabled_command.header.subcommand == 0x05) { + ses->log.info("Switch assist: replaying previous enable command"); + ses->server_channel.send(0x60, 0x00, &ses->last_switch_enabled_command, + sizeof(ses->last_switch_enabled_command)); + ses->client_channel.send(0x60, 0x00, &ses->last_switch_enabled_command, + sizeof(ses->last_switch_enabled_command)); + } + ses->last_switch_enabled_command = cmd; } - ses->last_switch_enabled_command = cmd; + + } else if (data[0] == 0x21) { + const auto& cmd = check_size_t(data); + ses->floor = cmd.floor; + + } else if (data[0] == 0x0C) { + if (is_v1_or_v2(ses->version()) && ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) { + send_remove_conditions(ses->client_channel, ses->lobby_client_id); + send_remove_conditions(ses->server_channel, ses->lobby_client_id); + } + + } else if (data[0] == 0x2F || data[0] == 0x4B || data[0] == 0x4C) { + if (ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) { + send_player_stats_change(ses->client_channel, + ses->lobby_client_id, PlayerStatsChange::ADD_HP, 2550); + send_player_stats_change(ses->server_channel, + ses->lobby_client_id, PlayerStatsChange::ADD_HP, 2550); + } + + } else if (data[0] == 0x3E) { + C_6x_movement(ses, data); + + } else if (data[0] == 0x3F) { + C_6x_movement(ses, data); + + } else if (data[0] == 0x40) { + C_6x_movement(ses, data); + + } else if (data[0] == 0x42) { + C_6x_movement(ses, data); + + } else if (data[0] == 0x48) { + if (ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED)) { + send_player_stats_change(ses->client_channel, + ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255); + send_player_stats_change(ses->server_channel, + ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255); + } + + } else if (data[0] == 0x5F) { + const auto& cmd = check_size_t(data, sizeof(G_DropItem_PC_V3_BB_6x5F)); + send_item_notification_if_needed(ses->require_server_state(), ses->client_channel, ses->config, cmd.item.item, true); + + } else if (data[0] == 0x60 || static_cast(data[0]) == 0xA2) { + return SC_6x60_6xA2(ses, data); } } diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index 88402602..4ac467a0 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -522,6 +522,7 @@ ProxyServer::LinkedSession::LinkedSession( sub_version(0), // This is set during resume() remote_guild_card_number(-1), next_item_id(0x0F000000), + drop_mode(DropMode::PASSTHROUGH), lobby_players(12), lobby_client_id(0), leader_client_id(0), @@ -529,7 +530,13 @@ ProxyServer::LinkedSession::LinkedSession( x(0.0), z(0.0), is_in_game(false), - is_in_quest(false) { + is_in_quest(false), + lobby_event(0), + lobby_difficulty(0), + lobby_section_id(0), + lobby_mode(GameMode::NORMAL), + lobby_episode(Episode::EP1), + lobby_random_seed(0) { this->last_switch_enabled_command.header.subcommand = 0; memset(this->prev_server_command_bytes, 0, sizeof(this->prev_server_command_bytes)); } @@ -724,6 +731,70 @@ void ProxyServer::LinkedSession::clear_lobby_players(size_t num_slots) { this->log.info("Cleared lobby players"); } +void ProxyServer::LinkedSession::set_drop_mode(DropMode new_mode) { + this->drop_mode = new_mode; + if (this->drop_mode == DropMode::INTERCEPT) { + auto s = this->require_server_state(); + auto version = this->version(); + + shared_ptr rare_item_set; + shared_ptr common_item_set; + switch (version) { + case Version::PC_PATCH: + case Version::BB_PATCH: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + throw runtime_error("cannot create item creator for this base version"); + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + case Version::DC_V1: + // TODO: We should probably have a v1 common item set at some point too + common_item_set = s->common_item_set_v2; + rare_item_set = s->rare_item_sets.at("rare-table-v1"); + break; + case Version::DC_V2: + case Version::PC_NTE: + case Version::PC_V2: + common_item_set = s->common_item_set_v2; + rare_item_set = s->rare_item_sets.at("rare-table-v2"); + break; + case Version::GC_NTE: + case Version::GC_V3: + case Version::XB_V3: + common_item_set = s->common_item_set_v3_v4; + rare_item_set = s->rare_item_sets.at("rare-table-v3"); + break; + case Version::BB_V4: + common_item_set = s->common_item_set_v3_v4; + rare_item_set = s->rare_item_sets.at("rare-table-v4"); + break; + default: + throw logic_error("invalid lobby base version"); + } + uint32_t random_seed = this->config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED) + ? this->config.override_random_seed + : this->lobby_random_seed; + this->item_creator = make_shared( + common_item_set, + rare_item_set, + s->armor_random_set, + s->tool_random_set, + s->weapon_random_sets.at(this->lobby_difficulty), + s->tekker_adjustment_set, + s->item_parameter_table(version), + version, + this->lobby_episode, + (this->lobby_mode == GameMode::SOLO) ? GameMode::NORMAL : this->lobby_mode, + this->lobby_difficulty, + this->lobby_section_id, + random_seed, + // TODO: Can we get battle rules here somehow? + nullptr); + } else { + this->item_creator.reset(); + } +} + void ProxyServer::LinkedSession::send_to_game_server(const char* error_message) { // If there is no license, do nothing - we can't return to the game server // from unlicensed sessions diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index 31499800..4fc618d1 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -74,6 +74,16 @@ public: ItemData next_drop_item; uint32_t next_item_id; + enum class DropMode { + DISABLED = 0, + PASSTHROUGH, + INTERCEPT, + }; + DropMode drop_mode; + std::shared_ptr quest_dat_data; + std::shared_ptr item_creator; + std::shared_ptr map; + struct LobbyPlayer { uint32_t guild_card_number = 0; uint64_t xb_user_id = 0; @@ -90,7 +100,12 @@ public: float z; bool is_in_game; bool is_in_quest; - uint8_t difficulty; + uint8_t lobby_event; + uint8_t lobby_difficulty; + uint8_t lobby_section_id; + GameMode lobby_mode; + Episode lobby_episode; + uint32_t lobby_random_seed; uint64_t client_ping_start_time = 0; uint64_t server_ping_start_time = 0; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index c79e97a7..529d6d22 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -49,7 +49,7 @@ static shared_ptr proxy_options_menu_for_client(shared_ptrversion())) { switch (c->config.get_drop_notification_mode()) { case Client::ItemDropNotificationMode::NOTHING: diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index a3776543..2b43a996 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2148,27 +2148,7 @@ static void on_sort_inventory_bb(shared_ptr c, uint8_t, uint8_t, void* d //////////////////////////////////////////////////////////////////////////////// // EXP/Drop Item commands -static void on_entity_drop_item_request(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - if (!l->is_game()) { - return; - } - - switch (l->drop_mode) { - case Lobby::DropMode::CLIENT: - forward_subcommand(c, command, flag, data, size); - return; - case Lobby::DropMode::DISABLED: - return; - case Lobby::DropMode::SERVER_SHARED: - case Lobby::DropMode::SERVER_DUPLICATE: - case Lobby::DropMode::SERVER_PRIVATE: - break; - default: - throw logic_error("invalid drop mode"); - } - +G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, size_t size) { G_SpecializableItemDropRequest_6xA2 cmd; if (size == sizeof(G_SpecializableItemDropRequest_6xA2)) { cmd = check_size_t(data, size); @@ -2200,21 +2180,35 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u cmd.ignore_def = in_cmd.ignore_def; cmd.effective_area = in_cmd.floor; } + return cmd; +} + +DropReconcileResult reconcile_drop_request_with_map( + PrefixedLogger& log, + Channel& client_channel, + G_SpecializableItemDropRequest_6xA2& cmd, + Version version, + Episode episode, + const Client::Config& config, + shared_ptr map, + bool mark_drop) { + DropReconcileResult res; + res.effective_rt_index = 0xFF; + res.is_box = (cmd.rt_index == 0x30); + res.should_drop = true; + res.ignore_def = (cmd.ignore_def != 0); Map::Object* map_object = nullptr; Map::Enemy* map_enemy = nullptr; - bool ignore_def = (cmd.ignore_def != 0); - uint8_t effective_rt_index = 0xFF; - bool is_box = (cmd.rt_index == 0x30); - if (is_box) { - if (l->map) { - map_object = &l->map->objects.at(cmd.entity_id); - l->log.info("Drop check for K-%hX %c %s", - map_object->object_id, ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type)); + if (res.is_box) { + if (map) { + map_object = &map->objects.at(cmd.entity_id); + log.info("Drop check for K-%hX %c %s", + map_object->object_id, res.ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type)); if (cmd.floor != map_object->floor) { - l->log.warning("Floor %02hhX from command does not match object\'s expected floor %02hhX", cmd.floor, map_object->floor); + log.warning("Floor %02hhX from command does not match object\'s expected floor %02hhX", cmd.floor, map_object->floor); } - if (is_v1_or_v2(l->base_version) && (l->base_version != Version::GC_NTE)) { + if (is_v1_or_v2(version) && (version != Version::GC_NTE)) { // V1 and V2 don't have 6xA2, so we can't get ignore_def or the object // parameters from the client on those versions cmd.param3 = map_object->param3; @@ -2223,62 +2217,90 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u cmd.param6 = map_object->param6; } bool object_ignore_def = (map_object->param1 > 0.0); - if (ignore_def != object_ignore_def) { - l->log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)", - ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", map_object->param1); + if (res.ignore_def != object_ignore_def) { + log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)", + res.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", map_object->param1); } - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5K-%hX %c %s", - map_object->object_id, ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type)); + if (config.check_flag(Client::Flag::DEBUG_ENABLED)) { + send_text_message_printf(client_channel, "$C5K-%hX %c %s", + map_object->object_id, res.ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type)); } } } else { - if (l->map) { - map_enemy = &l->map->enemies.at(cmd.entity_id); - l->log.info("Drop check for E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type)); - effective_rt_index = rare_table_index_for_enemy_type(map_enemy->type); + if (map) { + map_enemy = &map->enemies.at(cmd.entity_id); + log.info("Drop check for E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type)); + res.effective_rt_index = rare_table_index_for_enemy_type(map_enemy->type); // rt_indexes in Episode 4 don't match those sent in the command; we just // ignore what the client sends. - if ((l->episode != Episode::EP4) && (cmd.rt_index != effective_rt_index)) { - l->log.warning("rt_index %02hhX from command does not match entity\'s expected index %02" PRIX32, - cmd.rt_index, effective_rt_index); - if (!is_v4(l->base_version)) { - effective_rt_index = cmd.rt_index; + if ((episode != Episode::EP4) && (cmd.rt_index != res.effective_rt_index)) { + log.warning("rt_index %02hhX from command does not match entity\'s expected index %02" PRIX32, + cmd.rt_index, res.effective_rt_index); + if (!is_v4(version)) { + res.effective_rt_index = cmd.rt_index; } } if (cmd.floor != map_enemy->floor) { - l->log.warning("Floor %02hhX from command does not match entity\'s expected floor %02hhX", + log.warning("Floor %02hhX from command does not match entity\'s expected floor %02hhX", cmd.floor, map_enemy->floor); } - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type)); + if (config.check_flag(Client::Flag::DEBUG_ENABLED)) { + send_text_message_printf(client_channel, "$C5E-%hX %s", map_enemy->enemy_id, name_for_enum(map_enemy->type)); } } } - bool should_drop = true; - if (map_object) { - if (map_object->item_drop_checked) { - l->log.info("Drop check has already occurred for K-%04hX; skipping it", map_object->object_id); - should_drop = false; - } else { - map_object->item_drop_checked = true; + if (mark_drop) { + if (map_object) { + if (map_object->item_drop_checked) { + log.info("Drop check has already occurred for K-%04hX; skipping it", map_object->object_id); + res.should_drop = false; + } else { + map_object->item_drop_checked = true; + } } - } - if (map_enemy) { - if (map_enemy->state_flags & Map::Enemy::Flag::ITEM_DROPPED) { - l->log.info("Drop check has already occurred for E-%04hX; skipping it", map_enemy->enemy_id); - should_drop = false; - } else { - map_enemy->state_flags |= Map::Enemy::Flag::ITEM_DROPPED; + if (map_enemy) { + if (map_enemy->state_flags & Map::Enemy::Flag::ITEM_DROPPED) { + log.info("Drop check has already occurred for E-%04hX; skipping it", map_enemy->enemy_id); + res.should_drop = false; + } else { + map_enemy->state_flags |= Map::Enemy::Flag::ITEM_DROPPED; + } } } - if (should_drop) { + return res; +} + +static void on_entity_drop_item_request(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + if (!l->is_game()) { + return; + } + + switch (l->drop_mode) { + case Lobby::DropMode::CLIENT: + forward_subcommand(c, command, flag, data, size); + return; + case Lobby::DropMode::DISABLED: + return; + case Lobby::DropMode::SERVER_SHARED: + case Lobby::DropMode::SERVER_DUPLICATE: + case Lobby::DropMode::SERVER_PRIVATE: + break; + default: + throw logic_error("invalid drop mode"); + } + + G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(data, size); + auto rec = reconcile_drop_request_with_map(c->log, c->channel, cmd, c->version(), l->episode, c->config, l->map, true); + + if (rec.should_drop) { auto generate_item = [&]() -> ItemCreator::DropResult { - if (is_box) { - if (ignore_def) { + if (rec.is_box) { + if (rec.ignore_def) { l->log.info("Creating item from box %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); return l->item_creator->on_box_item_drop(cmd.effective_area); } else { @@ -2288,7 +2310,7 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u } } else { l->log.info("Creating item from enemy %04hX (area %02hX)", cmd.entity_id.load(), cmd.effective_area); - return l->item_creator->on_monster_item_drop(effective_rt_index, cmd.effective_area); + return l->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area); } }; @@ -2308,12 +2330,12 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u l->log.info("Entity %04hX (area %02hX) created item %s", cmd.entity_id.load(), cmd.effective_area, name.c_str()); if (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE) { for (const auto& lc : l->clients) { - if (lc && (is_box || (lc->floor == cmd.floor))) { + if (lc && (rec.is_box || (lc->floor == cmd.floor))) { res.item.id = l->generate_item_id(0xFF); l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s", res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str()); l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id)); - send_drop_item_to_channel(s, lc->channel, res.item, !is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + send_drop_item_to_channel(s, lc->channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); send_item_notification_if_needed(s, lc->channel, lc->config, res.item, res.is_from_rare_table); } } @@ -2323,7 +2345,7 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients", res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load()); l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x00F); - send_drop_item_to_lobby(l, res.item, !is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + send_drop_item_to_lobby(l, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); for (auto lc : l->clients) { if (lc) { send_item_notification_if_needed(s, lc->channel, lc->config, res.item, res.is_from_rare_table); @@ -2335,7 +2357,7 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u } case Lobby::DropMode::SERVER_PRIVATE: { for (const auto& lc : l->clients) { - if (lc && (is_box || (lc->floor == cmd.floor))) { + if (lc && (rec.is_box || (lc->floor == cmd.floor))) { auto res = generate_item(); if (res.item.empty()) { l->log.info("No item was created for %s", lc->channel.name.c_str()); @@ -2346,7 +2368,7 @@ static void on_entity_drop_item_request(shared_ptr c, uint8_t command, u l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s", res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str()); l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id)); - send_drop_item_to_channel(s, lc->channel, res.item, !is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); + send_drop_item_to_channel(s, lc->channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id); send_item_notification_if_needed(s, lc->channel, lc->config, res.item, res.is_from_rare_table); } } diff --git a/src/ReceiveSubcommands.hh b/src/ReceiveSubcommands.hh index 677f3f3a..961d8806 100644 --- a/src/ReceiveSubcommands.hh +++ b/src/ReceiveSubcommands.hh @@ -3,6 +3,7 @@ #include #include "Client.hh" +#include "CommandFormats.hh" #include "Lobby.hh" #include "PSOProtocol.hh" #include "ServerState.hh" @@ -16,3 +17,22 @@ void send_item_notification_if_needed( const Client::Config& config, const ItemData& item, bool is_from_rare_table); + +G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, size_t size); + +struct DropReconcileResult { + uint8_t effective_rt_index; + bool is_box; + bool should_drop; + bool ignore_def; +}; + +DropReconcileResult reconcile_drop_request_with_map( + PrefixedLogger& log, + Channel& client_channel, + G_SpecializableItemDropRequest_6xA2& cmd, + Version version, + Episode episode, + const Client::Config& config, + std::shared_ptr map, + bool mark_drop);