implement $item command on non-bb and on proxy

This commit is contained in:
Martin Michelsen
2022-07-30 23:03:46 -07:00
parent 286997188e
commit 442f33733d
11 changed files with 211 additions and 120 deletions
+2 -2
View File
@@ -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 <username> <password> <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 <stat> <value>`: Modifies your character data.
* `$item <data>`: Sets the next item to be dropped from an enemy or box.
* Game state commands (game server only)
* `$maxlevel <level>`: 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 <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 <area-id>`: 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 <data>`: 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 <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.
+42 -6
View File
@@ -803,23 +803,59 @@ static void server_command_item(shared_ptr<ServerState>, shared_ptr<Lobby> 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<ServerState>,
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<u16string, ChatCommandDefinition> 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 <item-code>"}},
{u"$item" , {server_command_item , proxy_command_item , u"Usage:\nitem <item-code>"}},
{u"$kick" , {server_command_kick , nullptr , u"Usage:\nkick <name-or-number>"}},
{u"$li" , {server_command_lobby_info , proxy_command_lobby_info , u"Usage:\nli"}},
{u"$maxlevel" , {server_command_max_level , nullptr , u"Usage:\nmax_level <level>"}},
+7 -3
View File
@@ -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<uint8_t, 0x21> unknown_a1;
parray<uint8_t, 0x08> unknown_a1;
parray<uint8_t, 0x18> unknown_a2[4];
parray<uint8_t, 0x18> 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<uint8_t, 0xE8> unknown_a1;
};
// 0F: Invalid command
// 10 (C->S): Menu selection
+23 -4
View File
@@ -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()
+3
View File
@@ -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
+42 -1
View File
@@ -676,6 +676,30 @@ static HandlerResult process_server_60_62_6C_6D_C9_CB(shared_ptr<ServerState>,
}
}
if (!data.empty() &&
session.next_drop_item.data.data1d[0] &&
(session.version != GameVersion::BB)) {
if (data[0] == 0x60) {
const auto& cmd = check_size_t<G_EnemyDropItemRequest_6x60>(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<G_BoxItemDropRequest_6xA2>(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<ServerState>,
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 <typename CmdT>
static HandlerResult process_server_65_67_68(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) {
@@ -789,6 +823,7 @@ static HandlerResult process_server_65_67_68(shared_ptr<ServerState>,
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<ServerState>,
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<ServerState>,
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<ServerState> 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<PSOSubcommand> subs;
for (size_t amount = 1020; amount > 0;) {
+3 -1
View File
@@ -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));
}
+3
View File
@@ -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<LobbyPlayer> lobby_players;
size_t lobby_client_id;
size_t leader_client_id;
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
+77 -103
View File
@@ -671,63 +671,91 @@ static void process_subcommand_sort_inventory_bb(shared_ptr<ServerState>,
////////////////////////////////////////////////////////////////////////////////
// EXP/Drop Item commands
static void process_subcommand_enemy_drop_item_request(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (l->version == GameVersion::BB) {
const auto* cmd = check_size_sc<G_EnemyDropItemRequest_6x60>(data);
static bool drop_item(
std::shared_ptr<Lobby> 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<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!l->is_game()) {
return;
}
const auto* cmd = check_size_sc<G_EnemyDropItemRequest_6x60>(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<ServerState>,
static void process_subcommand_box_drop_item_request(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (l->version == GameVersion::BB) {
const auto* cmd = check_size_sc<G_BoxItemDropRequest_6xA2>(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<G_BoxItemDropRequest_6xA2>(data);
if (!drop_item(l, -1, cmd->area, cmd->x, cmd->z, cmd->request_id)) {
forward_subcommand(l, c, command, flag, data);
}
}
+7
View File
@@ -1267,6 +1267,13 @@ void send_revive_player(shared_ptr<Lobby> l, shared_ptr<Client> 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<Lobby> l, const ItemData& item,
bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) {
G_DropItem_6x5F cmd = {
+2
View File
@@ -208,6 +208,8 @@ void send_set_player_visibility(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, bool visible);
void send_revive_player(std::shared_ptr<Lobby> l, std::shared_ptr<Client> 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<Lobby> 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<Lobby> l, const ItemData& item,