diff --git a/README.md b/README.md index d99a5984..42ccfcf8 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,6 @@ Some commands only work on the game server and not on the proxy server. The chat * Blue Burst player commands (game server only) * `$bbchar <1-4>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot. * `$edit `: Modifies your character data. - * `$item `: Sets the next item to be dropped from an enemy or box. * Game state commands (game server only) * `$maxlevel `: Sets the maximum level for players to join the current game. @@ -101,11 +100,12 @@ Some commands only work on the game server and not on the proxy server. The chat * `$password `: Sets the game's join password. To unlock the game, run `$password` with nothing after it. * Cheat mode commands - * `$cheat`: Enables or disables cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. This command does nothing on the proxy server - cheat commands are always available there, but are off by default. + * `$cheat`: Enables or disables cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. This command does nothing on the proxy server - cheat commands are always available there. * `$infhp` / `$inftp`: Enables or disables infinite HP or TP mode. Applies to only you. In infinite HP mode, one-hit KO attacks will still kill you. * `$warp `: Warps yourself to the given area. * `$next` (game server only): Warps yourself to the next area. * `$swa`: Enables or disables switch assist. When enabled, the server will attempt to automatically unlock two-player doors in solo games if you step on both switches sequentially. + * `$item `: Sets the next item to be dropped from an enemy or box. Item codes must be between 2 and 16 hex bytes; all unspecified bytes are zeroes. If you are on the proxy server, you must be the game leader and not using Blue Burst for this command to work. On the game server, this command works for all versions, and you do not have to be the game leader. * 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 b17606f7..97a45555 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -803,23 +803,59 @@ static void server_command_item(shared_ptr, shared_ptr l, string data = parse_data_string(encode_sjis(args)); if (data.size() < 2) { - send_text_message(c, u"$C6Item codes must be\n2 bytes or more."); + send_text_message(c, u"$C6Item codes must be\n2 bytes or more"); return; } if (data.size() > 16) { - send_text_message(c, u"$C6Item codes must be\n16 bytes or fewer."); + send_text_message(c, u"$C6Item codes must be\n16 bytes or fewer"); return; } - ItemData item_data; + l->next_drop_item.clear(); if (data.size() <= 12) { memcpy(&l->next_drop_item.data.data1, data.data(), data.size()); } else { memcpy(&l->next_drop_item.data.data1, data.data(), 12); - memcpy(&l->next_drop_item.data.data2, data.data() + 12, 12 - data.size()); + memcpy(&l->next_drop_item.data.data2, data.data() + 12, data.size() - 12); } - send_text_message(c, u"$C6Next drop chosen."); + string name = name_for_item(l->next_drop_item.data, true); + send_text_message(c, u"$C7Next drop:\n" + decode_sjis(name)); +} + +static void proxy_command_item(shared_ptr, + ProxyServer::LinkedSession& session, const std::u16string& args) { + if (session.version == GameVersion::BB) { + send_text_message(session.client_channel, + u"$C6This command cannot\nbe used on the proxy\nserver in BB games"); + return; + } + if (session.lobby_client_id != session.leader_client_id) { + send_text_message(session.client_channel, + u"$C6You must be the\nleader to use this\ncommand"); + return; + } + + string data = parse_data_string(encode_sjis(args)); + if (data.size() < 2) { + send_text_message(session.client_channel, u"$C6Item codes must be\n2 bytes or more"); + return; + } + if (data.size() > 16) { + send_text_message(session.client_channel, u"$C6Item codes must be\n16 bytes or fewer"); + return; + } + + session.next_drop_item.clear(); + if (data.size() <= 12) { + memcpy(&session.next_drop_item.data.data1, data.data(), data.size()); + } else { + memcpy(&session.next_drop_item.data.data1, data.data(), 12); + memcpy(&session.next_drop_item.data.data2, data.data() + 12, data.size() - 12); + } + + string name = name_for_item(session.next_drop_item.data, true); + send_text_message(session.client_channel, u"$C7Next drop:\n" + decode_sjis(name)); } @@ -852,7 +888,7 @@ static const unordered_map chat_commands({ {u"$gc" , {server_command_get_self_card , nullptr , u"Usage:\ngc"}}, {u"$infhp" , {server_command_infinite_hp , proxy_command_infinite_hp , u"Usage:\ninfhp"}}, {u"$inftp" , {server_command_infinite_tp , proxy_command_infinite_tp , u"Usage:\ninftp"}}, - {u"$item" , {server_command_item , nullptr , u"Usage:\nitem "}}, + {u"$item" , {server_command_item , proxy_command_item , u"Usage:\nitem "}}, {u"$kick" , {server_command_kick , nullptr , u"Usage:\nkick "}}, {u"$li" , {server_command_lobby_info , proxy_command_lobby_info , u"Usage:\nli"}}, {u"$maxlevel" , {server_command_max_level , nullptr , u"Usage:\nmax_level "}}, diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index fcb9448c..ac34677b 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -478,11 +478,11 @@ struct C_MenuItemInfoRequest_09 { // softlocking the game. struct S_Unknown_PC_0E { - PlayerLobbyDataPC lobby_data[4]; // This type is a guess - parray unknown_a1; + parray unknown_a1; + parray unknown_a2[4]; + parray unknown_a3; }; -// TODO: Document XB format for this. It's probably the same as the GC format. struct S_Unknown_GC_0E { PlayerLobbyDataGC lobby_data[4]; // This type is a guess struct UnknownA0 { @@ -496,6 +496,10 @@ struct S_Unknown_GC_0E { uint8_t unknown_a3[4]; }; +struct S_Unknown_XB_0E { + parray unknown_a1; +}; + // 0F: Invalid command // 10 (C->S): Menu selection diff --git a/src/Player.cc b/src/Player.cc index 774d2fdd..70436567 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -626,6 +626,10 @@ void PlayerLobbyDataBB::clear() { constexpr uint32_t MESETA_IDENTIFIER = 0x00040000; ItemData::ItemData() { + this->clear(); +} + +void ItemData::clear() { this->data1d[0] = 0; this->data1d[1] = 0; this->data1d[2] = 0; @@ -643,22 +647,37 @@ uint32_t ItemData::primary_identifier() const { } } -PlayerInventoryItem::PlayerInventoryItem() - : equip_flags(0x0000), tech_flag(0x0000), game_flags(0x00000000), data() { } +PlayerInventoryItem::PlayerInventoryItem() { + this->clear(); +} PlayerInventoryItem::PlayerInventoryItem(const PlayerBankItem& src) : tech_flag(0x0001), data(src.data) { this->equip_flags = (this->data.data1[0] > 2) ? 0x0044 : 0x0050; } -PlayerBankItem::PlayerBankItem() - : data(), amount(0), show_flags(0) { } +void PlayerInventoryItem::clear() { + this->equip_flags = 0x0000; + this->tech_flag = 0x0000; + this->game_flags = 0x00000000; + this->data.clear(); +} + +PlayerBankItem::PlayerBankItem() { + this->clear(); +} PlayerBankItem::PlayerBankItem(const PlayerInventoryItem& src) : data(src.data), amount(stack_size_for_item(this->data)), show_flags(1) { } +void PlayerBankItem::clear() { + this->data.clear(); + this->amount = 0; + this->show_flags = 0; +} + PlayerInventory::PlayerInventory() diff --git a/src/Player.hh b/src/Player.hh index 253ef20d..d62e60db 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -28,6 +28,7 @@ struct ItemData { // 0x14 bytes } __attribute__((packed)); ItemData(); + void clear(); uint32_t primary_identifier() const; } __attribute__((packed)); @@ -42,6 +43,7 @@ struct PlayerInventoryItem { // 0x1C bytes PlayerInventoryItem(); PlayerInventoryItem(const PlayerBankItem&); + void clear(); } __attribute__((packed)); struct PlayerBankItem { // 0x18 bytes @@ -51,6 +53,7 @@ struct PlayerBankItem { // 0x18 bytes PlayerBankItem(); PlayerBankItem(const PlayerInventoryItem&); + void clear(); } __attribute__((packed)); struct PlayerInventory { // 0x34C bytes diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index a7f0abb6..1cd8d381 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -676,6 +676,30 @@ static HandlerResult process_server_60_62_6C_6D_C9_CB(shared_ptr, } } + if (!data.empty() && + session.next_drop_item.data.data1d[0] && + (session.version != GameVersion::BB)) { + if (data[0] == 0x60) { + const auto& cmd = check_size_t(data); + session.next_drop_item.data.id = session.next_item_id++; + send_drop_item(session.server_channel, session.next_drop_item.data, + true, cmd.area, cmd.x, cmd.z, cmd.request_id); + send_drop_item(session.client_channel, session.next_drop_item.data, + true, cmd.area, cmd.x, cmd.z, cmd.request_id); + session.next_drop_item.clear(); + return HandlerResult::Type::SUPPRESS; + } else if (data[0] == -0x5E) { // A2 + const auto& cmd = check_size_t(data); + session.next_drop_item.data.id = session.next_item_id++; + send_drop_item(session.server_channel, session.next_drop_item.data, + false, cmd.area, cmd.x, cmd.z, cmd.request_id); + send_drop_item(session.client_channel, session.next_drop_item.data, + false, cmd.area, cmd.x, cmd.z, cmd.request_id); + session.next_drop_item.clear(); + return HandlerResult::Type::SUPPRESS; + } + } + return HandlerResult::Type::FORWARD; } @@ -766,6 +790,16 @@ static HandlerResult process_server_gc_B8(shared_ptr, return HandlerResult::Type::FORWARD; } +static void update_leader_id(ProxyServer::LinkedSession& session, uint8_t leader_id) { + if (session.leader_client_id != leader_id) { + session.leader_client_id = leader_id; + session.log.info("Changed room leader to %zu", session.leader_client_id); + if (session.leader_client_id == session.lobby_client_id) { + send_text_message(session.client_channel, u"$C6You are now the leader"); + } + } +} + template static HandlerResult process_server_65_67_68(shared_ptr, ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) { @@ -789,6 +823,7 @@ static HandlerResult process_server_65_67_68(shared_ptr, bool modified = false; session.lobby_client_id = cmd.client_id; + update_leader_id(session, cmd.leader_id); for (size_t x = 0; x < flag; x++) { size_t index = cmd.entries[x].lobby_data.client_id; if (index >= session.lobby_players.size()) { @@ -840,6 +875,7 @@ static HandlerResult process_server_64(shared_ptr, bool modified = false; session.lobby_client_id = cmd->client_id; + update_leader_id(session, cmd->leader_id); for (size_t x = 0; x < flag; x++) { if (cmd->lobby_data[x].guild_card == session.remote_guild_card_number) { cmd->lobby_data[x].guild_card = session.license->serial_number; @@ -885,6 +921,7 @@ static HandlerResult process_server_66_69(shared_ptr, session.lobby_players[index].name.clear(); session.log.info("Removed lobby player (%zu)", index); } + update_leader_id(session, cmd.leader_id); return HandlerResult::Type::FORWARD; } @@ -974,7 +1011,11 @@ static HandlerResult process_client_60_62_6C_6D_C9_CB(shared_ptr s, if (cmd.guild_card_number == session.license->serial_number) { cmd.guild_card_number = session.remote_guild_card_number; } - } else if (data[0] == 0x2F || data[0] == 0x4C) { + } + } + + if (!data.empty()) { + if (data[0] == 0x2F || data[0] == 0x4C) { if (session.infinite_hp) { vector subs; for (size_t amount = 1020; amount > 0;) { diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index bdb4d910..b6fe696e 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -445,11 +445,13 @@ ProxyServer::LinkedSession::LinkedSession( infinite_tp(false), save_files(false), function_call_return_value(-1), + next_item_id(0x0F000000), override_section_id(-1), override_lobby_event(-1), override_lobby_number(-1), lobby_players(12), - lobby_client_id(0) { + lobby_client_id(0), + leader_client_id(0) { this->last_switch_enabled_command.subcommand = 0; memset(this->prev_server_command_bytes, 0, sizeof(this->prev_server_command_bytes)); } diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index a5b8f535..cecb4dbf 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -66,6 +66,8 @@ public: bool save_files; int64_t function_call_return_value; // -1 = don't block function calls G_SwitchStateChanged_6x05 last_switch_enabled_command; + PlayerInventoryItem next_drop_item; + uint32_t next_item_id; int16_t override_section_id; int16_t override_lobby_event; int16_t override_lobby_number; @@ -78,6 +80,7 @@ public: }; std::vector lobby_players; size_t lobby_client_id; + size_t leader_client_id; std::shared_ptr detector_crypt; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 4672e055..64d095bc 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -671,63 +671,91 @@ static void process_subcommand_sort_inventory_bb(shared_ptr, //////////////////////////////////////////////////////////////////////////////// // EXP/Drop Item commands -static void process_subcommand_enemy_drop_item_request(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); +static bool drop_item( + std::shared_ptr l, + int64_t enemy_id, + uint8_t area, + float x, + float z, + uint16_t request_id) { - if (!l->is_game()) { - return; - } - if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { - throw logic_error("item tracking not enabled in BB game"); - } + PlayerInventoryItem item; + + // If there's an override item set (via the $item command), use that item code + if (l->next_drop_item.data.data1d[0]) { + item = l->next_drop_item; + l->next_drop_item.clear(); + + // If the game is BB, run the rare + common drop logic + } else if (l->version == GameVersion::BB) { if (!l->common_item_creator.get()) { throw runtime_error("received box drop subcommand without item creator present"); } - PlayerInventoryItem item; - - // TODO: Deduplicate this code with the box drop item request handler - bool is_rare = false; - if (l->next_drop_item.data.data1d[0]) { - item = l->next_drop_item; - l->next_drop_item.data.data1d[0] = 0; - } else { - if (l->rare_item_set) { - if (cmd->enemy_id <= 0x65) { - is_rare = sample_rare_item(* - l->random, l->rare_item_set->rares[cmd->enemy_id].probability); - } - } - - if (is_rare) { - const auto& code = l->rare_item_set->rares[cmd->enemy_id].item_code; - item.data.data1[0] = code[0]; - item.data.data1[1] = code[1]; - item.data.data1[2] = code[2]; - //RandPercentages(); - if (item.data.data1d[0] == 0) { - item.data.data1[4] |= 0x80; // make it unidentified if it's a weapon + const RareItemDrop* rare_drop = nullptr; + if (l->rare_item_set) { + if (enemy_id < 0) { + for (size_t z = 0; z < 30; z++) { + if (l->rare_item_set->box_areas[z] != area) { + continue; + } + if (sample_rare_item( + *l->random, l->rare_item_set->box_rares[z].probability)) { + rare_drop = &l->rare_item_set->box_rares[z]; + break; + } } } else { - try { - item.data = l->common_item_creator->create_drop_item(false, l->episode, - l->difficulty, cmd->area, l->section_id); - } catch (const out_of_range&) { - // create_common_item throws this when it doesn't want to make an item - return; + if ((enemy_id <= 0x65) && + sample_rare_item( + *l->random, l->rare_item_set->rares[enemy_id].probability)) { + rare_drop = &l->rare_item_set->rares[enemy_id]; } } } - item.data.id = l->generate_item_id(0xFF); - l->add_item(item, cmd->area, cmd->x, cmd->z); - send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->z, - cmd->request_id); + if (rare_drop) { + item.data.data1[0] = rare_drop->item_code[0]; + item.data.data1[1] = rare_drop->item_code[1]; + item.data.data1[2] = rare_drop->item_code[2]; + // TODO: Add random percentages + if (item.data.data1d[0] == 0) { + item.data.data1[4] |= 0x80; // make it unidentified if it's a weapon + } + } else { + try { + item.data = l->common_item_creator->create_drop_item( + false, l->episode, l->difficulty, area, l->section_id); + } catch (const out_of_range&) { + // create_common_item throws this when it doesn't want to make an item + return true; + } + } + // If the game is not BB and there's no override item, forward the request to + // the leader instead of generating the item drop command } else { + return false; + } + + item.data.id = l->generate_item_id(0xFF); + + if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { + l->add_item(item, area, x, z); + } + send_drop_item(l, item.data, (enemy_id >= 0), area, x, z, request_id); + return true; +} + +static void process_subcommand_enemy_drop_item_request(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + if (!l->is_game()) { + return; + } + + const auto* cmd = check_size_sc(data); + if (!drop_item(l, cmd->enemy_id, cmd->area, cmd->x, cmd->z, cmd->request_id)) { forward_subcommand(l, c, command, flag, data); } } @@ -735,66 +763,12 @@ static void process_subcommand_enemy_drop_item_request(shared_ptr, static void process_subcommand_box_drop_item_request(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + if (!l->is_game()) { + return; + } - if (!l->is_game()) { - return; - } - if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { - throw logic_error("item tracking not enabled in BB game"); - } - if (!l->common_item_creator.get()) { - throw runtime_error("received box drop subcommand without item creator present"); - } - - PlayerInventoryItem item; - - bool is_rare = false; - if (l->next_drop_item.data.data1d[0]) { - item = l->next_drop_item; - l->next_drop_item.data.data1d[0] = 0; - } else { - size_t index; - if (l->rare_item_set) { - for (index = 0; index < 30; index++) { - if (l->rare_item_set->box_areas[index] != cmd->area) { - continue; - } - if (sample_rare_item( - *l->random, l->rare_item_set->box_rares[index].probability)) { - is_rare = true; - break; - } - } - } - - if (is_rare) { - const auto& code = l->rare_item_set->box_rares[index].item_code; - item.data.data1[0] = code[0]; - item.data.data1[1] = code[1]; - item.data.data1[2] = code[2]; - //RandPercentages(); - if (item.data.data1d[0] == 0) { - item.data.data1[4] |= 0x80; // make it unidentified if it's a weapon - } - } else { - try { - item.data = l->common_item_creator->create_drop_item(true, l->episode, - l->difficulty, cmd->area, l->section_id); - } catch (const out_of_range&) { - // create_common_item throws this when it doesn't want to make an item - return; - } - } - } - item.data.id = l->generate_item_id(0xFF); - - l->add_item(item, cmd->area, cmd->x, cmd->z); - send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->z, - cmd->request_id); - - } else { + const auto* cmd = check_size_sc(data); + if (!drop_item(l, -1, cmd->area, cmd->x, cmd->z, cmd->request_id)) { forward_subcommand(l, c, command, flag, data); } } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 84194693..fc780c52 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1267,6 +1267,13 @@ void send_revive_player(shared_ptr l, shared_ptr c) { //////////////////////////////////////////////////////////////////////////////// // BB game commands +void send_drop_item(Channel& ch, const ItemData& item, + bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) { + G_DropItem_6x5F cmd = { + 0x5F, 0x0B, 0x0000, area, from_enemy, request_id, x, z, 0, item, 0}; + ch.send(0x60, 0x00, &cmd, sizeof(cmd)); +} + void send_drop_item(shared_ptr l, const ItemData& item, bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) { G_DropItem_6x5F cmd = { diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 418b5c4c..8b66ac1c 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -208,6 +208,8 @@ void send_set_player_visibility(std::shared_ptr l, std::shared_ptr c, bool visible); void send_revive_player(std::shared_ptr l, std::shared_ptr c); +void send_drop_item(Channel& ch, const ItemData& item, + bool from_enemy, uint8_t area, float x, float z, uint16_t request_id); void send_drop_item(std::shared_ptr l, const ItemData& item, bool from_enemy, uint8_t area, float x, float z, uint16_t request_id); void send_drop_stacked_item(std::shared_ptr l, const ItemData& item,