#include "ReceiveSubcommands.hh" #include #include #include #include "Client.hh" #include "Lobby.hh" #include "Player.hh" #include "PSOProtocol.hh" #include "SendCommands.hh" #include "Text.hh" #include "Items.hh" using namespace std; // The functions in this file are called when a client sends a game command // (60, 62, 6C, or 6D). bool command_is_private(uint8_t command) { // TODO: are either of the Ep3 commands (C9/CB) private? Looks like not... return (command == 0x62) || (command == 0x6D); } template const CmdT* check_size_sc( const string& data, size_t min_size = sizeof(CmdT), size_t max_size = sizeof(CmdT), bool check_size_field = true) { if (max_size < min_size) { max_size = min_size; } const auto* cmd = &check_size_t(data, min_size, max_size); if (check_size_field && cmd->size) { throw runtime_error("invalid subcommand size field"); } return cmd; } template <> const PSOSubcommand* check_size_sc( const string& data, size_t min_size, size_t max_size, bool check_size_field) { if (max_size < min_size) { max_size = min_size; } const auto* ret = &check_size_t(data, min_size, max_size); if (check_size_field && (ret[0].byte[1] != data.size() / 4)) { throw runtime_error("invalid subcommand size field"); } return ret; } static void forward_subcommand(shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { // if the command is an Ep3-only command, make sure an Ep3 client sent it bool command_is_ep3 = (command & 0xF0) == 0xC0; if (command_is_ep3 && !(c->flags & Client::Flag::EPISODE_3)) { return; } if (command_is_private(command)) { if (flag >= l->max_clients) { return; } auto target = l->clients[flag]; if (!target) { return; } if (command_is_ep3 && !(target->flags & Client::Flag::EPISODE_3)) { return; } send_command(target, command, flag, data, size); } else { if (command_is_ep3) { for (auto& target : l->clients) { if (!target || (target == c) || !(target->flags & Client::Flag::EPISODE_3)) { continue; } send_command(target, command, flag, data, size); } } else { send_command_excluding_client(l, c, command, flag, data, size); } } } static void forward_subcommand(shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { forward_subcommand(l, c, command, flag, data.data(), data.size()); } //////////////////////////////////////////////////////////////////////////////// // Chat commands and the like static void process_subcommand_send_guild_card(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (!command_is_private(command) || !l || (flag >= l->max_clients) || (!l->clients[flag])) { return; } if (c->version == GameVersion::PC) { const auto* cmd = check_size_sc(data); c->player.guild_card_desc = cmd->desc; } else if (c->version == GameVersion::GC) { const auto* cmd = check_size_sc(data); c->player.guild_card_desc = cmd->desc; } else if (c->version == GameVersion::BB) { const auto* cmd = check_size_sc(data); c->player.guild_card_desc = cmd->desc; } send_guild_card(l->clients[flag], c); } // client sends a symbol chat static void process_subcommand_symbol_chat(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, 0x08, 0xFFFF); if (!c->can_chat || (p[1].byte[0] != c->lobby_client_id)) { return; } forward_subcommand(l, c, command, flag, data); } // client sends a word select chat static void process_subcommand_word_select(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, 0x20, 0xFFFF); if (!c->can_chat || (p[0].byte[2] != c->lobby_client_id)) { return; } // TODO: bring this back if it turns out to be important; I suspect it's not //p->byte[2] = p->byte[3] = p->byte[(c->version == GameVersion::BB) ? 2 : 3]; for (size_t x = 1; x < 8; x++) { if ((p[x].word[0] > 0x1863) && (p[x].word[0] != 0xFFFF)) { return; } if ((p[x].word[1] > 0x1863) && (p[x].word[1] != 0xFFFF)) { return; } } forward_subcommand(l, c, command, flag, data); } //////////////////////////////////////////////////////////////////////////////// // Game commands used by cheat mechanisms // need to process changing areas since we keep track of where players are static void process_subcommand_change_area(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, 0x08, 0xFFFF); if (!l->is_game()) { return; } c->area = p[1].dword; forward_subcommand(l, c, command, flag, data); } // when a player is hit by an enemy, heal them if infinite HP is enabled static void process_subcommand_hit_by_enemy(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, 0x04, 0xFFFF); if (!l->is_game() || (p->byte[2] != c->lobby_client_id)) { return; } forward_subcommand(l, c, command, flag, data); if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->infinite_hp) { send_player_stats_change(l, c, PlayerStatsChange::ADD_HP, 1020); } } // when a player casts a tech, restore TP if infinite TP is enabled static void process_subcommand_use_technique(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, 0x04, 0xFFFF); if (!l->is_game() || (p->byte[2] != c->lobby_client_id)) { return; } forward_subcommand(l, c, command, flag, data); if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->infinite_tp) { send_player_stats_change(l, c, PlayerStatsChange::ADD_TP, 255); } } static void process_subcommand_switch_state_changed(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { auto& cmd = check_size_t(data); if (!l->is_game()) { return; } forward_subcommand(l, c, command, flag, data); if (cmd.enabled && cmd.switch_id != 0xFFFF) { if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->switch_assist && (c->last_switch_enabled_command.subcommand == 0x05)) { log(INFO, "[Switch assist] Replaying previous enable command"); forward_subcommand(l, c, command, flag, &c->last_switch_enabled_command, sizeof(c->last_switch_enabled_command)); send_command(c, command, flag, c->last_switch_enabled_command); } c->last_switch_enabled_command = cmd; } } //////////////////////////////////////////////////////////////////////////////// // BB Item commands // player drops an item static void process_subcommand_drop_item(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 ((cmd->client_id != c->lobby_client_id)) { return; } PlayerInventoryItem item; c->player.remove_item(cmd->item_id, 0, &item); l->add_item(item); } forward_subcommand(l, c, command, flag, data); } // player splits a stack and drops part of it static void process_subcommand_drop_stacked_item(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() || (cmd->client_id != c->lobby_client_id)) { return; } PlayerInventoryItem item; c->player.remove_item(cmd->item_id, cmd->amount, &item); // if a stack was split, the original item still exists, so the dropped item // needs a new ID. remove_item signals this by returning an item with id=-1 if (item.data.item_id == 0xFFFFFFFF) { item.data.item_id = l->generate_item_id(c->lobby_client_id); } l->add_item(item); send_drop_stacked_item(l, item.data, cmd->area, cmd->x, cmd->y); } else { forward_subcommand(l, c, command, flag, data); } } // player requests to pick up an item static void process_subcommand_pick_up_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (l->version == GameVersion::BB) { auto* cmd = check_size_sc(data); if (!l->is_game() || (cmd->client_id != c->lobby_client_id)) { return; } PlayerInventoryItem item; l->remove_item(cmd->item_id, &item); c->player.add_item(item); send_pick_up_item(l, c, item.data.item_id, cmd->area); } else { forward_subcommand(l, c, command, flag, data); } } // player equips an item static void process_subcommand_equip_unequip_item(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 ((cmd->client_id != c->lobby_client_id)) { return; } size_t index = c->player.inventory.find_item(cmd->item_id); if (cmd->command == 0x25) { c->player.inventory.items[index].game_flags |= 0x00000008; // equip } else { c->player.inventory.items[index].game_flags &= 0xFFFFFFF7; // unequip } } else { forward_subcommand(l, c, command, flag, data); } } static void process_subcommand_use_item(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 (cmd->client_id != c->lobby_client_id) { return; } size_t index = c->player.inventory.find_item(cmd->item_id); if (cmd->command == 0x25) { c->player.inventory.items[index].game_flags |= 0x00000008; // equip } else { c->player.inventory.items[index].game_flags &= 0xFFFFFFF7; // unequip } player_use_item(c, index); } forward_subcommand(l, c, command, flag, data); } static void process_subcommand_open_shop_or_ep3_unknown(shared_ptr s, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (l->flags & Lobby::Flag::EPISODE_3_ONLY) { check_size_sc(data, 0x08, 0xFFFF); forward_subcommand(l, c, command, flag, data); } else { const auto* p = check_size_sc(data, 0x08); uint32_t shop_type = p[1].dword; if ((l->version == GameVersion::BB) && l->is_game()) { size_t num_items = (rand() % 4) + 9; c->player.current_shop_contents.clear(); while (c->player.current_shop_contents.size() < num_items) { ItemData item_data; if (shop_type == 0) { // tool shop item_data = s->common_item_creator->create_shop_item(l->difficulty, 3); } else if (shop_type == 1) { // weapon shop item_data = s->common_item_creator->create_shop_item(l->difficulty, 0); } else if (shop_type == 2) { // guards shop item_data = s->common_item_creator->create_shop_item(l->difficulty, 1); } else { // unknown shop... just leave it blank I guess break; } item_data.item_id = l->generate_item_id(c->lobby_client_id); c->player.current_shop_contents.emplace_back(item_data); } send_shop(c, shop_type); } } } static void process_subcommand_open_bank(shared_ptr, shared_ptr l, shared_ptr c, uint8_t, uint8_t, const string&) { if ((l->version == GameVersion::BB) && l->is_game()) { send_bank(c); } } static void process_subcommand_bank_action(shared_ptr, shared_ptr l, shared_ptr c, uint8_t, uint8_t, const string& data) { if (l->version == GameVersion::BB) { const auto* cmd = check_size_sc(data); if (!l->is_game()) { return; } if (cmd->action == 0) { // deposit if (cmd->item_id == 0xFFFFFFFF) { // meseta if (cmd->meseta_amount > c->player.disp.meseta) { return; } if ((c->player.bank.meseta + cmd->meseta_amount) > 999999) { return; } c->player.bank.meseta += cmd->meseta_amount; c->player.disp.meseta -= cmd->meseta_amount; } else { // item PlayerInventoryItem item; c->player.remove_item(cmd->item_id, cmd->item_amount, &item); c->player.bank.add_item(item.to_bank_item()); send_destroy_item(l, c, cmd->item_id, cmd->item_amount); } } else if (cmd->action == 1) { // take if (cmd->item_id == 0xFFFFFFFF) { // meseta if (cmd->meseta_amount > c->player.bank.meseta) { return; } if ((c->player.disp.meseta + cmd->meseta_amount) > 999999) { return; } c->player.bank.meseta -= cmd->meseta_amount; c->player.disp.meseta += cmd->meseta_amount; } else { // item PlayerBankItem bank_item; c->player.bank.remove_item(cmd->item_id, cmd->item_amount, &bank_item); PlayerInventoryItem item = bank_item.to_inventory_item(); item.data.item_id = l->generate_item_id(0xFF); c->player.add_item(item); send_create_inventory_item(l, c, item.data); } } } } // player sorts the items in their inventory static void process_subcommand_sort_inventory(shared_ptr, shared_ptr l, shared_ptr c, uint8_t, uint8_t, const string& data) { if (l->version == GameVersion::BB) { const auto* cmd = check_size_sc(data); PlayerInventory sorted; memset(&sorted, 0, sizeof(PlayerInventory)); for (size_t x = 0; x < 30; x++) { if (cmd->item_ids[x] == 0xFFFFFFFF) { sorted.items[x].data.item_id = 0xFFFFFFFF; } else { size_t index = c->player.inventory.find_item(cmd->item_ids[x]); sorted.items[x] = c->player.inventory.items[index]; } } sorted.num_items = c->player.inventory.num_items; sorted.hp_materials_used = c->player.inventory.hp_materials_used; sorted.tp_materials_used = c->player.inventory.tp_materials_used; sorted.language = c->player.inventory.language; c->player.inventory = sorted; } } //////////////////////////////////////////////////////////////////////////////// // BB EXP/Drop Item commands // enemy killed; leader sends drop item request static void process_subcommand_enemy_drop_item(shared_ptr s, 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; } PlayerInventoryItem item; memset(&item, 0, sizeof(PlayerInventoryItem)); bool is_rare = false; if (l->next_drop_item.data.item_data1d[0]) { item = l->next_drop_item; l->next_drop_item.data.item_data1d[0] = 0; } else { if (l->rare_item_set) { if (cmd->enemy_id <= 0x65) { is_rare = sample_rare_item(l->rare_item_set->rares[cmd->enemy_id].probability); } } if (is_rare) { memcpy(&item.data.item_data1d, l->rare_item_set->rares[cmd->enemy_id].item_code, 3); //RandPercentages(); if (item.data.item_data1d[0] == 0) { item.data.item_data1[4] |= 0x80; // make it untekked if it's a weapon } } else { try { item.data = s->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; } } } item.data.item_id = l->generate_item_id(0xFF); l->add_item(item); send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->y, cmd->request_id); } else { forward_subcommand(l, c, command, flag, data); } } // box broken; leader sends drop item request static void process_subcommand_box_drop_item(shared_ptr s, 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; } PlayerInventoryItem item; memset(&item, 0, sizeof(PlayerInventoryItem)); bool is_rare = false; if (l->next_drop_item.data.item_data1d[0]) { item = l->next_drop_item; l->next_drop_item.data.item_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->rare_item_set->box_rares[index].probability)) { is_rare = true; break; } } } if (is_rare) { memcpy(item.data.item_data1, l->rare_item_set->box_rares[index].item_code, 3); //RandPercentages(); if (item.data.item_data1d[0] == 0) { item.data.item_data1[4] |= 0x80; // make it untekked if it's a weapon } } else { try { item.data = s->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.item_id = l->generate_item_id(0xFF); l->add_item(item); send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->y, cmd->request_id); } else { forward_subcommand(l, c, command, flag, data); } } // enemy hit by player static void process_subcommand_enemy_hit(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 (cmd->enemy_id >= l->enemies.size()) { return; } if (l->enemies[cmd->enemy_id].hit_flags & 0x80) { return; } l->enemies[cmd->enemy_id].hit_flags |= (1 << c->lobby_client_id); l->enemies[cmd->enemy_id].last_hit = c->lobby_client_id; } forward_subcommand(l, c, command, flag, data); } // enemy killed by player static void process_subcommand_enemy_killed(shared_ptr s, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { forward_subcommand(l, c, command, flag, data); if (l->version == GameVersion::BB) { const auto* cmd = check_size_sc(data); if (!l->is_game() || (cmd->enemy_id >= l->enemies.size() || (l->enemies[cmd->enemy_id].hit_flags & 0x80))) { return; } if (l->enemies[cmd->enemy_id].experience == 0xFFFFFFFF) { send_text_message(c, u"$C6Unknown enemy type killed"); return; } auto& enemy = l->enemies[cmd->enemy_id]; enemy.hit_flags |= 0x80; for (size_t x = 0; x < l->max_clients; x++) { if (!((enemy.hit_flags >> x) & 1)) { continue; // player did not hit this enemy } auto other_c = l->clients[x]; if (!other_c) { continue; // no player } if (other_c->player.disp.level >= 199) { continue; // player is level 200 or higher } // killer gets full experience, others get 77% uint32_t exp; if (enemy.last_hit == other_c->lobby_client_id) { exp = enemy.experience; } else { exp = ((enemy.experience * 77) / 100); } other_c->player.disp.experience += exp; send_give_experience(l, other_c, exp); bool leveled_up = false; do { const auto& level = s->level_table->stats_for_level( other_c->player.disp.char_class, other_c->player.disp.level + 1); if (other_c->player.disp.experience >= level.experience) { leveled_up = true; level.apply(other_c->player.disp.stats); other_c->player.disp.level++; } else { break; } } while (other_c->player.disp.level < 199); if (leveled_up) { send_level_up(l, other_c); } } } } // destroy item (sent when there are too many items on the ground) static void process_subcommand_destroy_item(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; } l->remove_item(cmd->item_id, nullptr); } forward_subcommand(l, c, command, flag, data); } // player requests to tekk an item static void process_subcommand_identify_item(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() || (cmd->client_id != c->lobby_client_id)) { return; } size_t x = c->player.inventory.find_item(cmd->item_id); if (c->player.inventory.items[x].data.item_data1[0] != 0) { return; // only weapons can be identified } c->player.disp.meseta -= 100; c->player.identify_result = c->player.inventory.items[x]; c->player.identify_result.data.item_data1[4] &= 0x7F; // TODO: move this into a SendCommands.cc function G_IdentifyResult_BB_6xB9 res; res.subcommand = 0xB9; res.size = sizeof(cmd) / 4; res.client_id = c->lobby_client_id; res.unused = 0; res.item = c->player.identify_result.data; send_command(l, 0x60, 0x00, res); } else { forward_subcommand(l, c, command, flag, data); } } // player accepts the tekk // TODO: I don't know which subcommand id this is; the function should be // correct though so we can just put it in the table when we figure out the id // static void process_subcommand_accept_identified_item(shared_ptr s, // 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 (cmd->client_id != c->lobby_client_id) { // return; // } // // size_t x = c->player.inventory.find_item(cmd->item_id); // c->player.inventory.items[x] = c->player.identify_result; // // TODO: what do we send to the other clients? anything? // // } else { // forward_subcommand(l, c, command, flag, data); // } // } //////////////////////////////////////////////////////////////////////////////// static void process_subcommand_forward_check_size(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); forward_subcommand(l, c, command, flag, data); } static void process_subcommand_forward_check_game(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (!l->is_game()) { return; } forward_subcommand(l, c, command, flag, data); } static void process_subcommand_forward_check_game_loading(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (!l->is_game() || !l->any_client_loading()) { return; } forward_subcommand(l, c, command, flag, data); } static void process_subcommand_forward_check_size_client(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); if (p->byte[2] != c->lobby_client_id) { return; } forward_subcommand(l, c, command, flag, data); } static void process_subcommand_forward_check_size_game(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); if (!l->is_game()) { return; } forward_subcommand(l, c, command, flag, data); } static void process_subcommand_forward_check_size_ep3_lobby(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); if (l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { return; } forward_subcommand(l, c, command, flag, data); } static void process_subcommand_invalid(shared_ptr, shared_ptr, shared_ptr, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); if (command_is_private(command)) { log(WARNING, "Invalid subcommand: %02hhX (private to %hhu)", p->byte[0], flag); } else { log(WARNING, "Invalid subcommand: %02hhX (public)", p->byte[0]); } } static void process_subcommand_unimplemented(shared_ptr, shared_ptr, shared_ptr, uint8_t command, uint8_t flag, const string& data) { const auto* p = check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); if (command_is_private(command)) { log(WARNING, "Unknown subcommand: %02hhX (private to %hhu)", p->byte[0], flag); } else { log(WARNING, "Unknown subcommand: %02hhX (public)", p->byte[0]); } } //////////////////////////////////////////////////////////////////////////////// // Subcommands are described by four fields: the minimum size and maximum size (in DWORDs), // the handler function, and flags that tell when to allow the command. See command-input-subs.h // for more information on flags. The maximum size is not enforced if it's zero. typedef void (*subcommand_handler_t)(shared_ptr s, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data); subcommand_handler_t subcommand_handlers[0x100] = { /* 00 */ process_subcommand_invalid, /* 01 */ process_subcommand_unimplemented, /* 02 */ process_subcommand_unimplemented, /* 03 */ process_subcommand_unimplemented, /* 04 */ process_subcommand_unimplemented, /* 05 */ process_subcommand_switch_state_changed, /* 06 */ process_subcommand_send_guild_card, /* 07 */ process_subcommand_symbol_chat, /* 08 */ process_subcommand_unimplemented, /* 09 */ process_subcommand_unimplemented, /* 0A */ process_subcommand_enemy_hit, /* 0B */ process_subcommand_forward_check_size_game, /* 0C */ process_subcommand_forward_check_size_game, // Add condition (poison/slow/etc.) /* 0D */ process_subcommand_forward_check_size_game, // Remove condition (poison/slow/etc.) /* 0E */ process_subcommand_unimplemented, /* 0F */ process_subcommand_unimplemented, /* 10 */ process_subcommand_unimplemented, /* 11 */ process_subcommand_unimplemented, /* 12 */ process_subcommand_forward_check_size_game, /* 13 */ process_subcommand_forward_check_size_game, /* 14 */ process_subcommand_forward_check_size_game, /* 15 */ process_subcommand_forward_check_size_game, /* 16 */ process_subcommand_unimplemented, /* 17 */ process_subcommand_forward_check_size_game, /* 18 */ process_subcommand_forward_check_size_game, /* 19 */ process_subcommand_forward_check_size_game, /* 1A */ process_subcommand_unimplemented, /* 1B */ process_subcommand_unimplemented, /* 1C */ process_subcommand_forward_check_size_game, /* 1D */ process_subcommand_unimplemented, /* 1E */ process_subcommand_unimplemented, /* 1F */ process_subcommand_forward_check_size, /* 20 */ process_subcommand_forward_check_size, /* 21 */ process_subcommand_change_area, // Inter-level warp /* 22 */ process_subcommand_forward_check_size_client, // Set player visibility /* 23 */ process_subcommand_forward_check_size_client, // Set player visibility /* 24 */ process_subcommand_forward_check_size_game, /* 25 */ process_subcommand_equip_unequip_item, // Equip item /* 26 */ process_subcommand_equip_unequip_item, // Unequip item /* 27 */ process_subcommand_use_item, /* 28 */ process_subcommand_forward_check_size_game, // Feed MAG /* 29 */ process_subcommand_forward_check_size_game, // Delete item (via bank deposit / sale / feeding MAG) /* 2A */ process_subcommand_drop_item, /* 2B */ process_subcommand_forward_check_size_game, // Create inventory item (e.g. from tekker or bank withdrawal) /* 2C */ process_subcommand_forward_check_size, // Talk to NPC /* 2D */ process_subcommand_forward_check_size, // Done talking to NPC /* 2E */ process_subcommand_unimplemented, /* 2F */ process_subcommand_hit_by_enemy, /* 30 */ process_subcommand_forward_check_size_game, // Level up /* 31 */ process_subcommand_forward_check_size_game, // Medical center /* 32 */ process_subcommand_forward_check_size_game, // Medical center /* 33 */ process_subcommand_forward_check_size_game, // Revive player (only confirmed with moon atomizer) /* 34 */ process_subcommand_unimplemented, /* 35 */ process_subcommand_unimplemented, /* 36 */ process_subcommand_forward_check_game, /* 37 */ process_subcommand_forward_check_size_game, // Photon blast /* 38 */ process_subcommand_unimplemented, /* 39 */ process_subcommand_forward_check_size_game, // Photon blast ready /* 3A */ process_subcommand_forward_check_size_game, /* 3B */ process_subcommand_forward_check_size, /* 3C */ process_subcommand_unimplemented, /* 3D */ process_subcommand_unimplemented, /* 3E */ process_subcommand_forward_check_size, // Stop moving /* 3F */ process_subcommand_forward_check_size, /* 40 */ process_subcommand_forward_check_size, // Walk /* 41 */ process_subcommand_unimplemented, /* 42 */ process_subcommand_forward_check_size, // Run /* 43 */ process_subcommand_forward_check_size_client, /* 44 */ process_subcommand_forward_check_size_client, /* 45 */ process_subcommand_forward_check_size_client, /* 46 */ process_subcommand_forward_check_size_client, /* 47 */ process_subcommand_forward_check_size_client, /* 48 */ process_subcommand_use_technique, /* 49 */ process_subcommand_forward_check_size_client, /* 4A */ process_subcommand_forward_check_size_client, /* 4B */ process_subcommand_hit_by_enemy, /* 4C */ process_subcommand_hit_by_enemy, /* 4D */ process_subcommand_forward_check_size_client, /* 4E */ process_subcommand_forward_check_size_client, /* 4F */ process_subcommand_forward_check_size_client, /* 50 */ process_subcommand_forward_check_size_client, /* 51 */ process_subcommand_unimplemented, /* 52 */ process_subcommand_forward_check_size, // Toggle shop/bank interaction /* 53 */ process_subcommand_forward_check_size_game, /* 54 */ process_subcommand_unimplemented, /* 55 */ process_subcommand_forward_check_size_client, // Intra-map warp /* 56 */ process_subcommand_forward_check_size_client, /* 57 */ process_subcommand_forward_check_size_client, /* 58 */ process_subcommand_forward_check_size_game, /* 59 */ process_subcommand_forward_check_size_game, // Item picked up /* 5A */ process_subcommand_pick_up_item, // Request to pick up item /* 5B */ process_subcommand_unimplemented, /* 5C */ process_subcommand_unimplemented, /* 5D */ process_subcommand_forward_check_size_game, // Drop meseta or stacked item /* 5E */ process_subcommand_forward_check_size_game, // Buy item at shop /* 5F */ process_subcommand_forward_check_size_game, // Drop item from box/enemy /* 60 */ process_subcommand_enemy_drop_item, // Request for item drop (handled by the server on BB) /* 61 */ process_subcommand_forward_check_size_game, // Feed mag /* 62 */ process_subcommand_unimplemented, /* 63 */ process_subcommand_destroy_item, // Destroy an item on the ground (used when too many items have been dropped) /* 64 */ process_subcommand_unimplemented, /* 65 */ process_subcommand_unimplemented, /* 66 */ process_subcommand_forward_check_size_game, // Use star atomizer /* 67 */ process_subcommand_forward_check_size_game, // Create enemy set /* 68 */ process_subcommand_forward_check_size_game, // Telepipe/Ryuker /* 69 */ process_subcommand_forward_check_size_game, /* 6A */ process_subcommand_forward_check_size_game, /* 6B */ process_subcommand_forward_check_game_loading, /* 6C */ process_subcommand_forward_check_game_loading, /* 6D */ process_subcommand_forward_check_game_loading, /* 6E */ process_subcommand_forward_check_game_loading, /* 6F */ process_subcommand_forward_check_game_loading, /* 70 */ process_subcommand_forward_check_game_loading, /* 71 */ process_subcommand_forward_check_game_loading, /* 72 */ process_subcommand_forward_check_game_loading, /* 73 */ process_subcommand_invalid, /* 74 */ process_subcommand_word_select, /* 75 */ process_subcommand_forward_check_size_game, /* 76 */ process_subcommand_forward_check_size_game, // Enemy killed /* 77 */ process_subcommand_forward_check_size_game, // Sync quest data /* 78 */ process_subcommand_unimplemented, /* 79 */ process_subcommand_forward_check_size, // Lobby 14/15 soccer game /* 7A */ process_subcommand_unimplemented, /* 7B */ process_subcommand_unimplemented, /* 7C */ process_subcommand_forward_check_size_game, /* 7D */ process_subcommand_forward_check_size_game, /* 7E */ process_subcommand_unimplemented, /* 7F */ process_subcommand_unimplemented, /* 80 */ process_subcommand_forward_check_size_game, // trigger trap /* 81 */ process_subcommand_unimplemented, /* 82 */ process_subcommand_unimplemented, /* 83 */ process_subcommand_forward_check_size_game, // place trap /* 84 */ process_subcommand_forward_check_size_game, /* 85 */ process_subcommand_forward_check_size_game, /* 86 */ process_subcommand_forward_check_size_game, // Hit destructible wall /* 87 */ process_subcommand_unimplemented, /* 88 */ process_subcommand_forward_check_size_game, /* 89 */ process_subcommand_forward_check_size_game, /* 8A */ process_subcommand_unimplemented, /* 8B */ process_subcommand_unimplemented, /* 8C */ process_subcommand_unimplemented, /* 8D */ process_subcommand_forward_check_size_client, /* 8E */ process_subcommand_unimplemented, /* 8F */ process_subcommand_unimplemented, /* 90 */ process_subcommand_unimplemented, /* 91 */ process_subcommand_forward_check_size_game, /* 92 */ process_subcommand_unimplemented, /* 93 */ process_subcommand_forward_check_size_game, // Timed switch activated /* 94 */ process_subcommand_forward_check_size_game, // Warp (the $warp chat command is implemented using this) /* 95 */ process_subcommand_unimplemented, /* 96 */ process_subcommand_unimplemented, /* 97 */ process_subcommand_unimplemented, /* 98 */ process_subcommand_unimplemented, /* 99 */ process_subcommand_unimplemented, /* 9A */ process_subcommand_forward_check_size_game, // Update player stat ($infhp/$inftp are implemented using this command) /* 9B */ process_subcommand_unimplemented, /* 9C */ process_subcommand_forward_check_size_game, /* 9D */ process_subcommand_unimplemented, /* 9E */ process_subcommand_unimplemented, /* 9F */ process_subcommand_forward_check_size_game, // Episode 2 boss actions /* A0 */ process_subcommand_forward_check_size_game, // Episode 2 boss actions /* A1 */ process_subcommand_unimplemented, /* A2 */ process_subcommand_box_drop_item, // Request for item drop from box (handled by server on BB) /* A3 */ process_subcommand_forward_check_size_game, // Episode 2 boss actions /* A4 */ process_subcommand_unimplemented, /* A5 */ process_subcommand_forward_check_size_game, // Episode 2 boss actions /* A6 */ process_subcommand_forward_check_size, // trade proposal /* A7 */ process_subcommand_unimplemented, /* A8 */ process_subcommand_forward_check_size_game, // Episode 2 boss actions /* A9 */ process_subcommand_forward_check_size_game, // Episode 2 boss actions /* AA */ process_subcommand_forward_check_size_game, // Episode 2 boss actions /* AB */ process_subcommand_forward_check_size_client, // Create lobby chair /* AC */ process_subcommand_unimplemented, /* AD */ process_subcommand_unimplemented, /* AE */ process_subcommand_forward_check_size_client, /* AF */ process_subcommand_forward_check_size_client, // Turn in lobby chair /* B0 */ process_subcommand_forward_check_size_client, // Move in lobby chair /* B1 */ process_subcommand_unimplemented, /* B2 */ process_subcommand_unimplemented, /* B3 */ process_subcommand_unimplemented, /* B4 */ process_subcommand_unimplemented, /* B5 */ process_subcommand_open_shop_or_ep3_unknown, // BB shop request /* B6 */ process_subcommand_unimplemented, // BB shop contents (server->client only) /* B7 */ process_subcommand_unimplemented, // TODO: BB buy shop item /* B8 */ process_subcommand_identify_item, // Accept tekker result /* B9 */ process_subcommand_unimplemented, /* BA */ process_subcommand_unimplemented, /* BB */ process_subcommand_open_bank, // BB Bank request /* BC */ process_subcommand_unimplemented, // BB bank contents (server->client only) /* BD */ process_subcommand_bank_action, /* BE */ process_subcommand_unimplemented, // BB create inventory item (server->client only) /* BF */ process_subcommand_forward_check_size_ep3_lobby, // Ep3 change music, also BB give EXP (BB usage is server->client only) /* C0 */ process_subcommand_unimplemented, /* C1 */ process_subcommand_unimplemented, /* C2 */ process_subcommand_unimplemented, /* C3 */ process_subcommand_drop_stacked_item, // Split stacked item - not sent if entire stack is dropped /* C4 */ process_subcommand_sort_inventory, /* C5 */ process_subcommand_unimplemented, /* C6 */ process_subcommand_unimplemented, /* C7 */ process_subcommand_unimplemented, /* C8 */ process_subcommand_enemy_killed, /* C9 */ process_subcommand_unimplemented, /* CA */ process_subcommand_unimplemented, /* CB */ process_subcommand_unimplemented, /* CC */ process_subcommand_unimplemented, /* CD */ process_subcommand_unimplemented, /* CE */ process_subcommand_unimplemented, /* CF */ process_subcommand_forward_check_size_game, /* D0 */ process_subcommand_unimplemented, /* D1 */ process_subcommand_unimplemented, /* D2 */ process_subcommand_unimplemented, /* D3 */ process_subcommand_unimplemented, /* D4 */ process_subcommand_unimplemented, /* D5 */ process_subcommand_unimplemented, /* D6 */ process_subcommand_unimplemented, /* D7 */ process_subcommand_unimplemented, /* D8 */ process_subcommand_unimplemented, /* D9 */ process_subcommand_unimplemented, /* DA */ process_subcommand_unimplemented, /* DB */ process_subcommand_unimplemented, /* DC */ process_subcommand_unimplemented, /* DD */ process_subcommand_unimplemented, /* DE */ process_subcommand_unimplemented, /* DF */ process_subcommand_unimplemented, /* E0 */ process_subcommand_unimplemented, /* E1 */ process_subcommand_unimplemented, /* E2 */ process_subcommand_unimplemented, /* E3 */ process_subcommand_unimplemented, /* E4 */ process_subcommand_unimplemented, /* E5 */ process_subcommand_unimplemented, /* E6 */ process_subcommand_unimplemented, /* E7 */ process_subcommand_unimplemented, /* E8 */ process_subcommand_unimplemented, /* E9 */ process_subcommand_unimplemented, /* EA */ process_subcommand_unimplemented, /* EB */ process_subcommand_unimplemented, /* EC */ process_subcommand_unimplemented, /* ED */ process_subcommand_unimplemented, /* EE */ process_subcommand_unimplemented, /* EF */ process_subcommand_unimplemented, /* F0 */ process_subcommand_unimplemented, /* F1 */ process_subcommand_unimplemented, /* F2 */ process_subcommand_unimplemented, /* F3 */ process_subcommand_unimplemented, /* F4 */ process_subcommand_unimplemented, /* F5 */ process_subcommand_unimplemented, /* F6 */ process_subcommand_unimplemented, /* F7 */ process_subcommand_unimplemented, /* F8 */ process_subcommand_unimplemented, /* F9 */ process_subcommand_unimplemented, /* FA */ process_subcommand_unimplemented, /* FB */ process_subcommand_unimplemented, /* FC */ process_subcommand_unimplemented, /* FD */ process_subcommand_unimplemented, /* FE */ process_subcommand_unimplemented, /* FF */ process_subcommand_unimplemented, }; void process_subcommand(shared_ptr s, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (data.empty()) { throw runtime_error("game command is empty"); } uint8_t which = static_cast(data[0]); subcommand_handlers[which](s, l, c, command, flag, data); } bool subcommand_is_implemented(uint8_t which) { return subcommand_handlers[which] != process_subcommand_unimplemented; }