commit 5b8c26ccd797f17e9c5b9967a1579e7f59d122d0 Author: Martin Michelsen Date: Wed Oct 31 23:55:32 2018 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..90e29340 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.o +newserv diff --git a/ChatCommands.cc b/ChatCommands.cc new file mode 100644 index 00000000..61ba3705 --- /dev/null +++ b/ChatCommands.cc @@ -0,0 +1,739 @@ +#include "ChatCommands.hh" + +#include +#include +#include +#include +#include + +#include "Server.hh" +#include "Lobby.hh" +#include "Client.hh" +#include "SendCommands.hh" +#include "Text.hh" + +using namespace std; + + +//////////////////////////////////////////////////////////////////////////////// + +vector section_id_to_name({ + u"Viridia", u"Greennill", u"Skyly", u"Bluefull", u"Purplenum", u"Pinkal", + u"Redria", u"Oran", u"Yellowboze", u"Whitill"}); + +unordered_map name_to_section_id({ + {u"viridia", 0}, + {u"greennill", 1}, + {u"skyly", 2}, + {u"bluefull", 3}, + {u"purplenum", 4}, + {u"pinkal", 5}, + {u"redria", 6}, + {u"oran", 7}, + {u"yellowboze", 8}, + {u"whitill", 9}}); + +vector lobby_event_to_name({ + u"none", u"xmas", u"none", u"val", u"easter", u"hallo", u"sonic", + u"newyear", u"spring", u"white", u"wedding", u"fall", u"s-summer", + u"s-spring", u"summer"}); + +unordered_map name_to_lobby_event({ + {u"none", 0}, + {u"xmas", 1}, + {u"val", 3}, + {u"easter", 4}, + {u"hallo", 5}, + {u"sonic", 6}, + {u"newyear", 7}, + {u"spring", 8}, + {u"white", 9}, + {u"wedding", 10}, + {u"fall", 11}, + {u"s-summer", 12}, + {u"s-spring", 13}, + {u"summer", 14}, +}); + +unordered_map lobby_type_to_name({ + {0x00, u"normal"}, + {0x0F, u"inormal"}, + {0x10, u"ipc"}, + {0x11, u"iball"}, + {0x67, u"cave2u"}, + {0xD4, u"cave1"}, + {0xE9, u"planet"}, + {0xEA, u"clouds"}, + {0xED, u"cave"}, + {0xEE, u"jungle"}, + {0xEF, u"forest2-2"}, + {0xF0, u"forest2-1"}, + {0xF1, u"windpower"}, + {0xF2, u"overview"}, + {0xF3, u"seaside"}, + {0xF4, u"some?"}, + {0xF5, u"dmorgue"}, + {0xF6, u"caelum"}, + {0xF8, u"digital"}, + {0xF9, u"boss1"}, + {0xFA, u"boss2"}, + {0xFB, u"boss3"}, + {0xFC, u"dragon"}, + {0xFD, u"derolle"}, + {0xFE, u"volopt"}, + {0xFF, u"darkfalz"}, +}); + +unordered_map name_to_lobby_type({ + {u"normal", 0x00}, + {u"inormal", 0x0F}, + {u"ipc", 0x10}, + {u"iball", 0x11}, + {u"cave1", 0xD4}, + {u"cave2u", 0x67}, + {u"dragon", 0xFC}, + {u"derolle", 0xFD}, + {u"volopt", 0xFE}, + {u"darkfalz", 0xFF}, + {u"planet", 0xE9}, + {u"clouds", 0xEA}, + {u"cave", 0xED}, + {u"jungle", 0xEE}, + {u"forest2-2", 0xEF}, + {u"forest2-1", 0xF0}, + {u"windpower", 0xF1}, + {u"overview", 0xF2}, + {u"seaside", 0xF3}, + {u"some?", 0xF4}, + {u"dmorgue", 0xF5}, + {u"caelum", 0xF6}, + {u"digital", 0xF8}, + {u"boss1", 0xF9}, + {u"boss2", 0xFA}, + {u"boss3", 0xFB}, + {u"knight", 0xFC}, + {u"sky", 0xFE}, + {u"morgue", 0xFF}, +}); + +vector tech_id_to_name({ + u"foie", u"gifoie", u"rafoie", + u"barta", u"gibarta", u"rabarta", + u"zonde", u"gizonde", u"razonde", + u"grants", u"deband", u"jellen", u"zalure", u"shifta", + u"ryuker", u"resta", u"anti", u"reverser", u"megid"}); + +unordered_map name_to_tech_id({ + {u"foie", 0}, + {u"gifoie", 1}, + {u"rafoie", 2}, + {u"barta", 3}, + {u"gibarta", 4}, + {u"rabarta", 5}, + {u"zonde", 6}, + {u"gizonde", 7}, + {u"razonde", 8}, + {u"grants", 9}, + {u"deband", 10}, + {u"jellen", 11}, + {u"zalure", 12}, + {u"shifta", 13}, + {u"ryuker", 14}, + {u"resta", 15}, + {u"anti", 16}, + {u"reverser", 17}, + {u"megid", 18}, +}); + +vector npc_id_to_name({ + u"ninja", u"rico", u"sonic", u"knuckles", u"tails", u"flowen", u"elly"}); + +unordered_map name_to_npc_id({ + {u"ninja", 0}, + {u"rico", 1}, + {u"sonic", 2}, + {u"knuckles", 3}, + {u"tails", 4}, + {u"flowen", 5}, + {u"elly", 6}}); + + + +//////////////////////////////////////////////////////////////////////////////// +// Checks + + + +static void check_privileges(shared_ptr c, uint64_t mask) { + if (!c->license) { + throw runtime_error("not logged in"); + } + if ((c->license->privileges & mask) != mask) { + throw runtime_error("insufficient permissions"); + } +} + +static void check_version(shared_ptr c, GameVersion version) { + if (c->version != version) { + throw runtime_error("incorrect version"); + } +} + +static void check_not_version(shared_ptr c, GameVersion version) { + if (c->version == version) { + throw runtime_error("incorrect version"); + } +} + +static void check_is_game(shared_ptr l, bool is_game) { + if (l->is_game() != is_game) { + throw runtime_error(is_game ? "can only be used in games" : "can only be used in lobbies"); + } +} + +static void check_cheats_enabled(shared_ptr l) { + if (!(l->flags & LobbyFlag::CheatsEnabled)) { + throw runtime_error("can only be used in cheat mode"); + } +} + +static void check_is_leader(shared_ptr l, shared_ptr c) { + if (l->leader_id != c->lobby_client_id) { + throw runtime_error("you are not the game leader"); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +// Message commands + +static void command_lobby_info(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + // no preconditions - everyone can use this command + + string buffer; + if (l->is_game()) { + string level_string; + if (l->max_level == 0xFFFFFFFF) { + level_string = string_printf("Levels: %d+", l->min_level + 1); + } else { + level_string = string_printf("Levels: %d-%d", l->min_level + 1, l->max_level + 1); + } + + send_text_message_printf(c, "$C6Lobby ID: %08X\n%s\nSection ID: %s\nCheat mode: %s", + l->lobby_id, level_string.c_str(), + section_id_to_name.at(l->section_id).c_str(), + (l->flags & LobbyFlag::CheatsEnabled) ? u"on" : u"off"); + + } else { + send_text_message_printf(c, "$C6Lobby ID: %08X", l->lobby_id); + } + +} + +static void command_ax(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_privileges(c, Privilege::Announce); + log(INFO, "[$ax from %010u] %S\n", c->license->serial_number, args); +} + +static void command_announce(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_privileges(c, Privilege::Announce); + // TODO: implement this +} + +static void command_arrow(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + // no preconditions + + c->lobby_arrow_color = stoull(encode_sjis(args), NULL, 0); + if (!l->is_game()) { + send_arrow_update(l); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Lobby commands + +static void command_cheat(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_is_leader(l, c); + + l->flags ^= LobbyFlag::CheatsEnabled; + send_text_message_printf(l, "Cheat mode %s", + (l->flags & LobbyFlag::CheatsEnabled) ? u"enabled" : u"disabled"); + + // if cheat mode was disabled, turn off all the cheat features that were on + if (!(l->flags & LobbyFlag::CheatsEnabled)) { + rw_guard g(l->lock, true); + for (size_t x = 0; x < l->max_clients; x++) { + auto c = l->clients[x]; + if (!c) { + continue; + } + c->infinite_hp = false; + c->infinite_tp = false; + } + memset(&l->next_drop_item, 0, sizeof(l->next_drop_item)); + } +} + +static void command_lobby_event(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, false); + check_privileges(c, Privilege::ChangeEvent); + + { + rw_guard g(l->lock, true); + l->event = name_to_lobby_event.at(args); + } + send_command(l, 0xDA, l->event, NULL, 0); +} + +static void command_lobby_event_all(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_privileges(c, Privilege::ChangeEvent); + + uint8_t event = name_to_lobby_event.at(args); + + for (auto l : s->all_lobbies()) { + if (l->is_game() || !(l->flags & LobbyFlag::Default)) { + continue; + } + + { + rw_guard g(l->lock, true); + l->event = event; + } + send_command(l, 0xDA, event, NULL, 0); + } +} + +static void command_lobby_type(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, false); + check_privileges(c, Privilege::ChangeEvent); + + { + rw_guard g(l->lock, true); + l->type = name_to_lobby_type.at(args); + if (l->type < ((l->flags & LobbyFlag::Episode3) ? 20 : 15)) { + l->type = l->block - 1; + } + } + + rw_guard g(l->lock, false); + for (size_t x = 0; x < l->max_clients; x++) { + if (l->clients[x]) { + send_join_lobby(l->clients[x], l); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Game commands + +static void command_password(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_is_leader(l, c); + + if (!args[0]) { + l->password[0] = 0; + send_text_message(l, u"$C6Game unlocked"); + + } else { + char16cpy(l->password, args, 0x10); + auto encoded = encode_sjis(l->password); + send_text_message_printf(l, "$C6Game locked with password:\n%s", + encoded.c_str()); + } +} + +static void command_min_level(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_is_leader(l, c); + + u16string buffer; + { + rw_guard g(l->lock, true); + l->min_level = stoull(encode_sjis(args)) - 1; + } + send_text_message_printf(l, "$C6Minimum level set to %" PRIu32, + l->min_level + 1); +} + +static void command_max_level(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_is_leader(l, c); + + { + rw_guard g(l->lock, true); + l->max_level = stoull(encode_sjis(args)) - 1; + if (l->max_level >= 200) { + l->max_level = 0xFFFFFFFF; + } + } + + if (l->max_level == 0xFFFFFFFF) { + send_text_message(l, u"$C6Maximum level set to unlimited"); + } else { + send_text_message_printf(l, "$C6Maximum level set to %" PRIu32, l->max_level + 1); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Character commands + +static void command_edit(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, false); + check_version(c, GameVersion::BB); + + string encoded_args = encode_sjis(args); + vector tokens = split(encoded_args, L' '); + + if (tokens.size() < 3) { + send_text_message(c, u"$C6Not enough arguments"); + return; + } + + if (tokens[0] == "atp") { + c->player.disp.stats.atp = stoul(tokens[1]); + } else if (tokens[0] == "mst") { + c->player.disp.stats.mst = stoul(tokens[1]); + } else if (tokens[0] == "evp") { + c->player.disp.stats.evp = stoul(tokens[1]); + } else if (tokens[0] == "hp") { + c->player.disp.stats.hp = stoul(tokens[1]); + } else if (tokens[0] == "dfp") { + c->player.disp.stats.dfp = stoul(tokens[1]); + } else if (tokens[0] == "ata") { + c->player.disp.stats.ata = stoul(tokens[1]); + } else if (tokens[0] == "lck") { + c->player.disp.stats.lck = stoul(tokens[1]); + } else if (tokens[0] == "meseta") { + c->player.disp.meseta = stoul(tokens[1]); + } else if (tokens[0] == "exp") { + c->player.disp.experience = stoul(tokens[1]); + } else if (tokens[0] == "level") { + c->player.disp.level = stoul(tokens[1]) - 1; + } else if (tokens[0] == "namecolor") { + sscanf(tokens[1].c_str(), "%8X", &c->player.disp.name_color); + } else if (tokens[0] == "secid") { + c->player.disp.section_id = name_to_section_id.at(decode_sjis(tokens[1])); + } else if (tokens[0] == "name") { + decode_sjis(c->player.disp.name, tokens[1].c_str(), 0x10); + add_language_marker_inplace(c->player.disp.name, u'J', 0x10); + } else if (tokens[0] == "npc") { + if (tokens[1] == "none") { + c->player.disp.extra_model = 0; + c->player.disp.v2_flags &= 0xFD; + } else { + c->player.disp.extra_model = name_to_npc_id.at(decode_sjis(tokens[1])); + c->player.disp.v2_flags |= 0x02; + } + } else if ((tokens[0] == "tech") && (tokens.size() > 2)) { + uint8_t level = stoul(tokens[2]) - 1; + if (tokens[1] == "all") { + for (size_t x = 0; x < 0x14; x++) { + c->player.disp.technique_levels[x] = level; + } + } else { + uint8_t tech_id = name_to_tech_id.at(decode_sjis(tokens[1])); + c->player.disp.technique_levels[tech_id] = level; + } + } else { + send_text_message(c, u"$C6Unknown field"); + return; + } + + // reload the client in the lobby/game + send_player_leave_notification(l, c->lobby_client_id); + send_complete_player_bb(c); + s->send_lobby_join_notifications(l, c); +} + +static void command_change_bank(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_version(c, GameVersion::BB); + + // TODO: implement this + // TODO: make sure the bank name is filesystem-safe +} + +static void command_convert_char_to_bb(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, false); + check_not_version(c, GameVersion::BB); + + vector tokens = split(encode_sjis(args), L' '); + if (tokens.size() != 3) { + send_text_message(c, u"$C6Incorrect argument count"); + return; + } + + // username/password are tokens[0] and [1] + c->pending_bb_save_player_index = stoul(tokens[2]) - 1; + if (c->pending_bb_save_player_index > 3) { + send_text_message(c, u"$C6Player index must be 1-4"); + return; + } + + try { + s->license_manager->verify_bb(tokens[0].c_str(), tokens[1].c_str()); + } catch (const exception& e) { + send_text_message_printf(c, "$C6Login failed: %s", e.what()); + return; + } + + c->pending_bb_save_username = tokens[0]; + + // request the player data. the client will respond with a 61, and the handler + // for that command will execute the conversion + send_command(c, 0x95, 0x00); +} + +//////////////////////////////////////////////////////////////////////////////// +// Administration commands + +static void command_silence(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_privileges(c, Privilege::SilenceUser); + + auto target = s->find_client(args); + if (!target->license) { + // this should be impossible, but I'll bet it's not actually + send_text_message(c, u"$C6Client not logged in"); + return; + } + + if (target->license->privileges & Privilege::Moderator) { + send_text_message(c, u"$C6You do not have\nsufficient privileges."); + return; + } + + rw_guard g(target->lock, true); + target->can_chat = !target->can_chat; + send_text_message_printf(l, "$C6%s %ssilenced", target->player.disp.name, + target->can_chat ? "un" : ""); +} + +static void command_kick(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_privileges(c, Privilege::KickUser); + + auto target = s->find_client(args); + if (!target->license) { + // this should be impossible, but I'll bet it's not actually + send_text_message(c, u"$C6Client not logged in"); + return; + } + + if (target->license->privileges & Privilege::Moderator) { + send_text_message(c, u"$C6You do not have\nsufficient privileges."); + return; + } + + send_message_box(target, u"$C6You were kicked off by a moderator."); + target->should_disconnect = true; + send_text_message_printf(l, "$C6%s kicked off", target->player.disp.name); +} + +static void command_ban(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_privileges(c, Privilege::BanUser); + + u16string args_str(args); + size_t space_pos = args_str.find(L' '); + if (space_pos == string::npos) { + send_text_message(c, u"$C6Incorrect argument count"); + return; + } + + auto target = s->find_client(args_str.data() + space_pos + 1); + if (!target->license) { + // this should be impossible, but I'll bet it's not actually + send_text_message(c, u"$C6Client not logged in"); + return; + } + + if (target->license->privileges & Privilege::BanUser) { + send_text_message(c, u"$C6You do not have\nsufficient privileges."); + return; + } + + uint64_t usecs = stoull(encode_sjis(args), NULL, 0) * 1000000; + + size_t unit_offset = 0; + for (; isdigit(args[unit_offset]); unit_offset++); + if (args[unit_offset] == 'm') { + usecs *= 60; + } else if (args[unit_offset] == 'h') { + usecs *= 60 * 60; + } else if (args[unit_offset] == 'd') { + usecs *= 60 * 60 * 24; + } else if (args[unit_offset] == 'w') { + usecs *= 60 * 60 * 24 * 7; + } else if (args[unit_offset] == 'M') { + usecs *= 60 * 60 * 24 * 30; + } else if (args[unit_offset] == 'y') { + usecs *= 60 * 60 * 24 * 365; + } + + // TODO: put the length of time in this message. or don't; presumably the + // person deserved it + s->license_manager->ban_until(target->license->serial_number, now() + usecs); + send_message_box(target, u"$C6You were banned by a moderator."); + target->should_disconnect = true; + auto encoded_name = encode_sjis(target->player.disp.name); + send_text_message_printf(l, "$C6%s banned", encoded_name.c_str()); +} + +//////////////////////////////////////////////////////////////////////////////// +// Cheat commands + +static void command_warp(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_cheats_enabled(l); + + uint32_t area = stoul(encode_sjis(args), NULL, 0); + if (!l->episode || (l->episode > 3)) { + return; + } + if (c->area == area) { + return; + } + + if ((l->episode == 1) && (area > 17)) { + send_text_message(c, u"$C6Area numbers must be\n17 or less."); + return; + } + if ((l->episode == 2) && (area > 17)) { + send_text_message(c, u"$C6Area numbers must be\n17 or less."); + return; + } + if ((l->episode == 3) && (area > 10)) { + send_text_message(c, u"$C6Area numbers must be\n10 or less."); + return; + } + + send_warp(c, area); +} + +static void command_infinite_hp(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_cheats_enabled(l); + + c->infinite_hp = !c->infinite_hp; + send_text_message_printf(c, "$C6Infinite HP %s", c->infinite_hp ? "enabled" : "disabled"); +} + +static void command_infinite_tp(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_cheats_enabled(l); + + c->infinite_tp = !c->infinite_tp; + send_text_message_printf(c, "$C6Infinite TP %s", c->infinite_tp ? "enabled" : "disabled"); +} + +static void command_item(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, true); + check_cheats_enabled(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."); + return; + } + if (data.size() > 16) { + send_text_message(c, u"$C6Item codes must be\n16 bytes or fewer."); + return; + } + + ItemData item_data; + memset(&item_data, 0, sizeof(item_data)); + if (data.size() < 12) { + memcpy(&l->next_drop_item.data.item_data1, data.data(), data.size()); + } else { + memcpy(&l->next_drop_item.data.item_data1, data.data(), 12); + memcpy(&l->next_drop_item.data.item_data2, data.data() + 12, 12 - data.size()); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// + +typedef void (*handler_t)(shared_ptr s, shared_ptr l, + shared_ptr c, const char16_t* args); +struct ChatCommandDefinition { + handler_t handler; + const char16_t* usage; +}; + +static const unordered_map chat_commands({ + {u"allevent" , {command_lobby_event_all , u"usage:\nallevent "}}, + {u"ann" , {command_announce , u"usage:\nann "}}, + {u"arrow" , {command_arrow , u"usage:\narrow "}}, + {u"ax" , {command_ax , u"usage:\nax "}}, + {u"ban" , {command_ban , u"usage:\nban "}}, + {u"bbchar" , {command_convert_char_to_bb, u"usage:\nbbchar <1-4>"}}, + {u"changebank", {command_change_bank , u"usage:\nchangebank "}}, + {u"cheat" , {command_cheat , u"usage:\nduh"}}, + {u"edit" , {command_edit , u"usage:\nedit "}}, + {u"event" , {command_lobby_event , u"usage:\nevent "}}, + {u"infhp" , {command_infinite_hp , u"usage:\nduh"}}, + {u"inftp" , {command_infinite_tp , u"usage:\nduh"}}, + {u"item" , {command_item , u"usage:\nitem "}}, + {u"kick" , {command_kick , u"usage:\nkick "}}, + {u"li" , {command_lobby_info , u"usage:\nli"}}, + {u"password" , {command_password , u"usage:\nlock [password]\nomit password to\nunlock game"}}, + {u"maxlevel" , {command_max_level , u"usage:\nmax_level "}}, + {u"minlevel" , {command_min_level , u"usage:\nmin_level "}}, + {u"silence" , {command_silence , u"usage:\nsilence "}}, + {u"type" , {command_lobby_type , u"usage:\ntype "}}, + {u"warp" , {command_warp , u"usage:\nwarp "}}, +}); + +// this function is called every time any player sends a chat beginning with a dollar sign. +// It is this function's responsibility to see if the chat is a command, and to +// execute the command and block the chat if it is. +void process_chat_command(std::shared_ptr s, std::shared_ptr l, + std::shared_ptr c, const char16_t* text) { + + u16string command_name; + u16string text_str(text); + size_t space_pos = text_str.find(L' '); + if (space_pos != string::npos) { + command_name = text_str.substr(0, space_pos); + text_str = text_str.substr(space_pos + 1); + } else { + command_name = text_str; + text_str.clear(); + } + + const ChatCommandDefinition* def = NULL; + try { + def = &chat_commands.at(command_name); + } catch (const out_of_range&) { + send_text_message(c, u"$C6Unknown command."); + return; + } + + try { + def->handler(s, l, c, text_str.c_str()); + } catch (const exception& e) { + send_text_message_printf(c, "$C6Failed:\n%s", e.what()); + return; + } +} diff --git a/ChatCommands.hh b/ChatCommands.hh new file mode 100644 index 00000000..e8cc91af --- /dev/null +++ b/ChatCommands.hh @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include "ServerState.hh" +#include "Lobby.hh" +#include "Client.hh" + +void process_chat_command(std::shared_ptr s, std::shared_ptr l, + std::shared_ptr c, const char16_t* text); diff --git a/Client.cc b/Client.cc new file mode 100644 index 00000000..9314a76c --- /dev/null +++ b/Client.cc @@ -0,0 +1,47 @@ +#include "Client.hh" + +#include +#include +#include +#include +#include + +#include +#include + +#include "Version.hh" + +using namespace std; + + + +Client::Client(struct bufferevent* bev, GameVersion version, + ServerBehavior server_behavior) : version(version), + flags(flags_for_version(version, 0)), bev(bev), + server_behavior(server_behavior), should_disconnect(false), + play_time_begin(now()), last_recv_time(this->play_time_begin), + last_send_time(0), in_information_menu(false), area(0), lobby_id(0), + lobby_client_id(0), lobby_arrow_color(0), next_exp_value(0), + infinite_hp(false), infinite_tp(false), can_chat(true) { + + int fd = bufferevent_getfd(this->bev); + get_socket_addresses(fd, &this->local_addr, &this->remote_addr); + memset(this->name, 0, sizeof(this->name)); + memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr)); +} + +bool Client::send(string&& data) { + rw_guard g(this->lock, false); + + if (!this->bev) { + return false; + } + + if (this->crypt_out.get()) { + this->crypt_out->encrypt(const_cast(data.data()), data.size()); + } + + struct evbuffer* buf = bufferevent_get_output(this->bev); + evbuffer_add(buf, data.data(), data.size()); + return true; +} diff --git a/Client.hh b/Client.hh new file mode 100644 index 00000000..f3b65d0a --- /dev/null +++ b/Client.hh @@ -0,0 +1,85 @@ +#pragma once + +#include + +#include +#include + +#include "License.hh" +#include "Player.hh" +#include "PSOEncryption.hh" + + + +enum class ServerBehavior { + SplitReconnect = 0, + LoginServer, + LobbyServer, + DataServerBB, + PatchServer, +}; + +struct ClientConfig { + uint32_t magic; // must be set to 0x48615467 + uint8_t bb_game_state; // status of client connecting on BB + uint8_t bb_player_index; // selected char + uint16_t flags; // just in case we lose them somehow between connections + uint16_t ports[4]; // used by shipgate clients + uint32_t unused[4]; +}; + +struct ClientConfigBB { + ClientConfig cfg; + uint32_t unused[2]; +}; + +struct Client { + rw_lock lock; + + // license & account + std::shared_ptr license; + char16_t name[0x20]; + ClientConfigBB config; + GameVersion version; + uint16_t flags; + + // encryption + std::unique_ptr crypt_in; + std::unique_ptr crypt_out; + + // network + struct sockaddr_storage local_addr; + struct sockaddr_storage remote_addr; + struct bufferevent* bev; + struct sockaddr_storage next_connection_addr; + ServerBehavior server_behavior; + bool should_disconnect; + std::string recv_buffer; + + // timing & menus + uint64_t play_time_begin; // time of connection (used for incrementing play time on BB) + uint64_t last_recv_time; // time of last data received + uint64_t last_send_time; // time of last data sent + bool in_information_menu; + + // lobby/positioning + uint32_t area; // which area is the client in? + uint32_t lobby_id; // which lobby is this person in? + uint8_t lobby_client_id; // which client number is this person? + uint8_t lobby_arrow_color; // lobby arrow color ID + Player player; + + // miscellaneous (used by chat commands) + uint32_t next_exp_value; // next EXP value to give + bool infinite_hp; // cheats enabled + bool infinite_tp; // cheats enabled + bool can_chat; + std::string pending_bb_save_username; + uint8_t pending_bb_save_player_index; + + Client(struct bufferevent* bev, GameVersion version, + ServerBehavior server_behavior); + + // adds data to the client's output buffer, encrypting it first + bool send(std::string&& data); +}; diff --git a/Compression.cc b/Compression.cc new file mode 100644 index 00000000..9bedd4c0 --- /dev/null +++ b/Compression.cc @@ -0,0 +1,260 @@ +#include "Compression.hh" + +#include +#include +#include +#include +#include + +#include + +using namespace std; + + + +struct prs_compress_ctx { + unsigned char bitpos; + std::string forward_log; + std::string output; + + prs_compress_ctx() : bitpos(0) { } + + string finish() { + this->put_control_bit(0); + this->put_control_bit(1); + if (this->bitpos != 0) { + this->forward_log[0] = ((this->forward_log[0] << this->bitpos) >> 8); + } + this->put_static_data(0); + this->put_static_data(0); + this->output += this->forward_log; + this->forward_log.clear(); + return this->output; + } + + void put_control_bit_nosave(bool bit) { + this->forward_log[0] = this->forward_log[0] >> 1; + this->forward_log[0] |= ((!!bit) << 7); + this->bitpos++; + } + + void put_control_save() { + if (this->bitpos >= 8) { + this->bitpos = 0; + this->output += this->forward_log; + this->forward_log.resize(1); + this->forward_log[0] = 0; + } + } + + void put_control_bit(bool bit) { + this->put_control_bit_nosave(bit); + this->put_control_save(); + } + + void put_static_data(uint8_t data) { + this->forward_log += static_cast(data); + } + + void raw_byte(uint8_t value) { + this->put_control_bit_nosave(1); + this->put_static_data(value); + this->put_control_save(); + } + + void short_copy(ssize_t offset, uint8_t size) { + size -= 2; + this->put_control_bit(0); + this->put_control_bit(0); + this->put_control_bit((size >> 1) & 1); + this->put_control_bit_nosave(size & 1); + this->put_static_data(offset & 0xFF); + this->put_control_save(); + } + + void long_copy(ssize_t offset, uint8_t size) { + if (size <= 9) { + this->put_control_bit(0); + this->put_control_bit_nosave(1); + this->put_static_data(((offset << 3) & 0xF8) | ((size - 2) & 0x07)); + this->put_static_data((offset >> 5) & 0xFF); + this->put_control_save(); + } else { + this->put_control_bit(0); + this->put_control_bit_nosave(1); + this->put_static_data((offset << 3) & 0xF8); + this->put_static_data((offset >> 5) & 0xFF); + this->put_static_data(size - 1); + this->put_control_save(); + } + } + + void copy(ssize_t offset, uint8_t size) { + if ((offset > -0x100) && (size <= 5)) { + this->short_copy(offset, size); + } else { + this->long_copy(offset, size); + } + } +}; + +string prs_compress(const string& data) { + prs_compress_ctx pc; + + ssize_t read_offset = 0; + while (read_offset < data.size()) { + + // look for a chunk of data in history matching what's at the current offset + ssize_t best_offset = 0; + size_t best_size = 0; + for (ssize_t this_offset = -3; + (this_offset + data.size() >= 0) && + (this_offset > -0x1FF0) && + (best_size < 255); + this_offset--) { + + // for this offset, expand the match as much as possible + size_t this_size = 1; + while ((this_size < 0x100) && // max copy size is 255 bytes + ((this_offset + this_size) < 0) && // don't copy past the read offset + (this_size <= data.size() - read_offset) && // don't copy past the end + !memcmp(data.data() + read_offset + this_offset, + data.data() + read_offset, this_size)) { + this_size++; + } + this_size--; + + if (this_size > best_size) { + best_offset = this_offset; + best_size = this_size; + } + } + + // if there are no good matches, write the byte directly + if (best_size < 3) { + pc.raw_byte(data[read_offset]); + read_offset++; + + } else { + pc.copy(best_offset, best_size); + read_offset += best_size; + } + } + + return pc.finish(); +} + + + +static int16_t get_u8_or_eof(StringReader& r) { + return r.eof() ? -1 : r.get_u8(); +} + +string prs_decompress(const string& data, size_t max_size) { + string output; + StringReader r(data.data(), data.size()); + + int32_t r3, r5; + int bitpos = 9; + int16_t currentbyte; // int16_t because it can be -1 when EOF occurs + int flag; + int offset; + unsigned long x, t; + + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output; + } + + for (;;) { + bitpos--; + if (bitpos == 0) { + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output; + } + bitpos = 8; + } + flag = currentbyte & 1; + currentbyte = currentbyte >> 1; + if (flag) { + int ch = get_u8_or_eof(r); + if (ch == EOF) { + return output; + } + output += static_cast(ch); + if (max_size && (output.size() > max_size)) { + throw runtime_error("maximumoutput size exceeded"); + } + continue; + } + bitpos--; + if (bitpos == 0) { + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output; + } + bitpos = 8; + } + flag = currentbyte & 1; + currentbyte = currentbyte >> 1; + if (flag) { + r3 = get_u8_or_eof(r); + if (r3 == EOF) { + return output; + } + int high_byte = get_u8_or_eof(r); + if (high_byte == EOF) { + return output; + } + offset = ((high_byte & 0xFF) << 8) | (r3 & 0xFF); + if (offset == 0) { + return output; + } + r3 = r3 & 0x00000007; + r5 = (offset >> 3) | 0xFFFFE000; + if (r3 == 0) { + flag = 0; + r3 = get_u8_or_eof(r); + if (r3 == EOF) { + return output; + } + r3 = (r3 & 0xFF) + 1; + } else { + r3 += 2; + } + } else { + r3 = 0; + for (x = 0; x < 2; x++) { + bitpos--; + if (bitpos == 0) { + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output; + } + bitpos = 8; + } + flag = currentbyte & 1; + currentbyte = currentbyte >> 1; + offset = r3 << 1; + r3 = offset | flag; + } + offset = get_u8_or_eof(r); + if (offset == EOF) { + return output; + } + r3 += 2; + r5 = offset | 0xFFFFFF00; + } + if (r3 == 0) { + continue; + } + t = r3; + for (x = 0; x < t; x++) { + output += output.at(output.size() + r5); + if (max_size && (output.size() > max_size)) { + throw runtime_error("maximum output size exceeded"); + } + } + } +} diff --git a/Compression.hh b/Compression.hh new file mode 100644 index 00000000..790e86ca --- /dev/null +++ b/Compression.hh @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include + + + +std::string prs_compress(const std::string& data); +std::string prs_decompress(const std::string& data, size_t max_size = 0); diff --git a/DNSServer.cc b/DNSServer.cc new file mode 100644 index 00000000..79f5bd53 --- /dev/null +++ b/DNSServer.cc @@ -0,0 +1,131 @@ +#include "DNSServer.hh" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "NetworkAddresses.hh" + +using namespace std; + + + +DNSServer::DNSServer(uint32_t local_connect_address, + uint32_t external_connect_address) : + should_exit(false), local_connect_address(local_connect_address), + external_connect_address(external_connect_address) { } + +DNSServer::~DNSServer() { + for (int fd : this->fds) { + close(fd); + } +} + +void DNSServer::listen(const std::string& socket_path) { + this->add_socket(::listen(socket_path, 0, 0)); +} + +void DNSServer::listen(const std::string& addr, int port) { + this->add_socket(::listen(addr, port, 0)); +} + +void DNSServer::listen(int port) { + this->add_socket(::listen("", port, 0)); +} + +void DNSServer::add_socket(int fd) { + this->fds.emplace(fd); +} + +void DNSServer::start() { + this->t = thread(&DNSServer::run_thread, this); +} + +void DNSServer::schedule_stop() { + this->should_exit = true; +} + +void DNSServer::wait_for_stop() { + this->t.join(); +} + +void DNSServer::run_thread() { + vector poll_fds; + for (int fd : this->fds) { + poll_fds.emplace_back(); + auto& pfd = poll_fds.back(); + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + } + + while (!this->should_exit) { + sockaddr_in remote; + socklen_t remote_size = sizeof(sockaddr_in); + memset(&remote, 0, remote_size); + + // 10 second timeout + int num_fds = poll(poll_fds.data(), poll_fds.size(), 10000); + if (num_fds < 0) { + auto s = string_for_error(errno); + log(ERROR, "DNS server terminating due to error: %s", s.c_str()); + break; + } + + if (num_fds == 0) { + continue; + } + + for (const auto& pfd : poll_fds) { + if (!(pfd.revents & POLLIN)) { + continue; + } + + string input(2048, 0); + ssize_t bytes = recvfrom(pfd.fd, const_cast(input.data()), + input.size(), 0, reinterpret_cast(&remote), &remote_size); + if (bytes > 0) { + input.resize(bytes); + + uint32_t connect_address; + if (is_local_address(remote.sin_addr.s_addr)) { + connect_address = this->local_connect_address; + } else { + connect_address = this->external_connect_address; + } + + string output = this->build_response(input, connect_address); + if (!output.empty()) { + sendto(pfd.fd, output.data(), output.size(), 0, + reinterpret_cast(&remote), remote_size); + } + } + } + } +} + + +string DNSServer::build_response(const std::string& input, + uint32_t connect_address) { + + if (input.size() < 0x0C) { + return ""; + } + + string ret; + size_t name_len = strlen(input.data() + 0x0C) + 1; + + ret.append(input.substr(0, 2)); + ret.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10); + ret.append(input.substr(12, name_len)); + ret.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16); + ret.append(reinterpret_cast(&connect_address), 4); + + return ret; +} diff --git a/DNSServer.hh b/DNSServer.hh new file mode 100644 index 00000000..b7c21da5 --- /dev/null +++ b/DNSServer.hh @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + + +class DNSServer { +public: + DNSServer(uint32_t local_connect_address, uint32_t external_connect_address); + DNSServer(const DNSServer&) = delete; + DNSServer(DNSServer&&) = delete; + virtual ~DNSServer(); + + void listen(const std::string& socket_path); + void listen(const std::string& addr, int port); + void listen(int port); + void add_socket(int fd); + + virtual void start(); + virtual void schedule_stop(); + virtual void wait_for_stop(); + +private: + std::atomic should_exit; + std::thread t; + + std::set fds; + + uint32_t local_connect_address; + uint32_t external_connect_address; + + void run_thread(); + static std::string build_response(const std::string& input, + uint32_t connect_address); +}; diff --git a/FileContentsCache.cc b/FileContentsCache.cc new file mode 100644 index 00000000..82a800de --- /dev/null +++ b/FileContentsCache.cc @@ -0,0 +1,40 @@ +#include "FileContentsCache.hh" + +#include + +#include +#include + +using namespace std; + + +FileContentsCache::File::File(const string& name, shared_ptr contents, + uint64_t load_time) : name(name), contents(contents), load_time(load_time) { } + +shared_ptr FileContentsCache::get(const std::string& name) { + + uint64_t t = now(); + try { + lock_guard g(this->lock); + auto& entry = this->name_to_file.at(name); + if (t - entry.load_time < 300000000) { // not 5 minutes old? return it + return entry.contents; + } + } catch (const out_of_range& e) { } + + shared_ptr contents(new string(load_file(name))); + + { + lock_guard g(this->lock); + this->name_to_file.erase(name); + this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name), + forward_as_tuple(name, contents, t)); + } + + return contents; + +} + +shared_ptr FileContentsCache::get(const char* name) { + return this->get(string(name)); +} diff --git a/FileContentsCache.hh b/FileContentsCache.hh new file mode 100644 index 00000000..ffa0ead6 --- /dev/null +++ b/FileContentsCache.hh @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include + +using namespace std; + + +class FileContentsCache { +private: + struct File { + std::string name; + std::shared_ptr contents; + uint64_t load_time; + + File() = delete; + File(const std::string& name, std::shared_ptr contents, + uint64_t load_time); + File(const File&) = delete; + File(File&&) = delete; + File& operator=(const File&) = delete; + File& operator=(File&&) = delete; + ~File() = default; + }; + +public: + FileContentsCache() = default; + FileContentsCache(const FileContentsCache&) = delete; + FileContentsCache(FileContentsCache&&) = delete; + FileContentsCache& operator=(const FileContentsCache&) = delete; + FileContentsCache& operator=(FileContentsCache&&) = delete; + ~FileContentsCache() = default; + + std::shared_ptr get(const std::string& name); + std::shared_ptr get(const char* name); + +private: + std::unordered_map name_to_file; + mutable std::mutex lock; +}; diff --git a/Items.cc b/Items.cc new file mode 100644 index 00000000..9b688df1 --- /dev/null +++ b/Items.cc @@ -0,0 +1,462 @@ +#include "Items.hh" + +using namespace std; + + + +//////////////////////////////////////////////////////////////////////////////// + +/* these items all need some kind of special handling that hasn't been implemented yet. + +030B04 = TP Material (?) +030C00 = Cell Of MAG 502 +030C01 = Cell Of MAG 213 +030C02 = Parts Of RoboChao +030C03 = Heart Of Opa Opa +030C04 = Heart Of Pian +030C05 = Heart Of Chao + +030D00 = Sorcerer's Right Arm +030D01 = S-beat's Arms +030D02 = P-arm's Arms +030D03 = Delsabre's Right Arm +030D04 = C-bringer's Right Arm +030D05 = Delsabre's Left Arm +030D06 = S-red's Arms +030D07 = Dragon's Claw +030D08 = Hildebear's Head +030D09 = Hildeblue's Head +030D0A = Parts of Baranz +030D0B = Belra's Right Arms +030D0C = GIGUE'S ARMS +030D0D = S-BERILL'S ARMS +030D0E = G-ASSASIN'S ARMS +030D0F = BOOMA'S RIGHT ARMS +030D10 = GOBOOMA'S RIGHT ARMS +030D11 = GIGOBOOMA'S RIGHT ARMS +030D12 = GAL WIND +030D13 = RAPPY'S WING + +030E00 = BERILL PHOTON +030E01 = PARASITIC GENE FLOW +030E02 = MAGICSTONE IRITISTA +030E03 = BLUE BLACK STONE +030E04 = SYNCESTA +030E05 = MAGIC WATER +030E06 = PARASITIC CELL TYPE D +030E07 = MAGIC ROCK HEART KEY +030E08 = MAGIC ROCK MOOLA +030E09 = STAR AMPLIFIER +030E0A = BOOK OF HITOGATA +030E0B = HEART OF CHU CHU +030E0C = PART OF EGG BLASTER +030E0D = HEART OF ANGLE +030E0E = HEART OF DEVIL +030E0F = KIT OF HAMBERGER +030E10 = PANTHER'S SPIRIT +030E11 = KIT OF MARK3 +030E12 = KIT OF MASTER SYSTEM +030E13 = KIT OF GENESIS +030E14 = KIT OF SEGA SATURN +030E15 = KIT OF DREAMCAST +030E16 = AMP. RESTA +030E17 = AMP. ANTI +030E18 = AMP. SHIFTA +030E19 = AMP. DEBAND +030E1A = AMP. +030E1B = AMP. +030E1C = AMP. +030E1D = AMP. +030E1E = AMP. +030E1F = AMP. +030E20 = AMP. +030E21 = AMP. +030E22 = AMP. +030E23 = AMP. +030E24 = AMP. +030E25 = AMP. +030E26 = HEART OF KAPUKAPU +030E27 = PROTON BOOSTER +030F00 = ADD SLOT +031000 = PHOTON DROP +031001 = PHOTON SPHERE +031002 = PHOTON CRYSTAL +031100 = BOOK OF KATANA 1 +031101 = BOOK OF KATANA 2 +031102 = BOOK OF KATANA 3 +031200 = WEAPONS BRONZE BADGE +031201 = WEAPONS SILVER BADGE +031202 = WEAPONS GOLD BADGE +031203 = WEAPONS CRYSTAL BADGE +031204 = WEAPONS STEEL BADGE +031205 = WEAPONS ALUMINUM BADGE +031206 = WEAPONS LEATHER BADGE +031207 = WEAPONS BONE BADGE +031208 = LETTER OF APPRECATION +031209 = AUTOGRAPH ALBUM +03120A = VALENTINE'S CHOCOLATE +03120B = NEWYEAR'S CARD +03120C = CRISMAS CARD +03120D = BIRTHDAY CARD +03120E = PROOF OF SONIC TEAM +03120F = SPECIAL EVENT TICKET +031300 = PRESENT +031400 = CHOCOLATE +031401 = CANDY +031402 = CAKE +031403 = SILVER BADGE +031404 = GOLD BADGE +031405 = CRYSTAL BADGE +031406 = IRON BADGE +031407 = ALUMINUM BADGE +031408 = LEATHER BADGE +031409 = BONE BADGE +03140A = BONQUET +03140B = DECOCTION +031500 = CRISMAS PRESENT +031501 = EASTER EGG +031502 = JACK-O'S-LANTERN +031700 = HUNTERS REPORT +031701 = HUNTERS REPORT RANK A +031702 = HUNTERS REPORT RANK B +031703 = HUNTERS REPORT RANK C +031704 = HUNTERS REPORT RANK F +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031705 = HUNTERS REPORT +031802 = Dragon Scale +031803 = Heaven Striker Coat +031807 = Rappys Beak +031802 = Dragon Scale */ + +//////////////////////////////////////////////////////////////////////////////// + +void player_use_item_locked(shared_ptr l, shared_ptr c, + size_t item_index) { + + ssize_t equipped_weapon = -1; + ssize_t equipped_armor = -1; + ssize_t equipped_shield = -1; + ssize_t equipped_mag = -1; + for (size_t y = 0; y < c->player.inventory.num_items; y++) { + if (c->player.inventory.items[y].equip_flags & 0x0008) { + if (c->player.inventory.items[y].data.item_data1[0] == 0) { + equipped_weapon = y; + } else if ((c->player.inventory.items[y].data.item_data1[0] == 1) && + (c->player.inventory.items[y].data.item_data1[1] == 1)) { + equipped_armor = y; + } else if ((c->player.inventory.items[y].data.item_data1[0] == 1) && + (c->player.inventory.items[y].data.item_data1[1] == 2)) { + equipped_shield = y; + } else if (c->player.inventory.items[y].data.item_data1[0] == 2) { + equipped_mag = y; + } + } + } + + bool should_delete_item = true; + + auto& item = c->player.inventory.items[item_index]; + if (item.data.item_data1w[0] == 0x0203) { // technique disk + c->player.disp.technique_levels[item.data.item_data1[4]] = item.data.item_data1[2]; + + } else if (item.data.item_data1w[0] == 0x0A03) { // grinder + if (equipped_weapon < 0) { + throw invalid_argument("grinder used with no weapon equipped"); + } + if (item.data.item_data1[2] > 2) { + throw invalid_argument("incorrect grinder value"); + } + c->player.inventory.items[equipped_weapon].data.item_data1[3] += (item.data.item_data1[2] + 1); + // TODO: we should check for max grind here + + } else if (item.data.item_data1w[0] == 0x0B03) { // material + switch (item.data.item_data1[2]) { + case 0: // Power Material + c->player.disp.stats.atp += 2; + break; + case 1: // Mind Material + c->player.disp.stats.mst += 2; + break; + case 2: // Evade Material + c->player.disp.stats.evp += 2; + break; + case 3: // HP Material + c->player.inventory.hp_materials_used += 2; + break; + case 4: // TP Material + c->player.inventory.tp_materials_used += 2; + break; + case 5: // Def Material + c->player.disp.stats.dfp += 2; + break; + case 6: // Luck Material + c->player.disp.stats.lck += 2; + break; + default: + throw invalid_argument("unknown material used"); + } + + } else { + // default item action is to unwrap the item if it's a present + if ((item.data.item_data1[0] == 2) && (item.data.item_data2[2] & 0x40)) { + item.data.item_data2[2] &= 0xBF; + should_delete_item = false; + } else if ((item.data.item_data1[0] != 2) && (item.data.item_data1[4] & 0x40)) { + item.data.item_data1[4] &= 0xBF; + should_delete_item = false; + } + } + + if (should_delete_item) { + c->player.remove_item(item.data.item_id, 1, NULL); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +// reads the non-rare item preferences from the config file. +CommonItemCreator::CommonItemCreator( + const vector& enemy_item_categories, + const vector& box_item_categories, + const vector>& unit_types) : + enemy_item_categories(enemy_item_categories), + box_item_categories(box_item_categories), + unit_types(unit_types) { + + // sanity check the values + if (this->enemy_item_categories.size() != 8) { + throw invalid_argument("enemy item categories is incorrect length"); + } + if (this->box_item_categories.size() != 8) { + throw invalid_argument("box item categories is incorrect length"); + } + if (this->unit_types.size() != 4) { + throw invalid_argument("unit types is incorrect length"); + } + + { + uint64_t sum = 0; + for (uint32_t v : this->enemy_item_categories) { + sum += v; + } + if (sum > 0xFFFFFFFF) { + throw invalid_argument("enemy item category sum is too large"); + } + } + + { + uint64_t sum = 0; + for (uint32_t v : this->box_item_categories) { + sum += v; + } + if (sum > 0xFFFFFFFF) { + throw invalid_argument("box item category sum is too large"); + } + } +} + +int32_t CommonItemCreator::decide_item_type(bool is_box) const { + uint32_t determinant = (rand() << 30) ^ (rand() << 15) ^ rand(); + + const auto* v = is_box ? &this->box_item_categories : &this->enemy_item_categories; + for (size_t x = 0; x < v->size(); x++) { + uint32_t probability = v->at(x); + if (probability > determinant) { + return x; + } + determinant -= probability; + } + return -1; +} + +ItemData CommonItemCreator::create_item(bool is_box, uint8_t episode, + uint8_t difficulty, uint8_t area, uint8_t section_id) const { + // change the area if it's invalid (data for the bosses are actually in other areas) + if (area > 10) { + if (episode == 1) { + if (area == 11) { + area = 3; // dragon + } else if (area == 12) { + area = 6; // de rol le + } else if (area == 13) { + area = 8; // vol opt + } else if (area == 14) { + area = 10; // dark falz + } else { + area = 1; // unknown area -> forest 1 + } + } else if (episode == 2) { + if (area == 12) { + area = 9; // gal gryphon + } else if (area == 13) { + area = 10; // olga flow + } else if (area == 14) { + area = 3; // barba ray + } else if (area == 15) { + area = 6; // gol dragon + } else { + area = 10; // tower + } + } else if (episode == 3) { + area = 1; + } + } + + ItemData item; + memset(&item, 0, sizeof(item)); + + // picks a random non-rare item type, then gives it appropriate random stats + // modify some of the constants in this section to change the system's + // parameters + int32_t type = this->decide_item_type(is_box); + switch (type) { + case 0x00: // material + item.item_data1[0] = 0x03; + item.item_data1[1] = 0x0B; + item.item_data1[2] = rand() % 7; + break; + + case 0x01: // equipment + switch (rand() % 4) { + case 0x00: // weapon + item.item_data1[1] = 1 + (rand() % 12); // random normal class + item.item_data1[2] = difficulty + (rand() % 3); // special type + if ((item.item_data1[1] > 0x09) && (item.item_data1[2] > 0x04)) { + item.item_data1[2] = 0x04; // no special classes above 4 + } + item.item_data1[4] = 0x80; // untekked + if (item.item_data1[2] < 0x04) { + item.item_data1[4] |= (rand() % 41); // give a special + } + for (size_t x = 0, y = 0; (x < 5) && (y < 3); x++) { // percentages + if ((rand() % 11) < 2) { // 1/11 chance of getting each type of percentage + item.item_data1[6 + (y * 2)] = x + 1; + item.item_data1[7 + (y * 2)] = (rand() % 11) * 5; + y++; + } + } + break; + + case 0x01: // armor + item.item_data1[0] = 0x01; + item.item_data1[1] = 0x01; + item.item_data1[2] = (6 * difficulty) + (rand() % ((area / 2) + 2)); // standard type based on difficulty and area + if (item.item_data1[2] > 0x17) { + item.item_data1[2] = 0x17; // no standard types above 0x17 + } + if ((rand() % 10) < 2) { // +/- + item.item_data1[4] = rand() % 6; + item.item_data1[6] = rand() % 3; + } + item.item_data1[5] = rand() % 5; // slots + break; + + case 0x02: // shield + item.item_data1[0] = 0x01; + item.item_data1[1] = 0x02; + item.item_data1[2] = (5 * difficulty) + (rand() % ((area / 2) + 2)); // standard type based on difficulty and area + if (item.item_data1[2] > 0x14) { + item.item_data1[2] = 0x14; // no standard types above 0x14 + } + if ((rand() % 11) < 2) { // +/- + item.item_data1[4] = rand() % 6; + item.item_data1[6] = rand() % 6; + } + break; + + case 0x03: { // unit + const auto& type_table = this->unit_types.at(difficulty); + uint8_t type = type_table[rand() % type_table.size()]; + if (type == 0xFF) { + throw out_of_range("no item dropped"); // 0xFF -> no item drops + } + item.item_data1[0] = 0x01; + item.item_data1[1] = 0x03; + item.item_data1[2] = type; + break; + } + } + break; + + case 0x02: // technique + item.item_data1[0] = 0x03; + item.item_data1[1] = 0x02; + item.item_data1[4] = rand() % 19; // tech type + if ((item.item_data1[4] != 14) && (item.item_data1[4] != 17)) { // if not ryuker or reverser, give it a level + if (item.item_data1[4] == 16) { // if not anti, give it a level between 1 and 30 + if (area > 3) { + item.item_data1[2] = difficulty + (rand() % ((area - 1) / 2)); + } else { + item.item_data1[2] = difficulty + (rand() % 1); + } + if (item.item_data1[2] > 6) { + item.item_data1[2] = 6; + } + } else { + item.item_data1[2] = (5 * difficulty) + (rand() % ((area * 3) / 2)); // else between 1 and 7 + } + } + break; + + case 0x03: // scape doll + item.item_data1[0] = 0x03; + item.item_data1[1] = 0x09; + item.item_data1[2] = 0x00; + break; + + case 0x04: // grinder + item.item_data1[0] = 0x03; + item.item_data1[1] = 0x0A; + item.item_data1[2] = rand() % 3; // mono, di, tri + break; + + case 0x05: // consumable + item.item_data1[0] = 0x03; + item.item_data1[5] = 0x01; + switch (rand() % 3) { + case 0: // antidote / antiparalysis + item.item_data1[1] = 6; + item.item_data1[2] = rand() & 1; + break; + + case 1: // telepipe / trap vision + item.item_data1[1] = 7 + rand() & 1; + break; + + case 2: // sol / moon / star atomizer + item.item_data1[1] = 3 + rand() % 3; + break; + } + break; + + case 0x06: // consumable + item.item_data1[0] = 0x03; + item.item_data1[5] = 0x01; + item.item_data1[1] = rand() & 1; // mate or fluid + if (difficulty == 0) { + item.item_data1[2] = rand() & 1; // only mono and di on normal + } else if (difficulty == 3) { + item.item_data1[2] = 1 + rand() & 1; // only di and tri on ultimate + } else { + item.item_data1[2] = rand() % 3; // else, any of the three + } + break; + + case 0x07: // meseta + item.item_data1[0] = 0x04; + item.item_data2d = (90 * difficulty) + ((rand() % 20) * (area * 2)); // meseta amount + break; + + default: + throw out_of_range("no item created"); + } + + return item; +} diff --git a/Items.hh b/Items.hh new file mode 100644 index 00000000..1457fbaf --- /dev/null +++ b/Items.hh @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +#include "Lobby.hh" +#include "Client.hh" + +void player_use_item_locked(std::shared_ptr l, std::shared_ptr c, + size_t item_index); + +struct CommonItemCreator { + std::vector enemy_item_categories; + std::vector box_item_categories; + std::vector> unit_types; + + CommonItemCreator(const std::vector& enemy_item_categories, + const std::vector& box_item_categories, + const std::vector>& unit_types); + + int32_t decide_item_type(bool is_box) const; + ItemData create_item(bool is_box, uint8_t episode, uint8_t difficulty, + uint8_t area, uint8_t section_id) const; +}; diff --git a/LevelTable.cc b/LevelTable.cc new file mode 100644 index 00000000..65acbea7 --- /dev/null +++ b/LevelTable.cc @@ -0,0 +1,51 @@ +#include "LevelTable.hh" + +#include + +#include "Compression.hh" + +using namespace std; + + + +LevelTable::LevelTable(const char* filename, bool compressed) { + + string data = load_file(filename); + if (compressed) { + data = prs_decompress(data); + } + + if (data.size() < sizeof(*this)) { + throw invalid_argument("level table size is incorrect"); + } + + memcpy(this, data.data(), sizeof(*this)); +} + +const PlayerStats& LevelTable::base_stats_for_class(uint8_t char_class) const { + if (char_class >= 12) { + throw out_of_range("invalid character class"); + } + return this->base_stats[char_class]; +} + + const LevelStats& LevelTable::stats_for_level(uint8_t char_class, + uint8_t level) const { + if (char_class >= 12) { + throw invalid_argument("invalid character class"); + } + if (level >= 200) { + throw invalid_argument("invalid character level"); + } + return this->levels[char_class][level]; +} + +// Levels up a character by adding the level-up bonuses to the player's stats. +void LevelStats::apply(PlayerStats& ps) const { + ps.ata += this->ata; + ps.atp += this->atp; + ps.dfp += this->dfp; + ps.evp += this->evp; + ps.hp += this->hp; + ps.mst += this->mst; +} diff --git a/LevelTable.hh b/LevelTable.hh new file mode 100644 index 00000000..27eef5f0 --- /dev/null +++ b/LevelTable.hh @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "Player.hh" + +// information on a single level for a single class +struct LevelStats { + uint8_t atp; // atp to add on level up + uint8_t mst; // mst to add on level up + uint8_t evp; // evp to add on level up + uint8_t hp; // hp to add on level up + uint8_t dfp; // dfp to add on level up + uint8_t ata; // ata to add on level up + uint8_t unknown[2]; + uint32_t experience; // EXP value of this level + + void apply(PlayerStats& ps) const; +}; + +// level table format (PlyLevelTbl.prs) +struct LevelTable { + PlayerStats base_stats[12]; + uint32_t unknown[12]; + LevelStats levels[12][200]; + + LevelTable(const char* filename, bool compressed); + + const PlayerStats& base_stats_for_class(uint8_t char_class) const; + const LevelStats& stats_for_level(uint8_t char_class, uint8_t level) const; +}; diff --git a/License.cc b/License.cc new file mode 100644 index 00000000..61f2b766 --- /dev/null +++ b/License.cc @@ -0,0 +1,112 @@ +#include +#include + +#include +#include + +#include "License.hh" + +using namespace std; + + + +LicenseManager::LicenseManager(const std::string& filename) : filename(filename) { + auto licenses = load_vector_file(this->filename); + + for (const auto& read_license : licenses) { + shared_ptr license(new License(read_license)); + this->bb_username_to_license.emplace(license->username, license); + this->serial_number_to_license.emplace(license->serial_number, license); + } +} + +void LicenseManager::save_locked() const { + auto f = fopen_unique(this->filename, "wb"); + for (const auto& it : this->serial_number_to_license) { + fwritex(f.get(), it.second.get(), sizeof(License)); + } +} + +shared_ptr LicenseManager::verify_pc(uint32_t serial_number, + const char* access_key, const char* password) const { + rw_guard g(this->lock, false); + + auto& license = this->serial_number_to_license.at(serial_number); + if (strncmp(license->access_key, access_key, 8)) { + throw invalid_argument("incorrect access key"); + } + if (password && (strcmp(license->gc_password, password))) { + throw invalid_argument("incorrect password"); + } + + if (license->ban_end_time && (license->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + return license; +} + +shared_ptr LicenseManager::verify_gc(uint32_t serial_number, + const char* access_key, const char* password) const { + rw_guard g(this->lock, false); + + auto& license = this->serial_number_to_license.at(serial_number); + if (strncmp(license->access_key, access_key, 12)) { + throw invalid_argument("incorrect access key"); + } + if (password && (strcmp(license->gc_password, password))) { + throw invalid_argument("incorrect password"); + } + + if (license->ban_end_time && (license->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + return license; +} + +shared_ptr LicenseManager::verify_bb(const char* username, + const char* password) const { + rw_guard g(this->lock, false); + + auto& license = this->bb_username_to_license.at(username); + if (password && strcmp(license->bb_password, password)) { + throw invalid_argument("incorrect password"); + } + + if (license->ban_end_time && (license->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + return license; +} + +void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) { + rw_guard g(this->lock, false); + this->serial_number_to_license.at(serial_number)->ban_end_time = end_time; + this->save_locked(); +} + +void LicenseManager::add(shared_ptr l) { + { + rw_guard g(this->lock, true); + this->serial_number_to_license.emplace(l->serial_number, l); + if (l->username[0]) { + this->bb_username_to_license.emplace(l->username, l); + } + } + + rw_guard g(this->lock, false); + this->save_locked(); +} + +void LicenseManager::remove(uint32_t serial_number) { + { + rw_guard g(this->lock, true); + auto l = this->serial_number_to_license.at(serial_number); + this->serial_number_to_license.erase(l->serial_number); + if (l->username[0]) { + this->bb_username_to_license.erase(l->username); + } + } + + rw_guard g(this->lock, false); + this->save_locked(); +} diff --git a/License.hh b/License.hh new file mode 100644 index 00000000..9f8ea27b --- /dev/null +++ b/License.hh @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +enum Privilege { + KickUser = 0x00000001, + BanUser = 0x00000002, + SilenceUser = 0x00000004, + ChangeLobbyInfo = 0x00000008, + ChangeEvent = 0x00000010, + Announce = 0x00000020, + FreeJoinGames = 0x00000040, + UnlockGames = 0x00000080, + + Moderator = 0x00000007, + Administrator = 0x0000003F, + Root = 0xFFFFFFFF, +}; + +enum LicenseVerifyAction { + BB = 0x00, + GC = 0x01, + PC = 0x02, + SerialNumber = 0x03, +}; + +struct License { + char username[20]; // BB username (max. 16 chars; should technically be Unicode) + char bb_password[20]; // BB password (max. 16 chars) + uint32_t serial_number; // PC/GC serial number. MUST BE PRESENT FOR BB LICENSES TOO; this is also the player's guild card number. + char access_key[16]; // PC/GC access key. (to log in using PC on a GC license, just enter the first 8 characters of the GC access key) + char gc_password[12]; // GC password + uint32_t privileges; // privilege level + uint64_t ban_end_time; // end time of ban (zero = not banned) +}; + +class LicenseManager { +public: + LicenseManager(const std::string& filename); + ~LicenseManager() = default; + + std::shared_ptr verify_pc(uint32_t serial_number, + const char* access_key, const char* password) const; + std::shared_ptr verify_gc(uint32_t serial_number, + const char* access_key, const char* password) const; + std::shared_ptr verify_bb(const char* username, + const char* password) const; + void ban_until(uint32_t serial_number, uint64_t seconds); + + void add(std::shared_ptr l); + void remove(uint32_t serial_number); + +protected: + void save_locked() const; + + mutable rw_lock lock; + + std::string filename; + std::unordered_map> bb_username_to_license; + std::unordered_map> serial_number_to_license; +}; diff --git a/Lobby.cc b/Lobby.cc new file mode 100644 index 00000000..ad5f8163 --- /dev/null +++ b/Lobby.cc @@ -0,0 +1,202 @@ +#include "Lobby.hh" + +#include "SendCommands.hh" +#include "Text.hh" + +using namespace std; + + + +Lobby::Lobby() : lobby_id(0), min_level(0), max_level(0xFFFFFFFF), + next_game_item_id(0), version(GameVersion::GC), section_id(0), episode(1), + difficulty(0), mode(0), event(0), block(0), type(0), leader_id(0), + max_clients(12), flags(0), loading_quest_id(0) { + + for (size_t x = 0; x < 12; x++) { + this->next_item_id[x] = 0; + } + memset(&this->next_drop_item, 0, sizeof(this->next_drop_item)); + memset(this->variations, 0, 0x20 * sizeof(this->variations[0])); + memset(this->password, 0, 36 * sizeof(this->password[0])); + memset(this->name, 0, 36 * sizeof(this->name[0])); +} + +bool Lobby::is_game() const { + return (this->lobby_id < 0); +} + +void Lobby::reassign_leader_on_client_departure_locked(size_t leaving_client_index) { + for (size_t x = 0; x < this->max_clients; x++) { + if (x == leaving_client_index) { + continue; + } + if (this->clients[x].get()) { + this->leader_id = x; + return; + } + } + throw out_of_range("no clients remain"); +} + +bool Lobby::any_client_loading() const { + rw_guard g(this->lock, false); + for (size_t x = 0; x < this->max_clients; x++) { + if (!this->clients[x].get()) { + continue; + } + if (this->clients[x]->flags & ClientFlag::Loading) { + return true; + } + } + return false; +} + +size_t Lobby::count_clients() const { + rw_guard g(this->lock, false); + size_t ret = 0; + for (size_t x = 0; x < this->max_clients; x++) { + if (this->clients[x].get()) { + ret++; + } + } + return ret; +} + +void Lobby::add_client(shared_ptr c) { + rw_guard g(this->lock, true); + this->add_client_locked(c); +} + +void Lobby::add_client_locked(shared_ptr c) { + rw_guard g(this->lock, true); + size_t index; + for (index = 0; index < this->max_clients; index++) { + if (!this->clients[index].get()) { + this->clients[index] = c; + } + } + if (index >= this->max_clients) { + throw out_of_range("no space left in lobby"); + } + c->lobby_client_id = index; + c->lobby_id = this->lobby_id; + + // if index == 0, then there might not be anyone else in the lobby; if this is + // the case, set the leader id as well + if (index == 0) { + for (index = 1; index < this->max_clients; index++) { + if (this->clients[index].get()) { + break; + } + } + if (index >= this->max_clients) { + this->leader_id = 0; + } + } +} + +void Lobby::remove_client(shared_ptr c) { + rw_guard g(this->lock, true); + this->remove_client_locked(c); +} + +void Lobby::remove_client_locked(shared_ptr c) { + rw_guard g(this->lock, true); + + if (this->clients[c->lobby_client_id] != c) { + throw logic_error("client\'s lobby client id does not match client list"); + } + + this->clients[c->lobby_client_id] = NULL; + c->lobby_id = 0; + + this->reassign_leader_on_client_departure_locked(c->lobby_client_id); +} + +void Lobby::move_client_to_lobby(shared_ptr dest_lobby, + shared_ptr c) { + if (dest_lobby.get() == this) { + return; + } + + // deadlock prevention: lock the lobbies in increasing order of memory address + vector guards; + uint8_t* this_ptr = reinterpret_cast(this); + uint8_t* dest_ptr = reinterpret_cast(dest_lobby.get()); + if (this_ptr < dest_ptr) { + guards.emplace_back(this->lock, true); + guards.emplace_back(dest_lobby->lock, true); + } else { + guards.emplace_back(dest_lobby->lock, true); + guards.emplace_back(this->lock, true); + } + + dest_lobby->add_client_locked(c); + this->remove_client_locked(c); +} + + + +shared_ptr Lobby::find_client(const char16_t* identifier, + uint64_t serial_number) { + rw_guard g(this->lock, false); + for (size_t x = 0; x < this->max_clients; x++) { + if (!this->clients[x]) { + continue; + } + if (serial_number && this->clients[x]->license && + (this->clients[x]->license->serial_number == serial_number)) { + return this->clients[x]; + } + if (identifier && !char16cmp(this->clients[x]->player.disp.name, identifier, 0x10)) { + return this->clients[x]; + } + } + + throw out_of_range("client not found"); +} + + + +uint8_t Lobby::game_event_for_lobby_event(uint8_t lobby_event) { + if (lobby_event > 7) { + return 0; + } + if (lobby_event == 7) { + return 2; + } + if (lobby_event == 2) { + return 0; + } + return lobby_event; +} + + + +void Lobby::add_item(const PlayerInventoryItem& item) { + rw_guard g(this->lock, true); + this->item_id_to_floor_item.emplace(item.data.item_id, item); +} + +void Lobby::remove_item(uint32_t item_id, PlayerInventoryItem* item) { + rw_guard g(this->lock, true); + auto item_it = this->item_id_to_floor_item.find(item_id); + if (item_it == this->item_id_to_floor_item.end()) { + throw out_of_range("item not present"); + } + *item = move(item_it->second); + this->item_id_to_floor_item.erase(item_it); +} + +uint32_t Lobby::generate_item_id(uint32_t client_id) { + if (client_id < this->max_clients) { + return this->next_item_id[client_id]++; + } + return this->next_game_item_id++; +} + +void Lobby::assign_item_ids_for_player(uint32_t client_id, PlayerInventory& inv) { + for (size_t x = 0; x < inv.num_items; x++) { + inv.items[x].data.item_id = this->generate_item_id(client_id); + } +} \ No newline at end of file diff --git a/Lobby.hh b/Lobby.hh new file mode 100644 index 00000000..52833ad2 --- /dev/null +++ b/Lobby.hh @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include +#include + +#include "Client.hh" +#include "Player.hh" +#include "Map.hh" +#include "RareItemSet.hh" + +enum LobbyFlag { + // games and lobbies are actually the same thing - games have negative IDs, + // lobbies have IDs >= 0 + IsGame = 0x01, + CheatsEnabled = 0x02, // game only + Public = 0x04, // lobby only + Episode3 = 0x08, // lobby only + QuestInProgress = 0x10, // game only + JoinableQuestInProgress = 0x20, // game only + Default = 0x40, // lobby only; false for private lobbies +}; + +struct Lobby { + mutable rw_lock lock; + + uint64_t lobby_id; + + uint32_t min_level; + uint32_t max_level; + + // item info + std::vector enemies; + std::shared_ptr rare_item_set; + uint32_t next_item_id[12]; + uint32_t next_game_item_id; + PlayerInventoryItem next_drop_item; + std::unordered_map item_id_to_floor_item; + uint32_t variations[0x20]; + + // game config + GameVersion version; + uint8_t section_id; + uint8_t episode; + uint8_t difficulty; + uint8_t mode; + char16_t password[36]; + char16_t name[36]; + + //EP3_GAME_CONFIG* ep3; // only present if this is an Episode 3 game + + // lobby stuff + uint8_t event; + uint8_t block; + uint8_t type; // number to give to PSO for the lobby number + uint8_t leader_id; + uint8_t max_clients; + uint32_t flags; + uint32_t loading_quest_id; // for use with joinable quests + std::shared_ptr clients[12]; + + Lobby(); + + bool is_game() const; + + void reassign_leader_on_client_departure_locked(size_t leaving_client_id); + size_t count_clients() const; + bool any_client_loading() const; + + void add_client(std::shared_ptr c); + void add_client_locked(std::shared_ptr c); + void remove_client(std::shared_ptr c); + void remove_client_locked(std::shared_ptr c); + + void move_client_to_lobby(std::shared_ptr dest_lobby, + std::shared_ptr c); + + std::shared_ptr find_client(const char16_t* identifier = NULL, + uint64_t serial_number = 0); + + void add_item(const PlayerInventoryItem& item); + void remove_item(uint32_t item_id, PlayerInventoryItem* item); + size_t find_item(uint32_t item_id); + uint32_t generate_item_id(uint32_t client_id); + + void assign_item_ids_for_player(uint32_t client_id, PlayerInventory& inv); + + static uint8_t game_event_for_lobby_event(uint8_t lobby_event); +}; diff --git a/Main.cc b/Main.cc new file mode 100644 index 00000000..987d5f01 --- /dev/null +++ b/Main.cc @@ -0,0 +1,170 @@ +#include +#include + +#include +#include +#include +#include + +#include "NetworkAddresses.hh" +#include "DNSServer.hh" +#include "ServerState.hh" +#include "Server.hh" +#include "FileContentsCache.hh" +#include "Text.hh" + +using namespace std; + + + +FileContentsCache file_cache; + + + +static const unordered_map default_port_to_behavior({ + {"gc-jp10", {9000, GameVersion::GC, ServerBehavior::LoginServer}}, + {"gc-jp11", {9001, GameVersion::GC, ServerBehavior::LoginServer}}, + {"gc-jp3", {9003, GameVersion::GC, ServerBehavior::LoginServer}}, + {"gc-us10", {9100, GameVersion::PC, ServerBehavior::SplitReconnect}}, + {"gc-us3", {9103, GameVersion::GC, ServerBehavior::LoginServer}}, + {"gc-eu10", {9200, GameVersion::GC, ServerBehavior::LoginServer}}, + {"gc-eu11", {9201, GameVersion::GC, ServerBehavior::LoginServer}}, + {"gc-eu3", {9203, GameVersion::GC, ServerBehavior::LoginServer}}, + {"pc-login", {9300, GameVersion::PC, ServerBehavior::LoginServer}}, + {"pc-patch", {10000, GameVersion::Patch, ServerBehavior::PatchServer}}, + {"bb-patch", {11000, GameVersion::Patch, ServerBehavior::PatchServer}}, + {"bb-data", {12000, GameVersion::BB, ServerBehavior::DataServerBB}}, + + // these aren't hardcoded in any games; user can override them + {"bb-data1", {12004, GameVersion::BB, ServerBehavior::DataServerBB}}, + {"bb-data2", {12005, GameVersion::BB, ServerBehavior::DataServerBB}}, + {"bb-login", {12008, GameVersion::BB, ServerBehavior::LoginServer}}, + {"pc-lobby", {9420, GameVersion::PC, ServerBehavior::LobbyServer}}, + {"gc-lobby", {9421, GameVersion::GC, ServerBehavior::LobbyServer}}, + {"bb-lobby", {9422, GameVersion::BB, ServerBehavior::LobbyServer}}, +}); + + + +template +vector parse_int_vector(shared_ptr o) { + vector ret; + for (const auto& x : o->as_list()) { + ret.emplace_back(x->as_int()); + } + return ret; +} + + + +void populate_state_from_config(shared_ptr s, + shared_ptr config_json) { + const auto& d = config_json->as_dict(); + + s->name = d.at("ServerName")->as_string(); + + // TODO: make this configurable + s->port_configuration = default_port_to_behavior; + + auto enemy_categories = parse_int_vector(d.at("CommonItemDropRates-Enemy")); + auto box_categories = parse_int_vector(d.at("CommonItemDropRates-Box")); + vector> unit_types; + for (const auto& item : d.at("CommonUnitTypes")->as_list()) { + unit_types.emplace_back(parse_int_vector(item)); + } + s->common_item_creator.reset(new CommonItemCreator(enemy_categories, + box_categories, unit_types)); + + shared_ptr> information_menu(new vector()); + shared_ptr> id_to_information_contents( + new unordered_map()); + + uint32_t item_id = 1; + for (const auto& item : d.at("InformationMenuContents")->as_list()) { + auto& v = item->as_list(); + information_menu->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), + decode_sjis(v.at(1)->as_string()), MenuItemFlag::RequiresMessageBoxes); + id_to_information_contents->emplace(item_id, decode_sjis(v.at(2)->as_string())); + item_id++; + } + s->information_menu = information_menu; + s->id_to_information_contents = id_to_information_contents; + + s->num_threads = d.at("Threads")->as_int(); + + auto local_address_str = d.at("LocalAddress")->as_string(); + uint32_t local_address = inet_addr(local_address_str.c_str()); + if (s->all_addresses.emplace(local_address).second) { + log(INFO, "added local address: %hhu.%hhu.%hhu.%hhu", + static_cast(local_address >> 24), static_cast(local_address >> 16), + static_cast(local_address >> 8), static_cast(local_address)); + } + + auto external_address_str = d.at("LocalAddress")->as_string(); + uint32_t external_address = inet_addr(external_address_str.c_str()); + if (s->all_addresses.emplace(external_address).second) { + log(INFO, "added external address: %hhu.%hhu.%hhu.%hhu", + static_cast(external_address >> 24), static_cast(external_address >> 16), + static_cast(external_address >> 8), static_cast(external_address)); + } +} + + + +int main(int argc,char* argv[]) { + log(INFO, "fuzziqer software newserv"); + + signal(SIGPIPE, SIG_IGN); + + log(INFO, "creating server state"); + shared_ptr state(new ServerState()); + + log(INFO, "reading network addresses"); + state->all_addresses = get_local_address_list(); + for (uint32_t addr : state->all_addresses) { + log(INFO, "found address: %hhu.%hhu.%hhu.%hhu", + static_cast(addr >> 24), static_cast(addr >> 16), + static_cast(addr >> 8), static_cast(addr)); + } + + log(INFO, "loading configuration"); + auto config_json = JSONObject::load("system/config.json"); + populate_state_from_config(state, config_json); + + log(INFO, "loading license list"); + state->license_manager.reset(new LicenseManager("system/licenses.nsi")); + + log(INFO, "loading battle parameters"); + state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry")); + + log(INFO, "loading level table"); + state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); + + log(INFO, "collecting quest metadata"); + state->quest_index.reset(new QuestIndex("system/quests")); + + log(INFO, "starting dns server"); + DNSServer dns_server(state->local_address, state->external_address); + // TODO: call dns_server.listen appropriately + dns_server.start(); + + log(INFO, "starting game server"); + Server game_server(state); + // TODO: call game_server.listen appropriately + game_server.start(); + + for (;;) { + sigset_t s; + sigemptyset(&s); + sigsuspend(&s); + } + + log(INFO, "waiting for servers to terminate"); + dns_server.schedule_stop(); + game_server.schedule_stop(); + dns_server.wait_for_stop(); + game_server.wait_for_stop(); + + return 0; +} + diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5d2a3601 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +OBJECTS=FileContentsCache.o Menu.o PSOProtocol.o Client.o Lobby.o \ + ServerState.o Server.o License.o PSOEncryption.o Player.o SendCommands.o \ + ChatCommands.o ReceiveSubcommands.o ReceiveCommands.o Version.o Items.o \ + LevelTable.o Compression.o Quest.o RareItemSet.o Map.o NetworkAddresses.o \ + Text.o DNSServer.o Main.o +CXX=g++ +CXXFLAGS=-I/opt/local/include -I/usr/local/include -std=c++14 -g -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H -Wall -Werror +LDFLAGS=-L/opt/local/lib -L/usr/local/lib -std=c++14 -levent -lphosg -lpthread +EXECUTABLE=newserv + +all: $(EXECUTABLE) + +$(EXECUTABLE): $(OBJECTS) + $(CXX) $(OBJECTS) $(LDFLAGS) -o $(EXECUTABLE) + +clean: + find . -name \*.o -delete + rm -rf *.dSYM $(EXECUTABLE) gmon.out + +.PHONY: clean test diff --git a/Map.cc b/Map.cc new file mode 100644 index 00000000..9a6331ce --- /dev/null +++ b/Map.cc @@ -0,0 +1,443 @@ +#include "Map.hh" + +#include + +#include "FileContentsCache.hh" + +using namespace std; + +extern FileContentsCache file_cache; + + + +static void load_battle_param_file(const string& filename, BattleParams* entries) { + scoped_fd fd(filename, O_RDONLY); + readx(fd, entries, 0x60 * sizeof(BattleParams)); +} + +BattleParamTable::BattleParamTable(const char* prefix) { + load_battle_param_file(string_printf("%s_on.dat", prefix), + &this->entries[0][0][0][0]); + load_battle_param_file(string_printf("%s_lab_on.dat", prefix), + &this->entries[0][1][0][0]); + load_battle_param_file(string_printf("%s_ep4_on.dat", prefix), + &this->entries[0][2][0][0]); + load_battle_param_file(string_printf("%s.dat", prefix), + &this->entries[1][0][0][0]); + load_battle_param_file(string_printf("%s_lab.dat", prefix), + &this->entries[1][1][0][0]); + load_battle_param_file(string_printf("%s_ep4.dat", prefix), + &this->entries[1][2][0][0]); +} + +const BattleParams& BattleParamTable::get(bool solo, uint8_t episode, + uint8_t difficulty, uint8_t monster_type) const { + if (episode > 3) { + throw invalid_argument("incorrect episode"); + } + if (difficulty > 4) { + throw invalid_argument("incorrect difficulty"); + } + if (monster_type > 0x60) { + throw invalid_argument("incorrect monster type"); + } + return this->entries[!!solo][episode][difficulty][monster_type]; +} + +const BattleParams* BattleParamTable::get_subtable(bool solo, uint8_t episode, + uint8_t difficulty) const { + if (episode > 3) { + throw invalid_argument("incorrect episode"); + } + if (difficulty > 4) { + throw invalid_argument("incorrect difficulty"); + } + return &this->entries[!!solo][episode][difficulty][0]; +} + + + +PSOEnemy::PSOEnemy() : PSOEnemy(0, 0) { } + +PSOEnemy::PSOEnemy(uint32_t experience, uint32_t rt_index) : unused(0), + hit_flags(0), last_hit(0), experience(experience), rt_index(rt_index) { } + + + +struct EnemyEntry { + uint32_t base; + uint16_t reserved0; + uint16_t num_clones; + uint32_t reserved[11]; + float reserved12; + uint32_t reserved13; + uint32_t reserved14; + uint32_t skin; + uint32_t reserved15; +}; + +static vector parse_map(uint8_t episode, uint8_t difficulty, + const BattleParams* battle_params, const EnemyEntry* map, + size_t entry_count, bool alt_enemies) { + + vector enemies; + enemies.resize(0xB50); + size_t num_enemies = 0; + + // TODO: this is some of the nastiest code ever. de-nastify it at your leisure + for (size_t y = 0; y < entry_count; y++) { + if (enemies.size() >= 0xB50) { + break; + } + + size_t num_clones = map[y].num_clones; + + switch (map[y].base) { + case 0x40: // Hildebear and Hildetorr + enemies[num_enemies].rt_index = 0x01 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x49 + (map[y].skin & 0x01)].experience; + break; + case 0x41: // Rappies + if (episode == 3) { // Del Rappy and Sand Rappy + enemies[num_enemies].rt_index = 17 + (map[y].skin & 0x01); + if (alt_enemies) { + enemies[num_enemies].experience = battle_params[0x17 + (map[y].skin & 0x01)].experience; + } else { + enemies[num_enemies].experience = battle_params[0x05 + (map[y].skin & 0x01)].experience; + } + } else { // Rag Rappy and Al Rappy (Love for Episode II) + if (map[y].skin & 0x01) { + enemies[num_enemies].rt_index = 0xFF; // No clue what rappy it could be... yet. + } else { + enemies[num_enemies].rt_index = 5; + } + enemies[num_enemies].experience = battle_params[0x18 + (map[y].skin & 0x01)].experience; + } + break; + case 0x42: // Monest + 30 Mothmants + enemies[num_enemies].experience = battle_params[0x01].experience; + enemies[num_enemies].rt_index = 4; + for (size_t x = 0; x < 30; x++) { + if (num_enemies >= 0xB50) { + break; + } + num_enemies++; + enemies[num_enemies].rt_index = 3; + enemies[num_enemies].experience = battle_params[0x00].experience; + } + break; + case 0x43: // Savage Wolf and Barbarous Wolf + enemies[num_enemies].rt_index = 7 + ((map[y].reserved[10] & 0x800000) ? 1 : 0); + enemies[num_enemies].experience = battle_params[0x02 + ((map[y].reserved[10] & 0x800000) ? 1 : 0)].experience; + break; + case 0x44: // Booma family + enemies[num_enemies].rt_index = 9 + (map[y].skin % 3); + enemies[num_enemies].experience = battle_params[0x4B + (map[y].skin % 3)].experience; + break; + case 0x60: // Grass Assassin + enemies[num_enemies].rt_index = 12; + enemies[num_enemies].experience = battle_params[0x4E].experience; + break; + case 0x61: // Del Lily, Poison Lily, Nar Lily + if ((episode == 2) && (alt_enemies)) { + enemies[num_enemies].rt_index = 83; + enemies[num_enemies].experience = battle_params[0x25].experience; + } else { + enemies[num_enemies].rt_index = 13 + ((map[y].reserved[10] & 0x800000) ? 1 : 0); + enemies[num_enemies].experience = battle_params[0x04 + ((map[y].reserved[10] & 0x800000) ? 1 : 0)].experience; + } + break; + case 0x62: // Nano Dragon + enemies[num_enemies].rt_index = 15; + enemies[num_enemies].experience = battle_params[0x1A].experience; + break; + case 0x63: // Shark family + enemies[num_enemies].rt_index = 16 + (map[y].skin % 3); + enemies[num_enemies].experience = battle_params[0x4F + (map[y].skin % 3)].experience; + break; + case 0x64: // Slime + 4 clones + enemies[num_enemies].rt_index = 19 + ((map[y].reserved[10] & 0x800000) ? 1 : 0); + enemies[num_enemies].experience = battle_params[0x2F + ((map[y].reserved[10] & 0x800000) ? 0 : 1)].experience; + for (size_t x = 0; x < 4; x++) { + if (num_enemies >= 0xB50) { + break; + } + num_enemies++; + enemies[num_enemies].rt_index = 19; + enemies[num_enemies].experience = battle_params[0x30].experience; + } + break; + case 0x65: // Pan Arms, Migium, Hidoom + for (size_t x = 0; x < 3; x++) { + enemies[num_enemies + x].rt_index = 21 + x; + enemies[num_enemies + x].experience = battle_params[0x31 + x].experience; + } + num_enemies += 2; + break; + case 0x80: // Dubchic and Gilchic + enemies[num_enemies].experience = battle_params[0x1B + (map[y].skin & 0x01)].experience; + if (map[y].skin & 0x01) { + enemies[num_enemies].rt_index = 50; + } else { + enemies[num_enemies].rt_index = 24; + } + break; + case 0x81: // Garanz + enemies[num_enemies].rt_index = 25; + enemies[num_enemies].experience = battle_params[0x1D].experience; + break; + case 0x82: // Sinow Beat and Gold + enemies[num_enemies].rt_index = 26 + ((map[y].reserved[10] & 0x800000) ? 1 : 0); + if (map[y].reserved[10] & 0x800000) { + enemies[num_enemies].experience = battle_params[0x13].experience; + } else { + enemies[num_enemies].experience = battle_params[0x06].experience; + } + if (map[y].num_clones == 0) { + num_clones = 4; // only if no clone # present + } + break; + case 0x83: // Canadine + enemies[num_enemies].rt_index = 28; + enemies[num_enemies].experience = battle_params[0x07].experience; + break; + case 0x84: // Canadine Group + enemies[num_enemies].rt_index = 29; + enemies[num_enemies].experience = battle_params[0x09].experience; + for (size_t x = 1; x < 9; x++) { + enemies[num_enemies + x].rt_index = 28; + enemies[num_enemies + x].experience = battle_params[0x08].experience; + } + num_enemies += 8; + break; + case 0x85: // Dubwitch + break; + case 0xA0: // Delsaber + enemies[num_enemies].rt_index = 30; + enemies[num_enemies].experience = battle_params[0x52].experience; + break; + case 0xA1: // Chaos Sorcerer + 2 Bits + enemies[num_enemies].rt_index = 31; + enemies[num_enemies].experience = battle_params[0x0A].experience; + num_enemies += 2; + break; + case 0xA2: // Dark Gunner + enemies[num_enemies].rt_index = 34; + enemies[num_enemies].experience = battle_params[0x1E].experience; + break; + case 0xA4: // Chaos Bringer + enemies[num_enemies].rt_index = 36; + enemies[num_enemies].experience = battle_params[0x0D].experience; + break; + case 0xA5: // Dark Belra + enemies[num_enemies].rt_index = 37; + enemies[num_enemies].experience = battle_params[0x0E].experience; + break; + case 0xA6: // Dimenian family + enemies[num_enemies].rt_index = 41 + (map[y].skin % 3); + enemies[num_enemies].experience = battle_params[0x53 + (map[y].skin % 3)].experience; + break; + case 0xA7: // Bulclaw + 4 claws + enemies[num_enemies].rt_index = 40; + enemies[num_enemies].experience = battle_params[0x1F].experience; + for (size_t x = 1; x < 5; x++) { + enemies[num_enemies + x].rt_index = 38; + enemies[num_enemies + x].experience = battle_params[0x20].experience; + } + num_enemies += 4; + break; + case 0xA8: // Claw + enemies[num_enemies].rt_index = 38; + enemies[num_enemies].experience = battle_params[0x20].experience; + break; + case 0xC0: // Dragon or Gal Gryphon + if (episode == 1) { + enemies[num_enemies].rt_index = 44; + enemies[num_enemies].experience = battle_params[0x12].experience; + } else if (episode == 0x02) { + enemies[num_enemies].rt_index = 77; + enemies[num_enemies].experience = battle_params[0x1E].experience; + } + break; + case 0xC1: // De Rol Le + enemies[num_enemies].rt_index = 45; + enemies[num_enemies].experience = battle_params[0x0F].experience; + break; + case 0xC2: // Vol Opt form 1 + break; + case 0xC5: // Vol Opt form 2 + enemies[num_enemies].rt_index = 46; + enemies[num_enemies].experience = battle_params[0x25].experience; + break; + case 0xC8: // Dark Falz + 510 Helpers + enemies[num_enemies].rt_index = 47; + if (difficulty) { + enemies[num_enemies].experience = battle_params[0x38].experience; // Form 2 + } else { + enemies[num_enemies].experience = battle_params[0x37].experience; + } + for (size_t x = 1; x < 511; x++) { + //enemies[num_enemies + x].base = 200; + enemies[num_enemies + x].experience = battle_params[0x35].experience; + } + num_enemies += 510; + break; + case 0xCA: // Olga Flow + enemies[num_enemies].rt_index = 78; + enemies[num_enemies].experience = battle_params[0x2C].experience; + num_enemies += 512; + break; + case 0xCB: // Barba Ray + enemies[num_enemies].rt_index = 73; + enemies[num_enemies].experience = battle_params[0x0F].experience; + num_enemies += 47; + break; + case 0xCC: // Gol Dragon + enemies[num_enemies].rt_index = 76; + enemies[num_enemies].experience = battle_params[0x12].experience; + num_enemies += 5; + break; + case 0xD4: // Sinow Berill & Spigell + enemies[num_enemies].rt_index = 62 + ((map[y].reserved[10] & 0x800000) ? 1 : 0); + enemies[num_enemies].experience = battle_params[(map[y].reserved[10] & 0x800000) ? 0x13 : 0x06].experience; + num_enemies += 4; // Add 4 clones which are never used... + break; + case 0xD5: // Merillia & Meriltas + enemies[num_enemies].rt_index = 52 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x4B + (map[y].skin & 0x01)].experience; + break; + case 0xD6: // Mericus, Merikle, & Mericarol + enemies[num_enemies].rt_index = 56 + (map[y].skin % 3); + if (map[y].skin) { + enemies[num_enemies].experience = battle_params[0x44 + (map[y].skin % 3)].experience; + } else { + enemies[num_enemies].experience = battle_params[0x3A].experience; + } + break; + case 0xD7: // Ul Gibbon and Zol Gibbon + enemies[num_enemies].rt_index = 59 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x3B + (map[y].skin & 0x01)].experience; + break; + case 0xD8: // Gibbles + enemies[num_enemies].rt_index = 61; + enemies[num_enemies].experience = battle_params[0x3D].experience; + break; + case 0xD9: // Gee + enemies[num_enemies].rt_index = 54; + enemies[num_enemies].experience = battle_params[0x07].experience; + break; + case 0xDA: // Gi Gue + enemies[num_enemies].rt_index = 55; + enemies[num_enemies].experience = battle_params[0x1A].experience; + break; + case 0xDB: // Deldepth + enemies[num_enemies].rt_index = 71; + enemies[num_enemies].experience = battle_params[0x30].experience; + break; + case 0xDC: // Delbiter + enemies[num_enemies].rt_index = 72; + enemies[num_enemies].experience = battle_params[0x0D].experience; + break; + case 0xDD: // Dolmolm and Dolmdarl + enemies[num_enemies].rt_index = 64 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x4F + (map[y].skin & 0x01)].experience; + break; + case 0xDE: // Morfos + enemies[num_enemies].rt_index = 66; + enemies[num_enemies].experience = battle_params[0x40].experience; + break; + case 0xDF: // Recobox & Recons + enemies[num_enemies].rt_index = 67; + enemies[num_enemies].experience = battle_params[0x41].experience; + for (size_t x = 1; x <= map[y].num_clones; x++) { + enemies[num_enemies + x].rt_index = 68; + enemies[num_enemies + x].experience = battle_params[0x42].experience; + } + break; + case 0xE0: // Epsilon, Sinow Zoa and Zele + if ((episode == 0x02) && (alt_enemies)) { + enemies[num_enemies].rt_index = 84; + enemies[num_enemies].experience = battle_params[0x23].experience; + num_enemies += 4; + } else { + enemies[num_enemies].rt_index = 69 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x43 + (map[y].skin & 0x01)].experience; + } + break; + case 0xE1: // Ill Gill + enemies[num_enemies].rt_index = 82; + enemies[num_enemies].experience = battle_params[0x26].experience; + break; + case 0x0110: // Astark + enemies[num_enemies].rt_index = 1; + enemies[num_enemies].experience = battle_params[0x09].experience; + break; + case 0x0111: // Satellite Lizard and Yowie + enemies[num_enemies].rt_index = 2 + ((map[y].reserved[10] & 0x800000) ? 0 : 1); + enemies[num_enemies].experience = battle_params[0x0D + ((map[y].reserved[10] & 0x800000) ? 1 : 0) + (alt_enemies ? 0x10 : 0)].experience; + break; + case 0x0112: // Merissa A/AA + enemies[num_enemies].rt_index = 4 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x19 + (map[y].skin & 0x01)].experience; + break; + case 0x0113: // Girtablulu + enemies[num_enemies].rt_index = 6; + enemies[num_enemies].experience = battle_params[0x1F].experience; + break; + case 0x0114: // Zu and Pazuzu + enemies[num_enemies].rt_index = 7 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x0B + (map[y].skin & 0x01) + (alt_enemies ? 0x14: 0x00)].experience; + break; + case 0x0115: // Boota family + enemies[num_enemies].rt_index = 9 + (map[y].skin % 3); + if (map[y].skin & 2) { + enemies[num_enemies].experience = battle_params[0x03].experience; + } else { + enemies[num_enemies].experience = battle_params[0x00 + (map[y].skin % 3)].experience; + } + break; + case 0x0116: // Dorphon and Eclair + enemies[num_enemies].rt_index = 12 + (map[y].skin & 0x01); + enemies[num_enemies].experience = battle_params[0x0F + (map[y].skin & 0x01)].experience; + break; + case 0x0117: // Goran family + if (map[y].skin & 0x02) { + enemies[num_enemies].rt_index = 15; + } else if (map[y].skin & 0x01) { + enemies[num_enemies].rt_index = 16; + } else { + enemies[num_enemies].rt_index = 14; + } + enemies[num_enemies].experience = battle_params[0x11 + (map[y].skin % 3)].experience; + break; + case 0x0119: // Saint Million, Shambertin, and Kondrieu + if (map[y].reserved[10] & 0x800000) { + enemies[num_enemies].rt_index = 21; + } else { + enemies[num_enemies].rt_index = 19 + (map[y].skin & 0x01); + } + enemies[num_enemies].experience = battle_params[0x22].experience; + break; + default: + enemies[num_enemies].experience = 0xFFFFFFFF; + log(WARNING, "unknown enemy type %08" PRIX32 " %08" PRIX32, map[y].base, + map[y].skin); + break; + } + if (num_clones) { + num_enemies += num_clones; + } + num_enemies++; + } + + return enemies; +} + +vector load_map(const char* filename, uint8_t episode, + uint8_t difficulty, const BattleParams* battle_params, bool alt_enemies) { + shared_ptr data = file_cache.get(filename); + const EnemyEntry* entries = reinterpret_cast(data->data()); + size_t entry_count = data->size() / sizeof(EnemyEntry); + return parse_map(episode, difficulty, battle_params, entries, entry_count, + alt_enemies); +} diff --git a/Map.hh b/Map.hh new file mode 100644 index 00000000..d14bfeac --- /dev/null +++ b/Map.hh @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include + + + +struct BattleParams { + uint16_t atp; // attack power + uint16_t psv; // perseverance (intelligence?) + uint16_t evp; // evasion + uint16_t hp; // hit points + uint16_t dfp; // defense + uint16_t ata; // accuracy + uint16_t lck; // luck + uint8_t unknown[14]; + uint32_t experience; + uint32_t difficulty; +}; + +struct BattleParamTable { + BattleParams entries[2][3][4][0x60]; // online/offline, episode, difficulty, monster type + + BattleParamTable(const char* filename_prefix); + + const BattleParams& get(bool solo, uint8_t episode, uint8_t difficulty, + uint8_t monster_type) const; + const BattleParams* get_subtable(bool solo, uint8_t episode, + uint8_t difficulty) const; +}; + + + +struct BattleParamIndex { + BattleParamTable table_for_episode[3]; +}; + +// an enemy entry as loaded by the game +struct PSOEnemy { + uint16_t unused; + uint8_t hit_flags; + uint8_t last_hit; + uint32_t experience; + uint32_t rt_index; + + PSOEnemy(); + PSOEnemy(uint32_t experience, uint32_t rt_index); +}; + +std::vector load_map(const char* filename, uint8_t episode, + uint8_t difficulty, const BattleParams* bp, bool alt_enemies); diff --git a/Menu.cc b/Menu.cc new file mode 100644 index 00000000..079a8776 --- /dev/null +++ b/Menu.cc @@ -0,0 +1,9 @@ +#include "Menu.hh" + +using namespace std; + + + +MenuItem::MenuItem(uint32_t item_id, const u16string& name, + const u16string& description, uint32_t flags) : item_id(item_id), name(name), + description(description), flags(flags) { } diff --git a/Menu.hh b/Menu.hh new file mode 100644 index 00000000..a7f96b9b --- /dev/null +++ b/Menu.hh @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + + + +enum MenuItemFlag { + InvisibleOnDC = 0x01, + InvisibleOnPC = 0x02, + InvisibleOnGC = 0x04, + InvisibleOnGCEpisode3 = 0x08, + InvisibleOnBB = 0x10, + RequiresMessageBoxes = 0x20, +}; + +struct MenuItem { + uint32_t item_id; + std::u16string name; + std::u16string description; + uint32_t flags; + + MenuItem(uint32_t item_id, const std::u16string& name, + const std::u16string& description, uint32_t flags); +}; diff --git a/NetworkAddresses.cc b/NetworkAddresses.cc new file mode 100644 index 00000000..6925cac8 --- /dev/null +++ b/NetworkAddresses.cc @@ -0,0 +1,79 @@ +#include "NetworkAddresses.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace std; + + + +uint32_t resolve_address(const char* address) { + struct addrinfo *res0; + if (getaddrinfo(address, NULL, NULL, &res0)) { + auto e = string_for_error(errno); + throw runtime_error(string_printf("can\'t resolve hostname %s: %s", address, + e.c_str())); + } + + std::unique_ptr res0_unique( + res0, freeaddrinfo); + struct addrinfo *res4 = NULL; + for (struct addrinfo* res = res0; res; res = res->ai_next) { + if (res->ai_family == AF_INET) { + res4 = res; + } + } + if (!res4) { + throw runtime_error(string_printf( + "can\'t resolve hostname %s: no usable data", address)); + } + + struct sockaddr_in* res_sin = (struct sockaddr_in*)res4->ai_addr; + return res_sin->sin_addr.s_addr; +} + +set get_local_address_list() { + struct ifaddrs* ifa_raw; + if (getifaddrs(&ifa_raw)) { + auto s = string_for_error(errno); + throw runtime_error(string_printf("failed to get interface addresses: %s", s.c_str())); + } + + unique_ptr ifa(ifa_raw, freeifaddrs); + + set ret; + for (struct ifaddrs* i = ifa.get(); i; i = i->ifa_next) { + if (!i->ifa_addr) { + continue; + } + + auto* sin = reinterpret_cast(i->ifa_addr); + if (sin->sin_family != AF_INET) { + continue; + } + + ret.emplace(bswap32(sin->sin_addr.s_addr)); + } + + return ret; +} + +bool is_local_address(uint32_t addr) { + uint8_t net = addr & 0xFF; + if ((net != 127) && (net != 172) && (net != 10) && (net != 192)) { + return false; + } + return true; +} diff --git a/NetworkAddresses.hh b/NetworkAddresses.hh new file mode 100644 index 00000000..704c7684 --- /dev/null +++ b/NetworkAddresses.hh @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include + + + +// PSO is IPv4-only, so we just treat addresses as uint32_t everywhere because +// it's easier + +uint32_t resolve_address(const char* address); +std::set get_local_address_list(); +uint32_t get_connected_address(int fd); +bool is_local_address(uint32_t daddr); diff --git a/PSOEncryption.cc b/PSOEncryption.cc new file mode 100644 index 00000000..2ab0bbbe --- /dev/null +++ b/PSOEncryption.cc @@ -0,0 +1,586 @@ +#include "PSOEncryption.hh" + +#include +#include + +#include + +using namespace std; + + + +// TODO: fix style in this file, especially in psobb functions + + + +// most ciphers used by pso are symmetric; alias decrypt to encrypt by default +void PSOEncryption::decrypt(void* data, size_t size) { + this->encrypt(data, size); +} + + + +void PSOPCEncryption::update_stream() { + uint32_t esi, edi, eax, ebp, edx; + edi = 1; + edx = 0x18; + eax = edi; + while (edx > 0) { + esi = this->stream[eax + 0x1F]; + ebp = this->stream[eax]; + ebp = ebp - esi; + this->stream[eax] = ebp; + eax++; + edx--; + } + edi = 0x19; + edx = 0x1F; + eax = edi; + while (edx > 0) { + esi = this->stream[eax - 0x18]; + ebp = this->stream[eax]; + ebp = ebp - esi; + this->stream[eax] = ebp; + eax++; + edx--; + } +} + +PSOPCEncryption::PSOPCEncryption(uint32_t seed) : offset(1) { + uint32_t esi, ebx, edi, eax, edx, var1; + esi = 1; + ebx = seed; + edi = 0x15; + this->stream[56] = ebx; + this->stream[55] = ebx; + while (edi <= 0x46E) { + eax = edi; + var1 = eax / 55; + edx = eax - (var1 * 55); + ebx = ebx - esi; + edi = edi + 0x15; + this->stream[edx] = esi; + esi = ebx; + ebx = this->stream[edx]; + } + for (size_t x = 0; x < 5; x++) { + this->update_stream(); + } +} + +uint32_t PSOPCEncryption::next() { + if (this->offset == PC_STREAM_LENGTH) { + this->update_stream(); + this->offset = 1; + } + return this->stream[this->offset++]; +} + +void PSOPCEncryption::encrypt(void* vdata, size_t size) { + if (size & 3) { + throw invalid_argument("size must be a multiple of 4"); + } + size >>= 2; + + uint32_t* data = reinterpret_cast(vdata); + for (size_t x = 0; x < size; x++) { + data[x] ^= this->next(); + } +} + + + +void PSOGCEncryption::update_stream() { + uint32_t r5, r6, r7; + r5 = 0; + r6 = 489; + r7 = 0; + + while (r6 != GC_STREAM_LENGTH) { + this->stream[r5++] ^= this->stream[r6++]; + } + + while (r5 != GC_STREAM_LENGTH) { + this->stream[r5++] ^= this->stream[r7++]; + } + + this->offset = 0; +} + +uint32_t PSOGCEncryption::next() { + this->offset++; + if (this->offset == GC_STREAM_LENGTH) { + this->update_stream(); + } + return this->stream[this->offset]; +} + +PSOGCEncryption::PSOGCEncryption(uint32_t seed) : offset(0) { + uint32_t x, y, basekey, source1, source2, source3; + basekey = 0; + + for (x = 0; x <= 16; x++) { + for (y = 0; y < 32; y++) { + seed = seed * 0x5D588B65; + basekey = basekey >> 1; + seed++; + if (seed & 0x80000000) { + basekey = basekey | 0x80000000; + } else { + basekey = basekey & 0x7FFFFFFF; + } + } + this->stream[this->offset] = basekey; + } + + this->stream[this->offset] = (((this->stream[0] >> 9) ^ (this->stream[this->offset] << 23)) ^ this->stream[15]); + + source1 = 0; + source2 = 1; + source3 = this->offset - 1; + while (this->offset != GC_STREAM_LENGTH) { + this->stream[this->offset] = (this->stream[source3] ^ (((this->stream[source1] << 23) & 0xFF800000) ^ ((this->stream[source2] >> 9) & 0x007FFFFF))); + this->offset++; + source1++; + source2++; + source3++; + } + + for (size_t x = 0; x < 4; x++) { + this->update_stream(); + } +} + +void PSOGCEncryption::encrypt(void* vdata, size_t size) { + if (size & 3) { + throw invalid_argument("size must be a multiple of 4"); + } + size >>= 2; + + uint32_t* data = reinterpret_cast(vdata); + for (size_t x = 0; x < size; x++) { + data[x] ^= this->next(); + } +} + + + +static const uint32_t psobb_private_key[0x400] = { + 0xD1310BA6,0x98DFB5AC,0x2FFD72DB,0xD01ADFB7,0xB8E1AFED,0x6A267E96,0xBA7C9045,0xF12C7F99, + 0x24A19947,0xB3916CF7,0x0801F2E2,0x858EFC16,0x636920D8,0x71574E69,0xA458FEA3,0xF4933D7E, + 0x0D95748F,0x728EB658,0x718BCD58,0x82154AEE,0x7B54A41D,0xC25A59B5,0x9C30D539,0x2AF26013, + 0xC5D1B023,0x286085F0,0xCA417918,0xB8DB38EF,0x8E79DCB0,0x603A180E,0x6C9E0E8B,0xB01E8A3E, + 0xD71577C1,0xBD314B27,0x78AF2FDA,0x55605C60,0xE65525F3,0xAA55AB94,0x57489862,0x63E81440, + 0x55CA396A,0x2AAB10B6,0xB4CC5C34,0x1141E8CE,0xA15486AF,0x7C72E993,0xB3EE1411,0x636FBC2A, + 0x2BA9C55D,0x741831F6,0xCE5C3E16,0x9B87931E,0xAFD6BA33,0x6C24CF5C,0x7A325381,0x28958677, + 0x3B8F4898,0x6B4BB9AF,0xC4BFE81B,0x66282193,0x61D809CC,0xFB21A991,0x487CAC60,0x5DEC8032, + 0xEF845D5D,0xE98575B1,0xDC262302,0xEB651B88,0x23893E81,0xD396ACC5,0x0F6D6FF3,0x83F44239, + 0x2E0B4482,0xA4842004,0x69C8F04A,0x9E1F9B5E,0x21C66842,0xF6E96C9A,0x670C9C61,0xABD388F0, + 0x6A51A0D2,0xD8542F68,0x960FA728,0xAB5133A3,0x6EEF0B6C,0x137A3BE4,0xBA3BF050,0x7EFB2A98, + 0xA1F1651D,0x39AF0176,0x66CA593E,0x82430E88,0x8CEE8619,0x456F9FB4,0x7D84A5C3,0x3B8B5EBE, + 0xE06F75D8,0x85C12073,0x401A449F,0x56C16AA6,0x4ED3AA62,0x363F7706,0x1BFEDF72,0x429B023D, + 0x37D0D724,0xD00A1248,0xDB0FEAD3,0x49F1C09B,0x075372C9,0x80991B7B,0x25D479D8,0xF6E8DEF7, + 0xE3FE501A,0xB6794C3B,0x976CE0BD,0x04C006BA,0xC1A94FB6,0x409F60C4,0x5E5C9EC2,0x196A2463, + 0x68FB6FAF,0x3E6C53B5,0x1339B2EB,0x3B52EC6F,0x6DFC511F,0x9B30952C,0xCC814544,0xAF5EBD09, + 0xBEE3D004,0xDE334AFD,0x660F2807,0x192E4BB3,0xC0CBA857,0x45C8740F,0xD20B5F39,0xB9D3FBDB, + 0x5579C0BD,0x1A60320A,0xD6A100C6,0x402C7279,0x679F25FE,0xFB1FA3CC,0x8EA5E9F8,0xDB3222F8, + 0x3C7516DF,0xFD616B15,0x2F501EC8,0xAD0552AB,0x323DB5FA,0xFD238760,0x53317B48,0x3E00DF82, + 0x9E5C57BB,0xCA6F8CA0,0x1A87562E,0xDF1769DB,0xD542A8F6,0x287EFFC3,0xAC6732C6,0x8C4F5573, + 0x695B27B0,0xBBCA58C8,0xE1FFA35D,0xB8F011A0,0x10FA3D98,0xFD2183B8,0x4AFCB56C,0x2DD1D35B, + 0x9A53E479,0xB6F84565,0xD28E49BC,0x4BFB9790,0xE1DDF2DA,0xA4CB7E33,0x62FB1341,0xCEE4C6E8, + 0xEF20CADA,0x36774C01,0xD07E9EFE,0x2BF11FB4,0x95DBDA4D,0xAE909198,0xEAAD8E71,0x6B93D5A0, + 0xD08ED1D0,0xAFC725E0,0x8E3C5B2F,0x8E7594B7,0x8FF6E2FB,0xF2122B64,0x8888B812,0x900DF01C, + 0x4FAD5EA0,0x688FC31C,0xD1CFF191,0xB3A8C1AD,0x2F2F2218,0xBE0E1777,0xEA752DFE,0x8B021FA1, + 0xE5A0CC0F,0xB56F74E8,0x18ACF3D6,0xCE89E299,0xB4A84FE0,0xFD13E0B7,0x7CC43B81,0xD2ADA8D9, + 0x165FA266,0x80957705,0x93CC7314,0x211A1477,0xE6AD2065,0x77B5FA86,0xC75442F5,0xFB9D35CF, + 0xEBCDAF0C,0x7B3E89A0,0xD6411BD3,0xAE1E7E49,0x00250E2D,0x2071B35E,0x226800BB,0x57B8E0AF, + 0x2464369B,0xF009B91E,0x5563911D,0x59DFA6AA,0x78C14389,0xD95A537F,0x207D5BA2,0x02E5B9C5, + 0x83260376,0x6295CFA9,0x11C81968,0x4E734A41,0xB3472DCA,0x7B14A94A,0x1B510052,0x9A532915, + 0xD60F573F,0xBC9BC6E4,0x2B60A476,0x81E67400,0x08BA6FB5,0x571BE91F,0xF296EC6B,0x2A0DD915, + 0xB6636521,0xE7B9F9B6,0xFF34052E,0xC5855664,0x53B02D5D,0xA99F8FA1,0x08BA4799,0x6E85076A, + 0x4B7A70E9,0xB5B32944,0xDB75092E,0xC4192623,0xAD6EA6B0,0x49A7DF7D,0x9CEE60B8,0x8FEDB266, + 0xECAA8C71,0x699A17FF,0x5664526C,0xC2B19EE1,0x193602A5,0x75094C29,0xA0591340,0xE4183A3E, + 0x3F54989A,0x5B429D65,0x6B8FE4D6,0x99F73FD6,0xA1D29C07,0xEFE830F5,0x4D2D38E6,0xF0255DC1, + 0x4CDD2086,0x8470EB26,0x6382E9C6,0x021ECC5E,0x09686B3F,0x3EBAEFC9,0x3C971814,0x6B6A70A1, + 0x687F3584,0x52A0E286,0xB79C5305,0xAA500737,0x3E07841C,0x7FDEAE5C,0x8E7D44EC,0x5716F2B8, + 0xB03ADA37,0xF0500C0D,0xF01C1F04,0x0200B3FF,0xAE0CF51A,0x3CB574B2,0x25837A58,0xDC0921BD, + 0xD19113F9,0x7CA92FF6,0x94324773,0x22F54701,0x3AE5E581,0x37C2DADC,0xC8B57634,0x9AF3DDA7, + 0xA9446146,0x0FD0030E,0xECC8C73E,0xA4751E41,0xE238CD99,0x3BEA0E2F,0x3280BBA1,0x183EB331, + 0x4E548B38,0x4F6DB908,0x6F420D03,0xF60A04BF,0x2CB81290,0x24977C79,0x5679B072,0xBCAF89AF, + 0xDE9A771F,0xD9930810,0xB38BAE12,0xDCCF3F2E,0x5512721F,0x2E6B7124,0x501ADDE6,0x9F84CD87, + 0x7A584718,0x7408DA17,0xBC9F9ABC,0xE94B7D8C,0xEC7AEC3A,0xDB851DFA,0x63094366,0xC464C3D2, + 0xEF1C1847,0x3215D908,0xDD433B37,0x24C2BA16,0x12A14D43,0x2A65C451,0x50940002,0x133AE4DD, + 0x71DFF89E,0x10314E55,0x81AC77D6,0x5F11199B,0x043556F1,0xD7A3C76B,0x3C11183B,0x5924A509, + 0xF28FE6ED,0x97F1FBFA,0x9EBABF2C,0x1E153C6E,0x86E34570,0xEAE96FB1,0x860E5E0A,0x5A3E2AB3, + 0x771FE71C,0x4E3D06FA,0x2965DCB9,0x99E71D0F,0x803E89D6,0x5266C825,0x2E4CC978,0x9C10B36A, + 0xC6150EBA,0x94E2EA78,0xA5FC3C53,0x1E0A2DF4,0xF2F74EA7,0x361D2B3D,0x1939260F,0x19C27960, + 0x5223A708,0xF71312B6,0xEBADFE6E,0xEAC31F66,0xE3BC4595,0xA67BC883,0xB17F37D1,0x018CFF28, + 0xC332DDEF,0xBE6C5AA5,0x65582185,0x68AB9802,0xEECEA50F,0xDB2F953B,0x2AEF7DAD,0x5B6E2F84, + 0x1521B628,0x29076170,0xECDD4775,0x619F1510,0x13CCA830,0xEB61BD96,0x0334FE1E,0xAA0363CF, + 0xB5735C90,0x4C70A239,0xD59E9E0B,0xCBAADE14,0xEECC86BC,0x60622CA7,0x9CAB5CAB,0xB2F3846E, + 0x648B1EAF,0x19BDF0CA,0xA02369B9,0x655ABB50,0x40685A32,0x3C2AB4B3,0x319EE9D5,0xC021B8F7, + 0x9B540B19,0x875FA099,0x95F7997E,0x623D7DA8,0xF837889A,0x97E32D77,0x11ED935F,0x16681281, + 0x0E358829,0xC7E61FD6,0x96DEDFA1,0x7858BA99,0x57F584A5,0x1B227263,0x9B83C3FF,0x1AC24696, + 0xCDB30AEB,0x532E3054,0x8FD948E4,0x6DBC3128,0x58EBF2EF,0x34C6FFEA,0xFE28ED61,0xEE7C3C73, + 0x5D4A14D9,0xE864B7E3,0x42105D14,0x203E13E0,0x45EEE2B6,0xA3AAABEA,0xDB6C4F15,0xFACB4FD0, + 0xC742F442,0xEF6ABBB5,0x654F3B1D,0x41CD2105,0xD81E799E,0x86854DC7,0xE44B476A,0x3D816250, + 0xCF62A1F2,0x5B8D2646,0xFC8883A0,0xC1C7B6A3,0x7F1524C3,0x69CB7492,0x47848A0B,0x5692B285, + 0x095BBF00,0xAD19489D,0x1462B174,0x23820E00,0x58428D2A,0x0C55F5EA,0x1DADF43E,0x233F7061, + 0x3372F092,0x8D937E41,0xD65FECF1,0x6C223BDB,0x7CDE3759,0xCBEE7460,0x4085F2A7,0xCE77326E, + 0xA6078084,0x19F8509E,0xE8EFD855,0x61D99735,0xA969A7AA,0xC50C06C2,0x5A04ABFC,0x800BCADC, + 0x9E447A2E,0xC3453484,0xFDD56705,0x0E1E9EC9,0xDB73DBD3,0x105588CD,0x675FDA79,0xE3674340, + 0xC5C43465,0x713E38D8,0x3D28F89E,0xF16DFF20,0x153E21E7,0x8FB03D4A,0xE6E39F2B,0xDB83ADF7, + 0xE93D5A68,0x948140F7,0xF64C261C,0x94692934,0x411520F7,0x7602D4F7,0xBCF46B2E,0xD4A20068, + 0xD4082471,0x3320F46A,0x43B7D4B7,0x500061AF,0x1E39F62E,0x97244546,0x14214F74,0xBF8B8840, + 0x4D95FC1D,0x96B591AF,0x70F4DDD3,0x66A02F45,0xBFBC09EC,0x03BD9785,0x7FAC6DD0,0x31CB8504, + 0x96EB27B3,0x55FD3941,0xDA2547E6,0xABCA0A9A,0x28507825,0x530429F4,0x0A2C86DA,0xE9B66DFB, + 0x68DC1462,0xD7486900,0x680EC0A4,0x27A18DEE,0x4F3FFEA2,0xE887AD8C,0xB58CE006,0x7AF4D6B6, + 0xAACE1E7C,0xD3375FEC,0xCE78A399,0x406B2A42,0x20FE9E35,0xD9F385B9,0xEE39D7AB,0x3B124E8B, + 0x1DC9FAF7,0x4B6D1856,0x26A36631,0xEAE397B2,0x3A6EFA74,0xDD5B4332,0x6841E7F7,0xCA7820FB, + 0xFB0AF54E,0xD8FEB397,0x454056AC,0xBA489527,0x55533A3A,0x20838D87,0xFE6BA9B7,0xD096954B, + 0x55A867BC,0xA1159A58,0xCCA92963,0x99E1DB33,0xA62A4A56,0x3F3125F9,0x5EF47E1C,0x9029317C, + 0xFDF8E802,0x04272F70,0x80BB155C,0x05282CE3,0x95C11548,0xE4C66D22,0x48C1133F,0xC70F86DC, + 0x07F9C9EE,0x41041F0F,0x404779A4,0x5D886E17,0x325F51EB,0xD59BC0D1,0xF2BCC18F,0x41113564, + 0x257B7834,0x602A9C60,0xDFF8E8A3,0x1F636C1B,0x0E12B4C2,0x02E1329E,0xAF664FD1,0xCAD18115, + 0x6B2395E0,0x333E92E1,0x3B240B62,0xEEBEB922,0x85B2A20E,0xE6BA0D99,0xDE720C8C,0x2DA2F728, + 0xD0127845,0x95B794FD,0x647D0862,0xE7CCF5F0,0x5449A36F,0x877D48FA,0xC39DFD27,0xF33E8D1E, + 0x0A476341,0x992EFF74,0x3A6F6EAB,0xF4F8FD37,0xA812DC60,0xA1EBDDF8,0x991BE14C,0xDB6E6B0D, + 0xC67B5510,0x6D672C37,0x2765D43B,0xDCD0E804,0xF1290DC7,0xCC00FFA3,0xB5390F92,0x690FED0B, + 0x667B9FFB,0xCEDB7D9C,0xA091CF0B,0xD9155EA3,0xBB132F88,0x515BAD24,0x7B9479BF,0x763BD6EB, + 0x37392EB3,0xCC115979,0x8026E297,0xF42E312D,0x6842ADA7,0xC66A2B3B,0x12754CCC,0x782EF11C, + 0x6A124237,0xB79251E7,0x06A1BBE6,0x4BFB6350,0x1A6B1018,0x11CAEDFA,0x3D25BDD8,0xE2E1C3C9, + 0x44421659,0x0A121386,0xD90CEC6E,0xD5ABEA2A,0x64AF674E,0xDA86A85F,0xBEBFE988,0x64E4C3FE, + 0x9DBC8057,0xF0F7C086,0x60787BF8,0x6003604D,0xD1FD8346,0xF6381FB0,0x7745AE04,0xD736FCCC, + 0x83426B33,0xF01EAB71,0xB0804187,0x3C005E5F,0x77A057BE,0xBDE8AE24,0x55464299,0xBF582E61, + 0x4E58F48F,0xF2DDFDA2,0xF474EF38,0x8789BDC2,0x5366F9C3,0xC8B38E74,0xB475F255,0x46FCD9B9, + 0x7AEB2661,0x8B1DDF84,0x846A0E79,0x915F95E2,0x466E598E,0x20B45770,0x8CD55591,0xC902DE4C, + 0xB90BACE1,0xBB8205D0,0x11A86248,0x7574A99E,0xB77F19B6,0xE0A9DC09,0x662D09A1,0xC4324633, + 0xE85A1F02,0x09F0BE8C,0x4A99A025,0x1D6EFE10,0x1AB93D1D,0x0BA5A4DF,0xA186F20F,0x2868F169, + 0xDCB7DA83,0x573906FE,0xA1E2CE9B,0x4FCD7F52,0x50115E01,0xA70683FA,0xA002B5C4,0x0DE6D027, + 0x9AF88C27,0x773F8641,0xC3604C06,0x61A806B5,0xF0177A28,0xC0F586E0,0x006058AA,0x30DC7D62, + 0x11E69ED7,0x2338EA63,0x53C2DD94,0xC2C21634,0xBBCBEE56,0x90BCB6DE,0xEBFC7DA1,0xCE591D76, + 0x6F05E409,0x4B7C0188,0x39720A3D,0x7C927C24,0x86E3725F,0x724D9DB9,0x1AC15BB4,0xD39EB8FC, + 0xED545578,0x08FCA5B5,0xD83D7CD3,0x4DAD0FC4,0x1E50EF5E,0xB161E6F8,0xA28514D9,0x6C51133C, + 0x6FD5C7E7,0x56E14EC4,0x362ABFCE,0xDDC6C837,0xD79A3234,0x92638212,0x670EFA8E,0x406000E0, + 0x3A39CE37,0xD3FAF5CF,0xABC27737,0x5AC52D1B,0x5CB0679E,0x4FA33742,0xD3822740,0x99BC9BBE, + 0xD5118E9D,0xBF0F7315,0xD62D1C7E,0xC700C47B,0xB78C1B6B,0x21A19045,0xB26EB1BE,0x6A366EB4, + 0x5748AB2F,0xBC946E79,0xC6A376D2,0x6549C2C8,0x530FF8EE,0x468DDE7D,0xD5730A1D,0x4CD04DC6, + 0x2939BBDB,0xA9BA4650,0xAC9526E8,0xBE5EE304,0xA1FAD5F0,0x6A2D519A,0x63EF8CE2,0x9A86EE22, + 0xC089C2B8,0x43242EF6,0xA51E03AA,0x9CF2D0A4,0x83C061BA,0x9BE96A4D,0x8FE51550,0xBA645BD6, + 0x2826A2F9,0xA73A3AE1,0x4BA99586,0xEF5562E9,0xC72FEFD3,0xF752F7DA,0x3F046F69,0x77FA0A59, + 0x80E4A915,0x87B08601,0x9B09E6AD,0x3B3EE593,0xE990FD5A,0x9E34D797,0x2CF0B7D9,0x022B8B51, + 0x96D5AC3A,0x017DA67D,0xD1CF3ED6,0x7C7D2D28,0x1F9F25CF,0xADF2B89B,0x5AD6B472,0x5A88F54C, + 0xE029AC71,0xE019A5E6,0x47B0ACFD,0xED93FA9B,0xE8D3C48D,0x283B57CC,0xF8D56629,0x79132E28, + 0x785F0191,0xED756055,0xF7960E44,0xE3D35E8C,0x15056DD4,0x88F46DBA,0x03A16125,0x0564F0BD, + 0xC3EB9E15,0x3C9057A2,0x97271AEC,0xA93A072A,0x1B3F6D9B,0x1E6321F5,0xF59C66FB,0x26DCF319, + 0x7533D928,0xB155FDF5,0x03563482,0x8ABA3CBB,0x28517711,0xC20AD9F8,0xABCC5167,0xCCAD925F, + 0x4DE81751,0x3830DC8E,0x379D5862,0x9320F991,0xEA7A90C2,0xFB3E7BCE,0x5121CE64,0x774FBE32, + 0xA8B6E37E,0xC3293D46,0x48DE5369,0x6413E680,0xA2AE0810,0xDD6DB224,0x69852DFD,0x09072166, + 0xB39A460A,0x6445C0DD,0x586CDECF,0x1C20C8AE,0x5BBEF7DD,0x1B588D40,0xCCD2017F,0x6BB4E3BB, + 0xDDA26A7E,0x3A59FF45,0x3E350A44,0xBCB4CDD5,0x72EACEA8,0xFA6484BB,0x8D6612AE,0xBF3C6F47, + 0xD29BE463,0x542F5D9E,0xAEC2771B,0xF64E6370,0x740E0D8D,0xE75B1357,0xF8721671,0xAF537D5D, + 0x4040CB08,0x4EB4E2CC,0x34D2466A,0x0115AF84,0xE1B00428,0x95983A1D,0x06B89FB4,0xCE6EA048, + 0x6F3F3B82,0x3520AB82,0x011A1D4B,0x277227F8,0x611560B1,0xE7933FDC,0xBB3A792B,0x344525BD, + 0xA08839E1,0x51CE794B,0x2F32C9B7,0xA01FBAC9,0xE01CC87E,0xBCC7D1F6,0xCF0111C3,0xA1E8AAC7, + 0x1A908749,0xD44FBD9A,0xD0DADECB,0xD50ADA38,0x0339C32A,0xC6913667,0x8DF9317C,0xE0B12B4F, + 0xF79E59B7,0x43F5BB3A,0xF2D519FF,0x27D9459C,0xBF97222C,0x15E6FC2A,0x0F91FC71,0x9B941525, + 0xFAE59361,0xCEB69CEB,0xC2A86459,0x12BAA8D1,0xB6C1075E,0xE3056A0C,0x10D25065,0xCB03A442, + 0xE0EC6E0E,0x1698DB3B,0x4C98A0BE,0x3278E964,0x9F1F9532,0xE0D392DF,0xD3A0342B,0x8971F21E, + 0x1B0A7441,0x4BA3348C,0xC5BE7120,0xC37632D8,0xDF359F8D,0x9B992F2E,0xE60B6F47,0x0FE3F11D, + 0xE54CDA54,0x1EDAD891,0xCE6279CF,0xCD3E7E6F,0x1618B166,0xFD2C1D05,0x848FD2C5,0xF6FB2299, + 0xF523F357,0xA6327623,0x93A83531,0x56CCCD02,0xACF08162,0x5A75EBB5,0x6E163697,0x88D273CC, + 0xDE966292,0x81B949D0,0x4C50901B,0x71C65614,0xE6C6C7BD,0x327A140A,0x45E1D006,0xC3F27B9A, + 0xC9AA53FD,0x62A80F00,0xBB25BFE2,0x35BDD2F6,0x71126905,0xB2040222,0xB6CBCF7C,0xCD769C2B, + 0x53113EC0,0x1640E3D3,0x38ABBD60,0x2547ADF0,0xBA38209C,0xF746CE76,0x77AFA1C5,0x20756060, + 0x85CBFE4E,0x8AE88DD8,0x7AAAF9B0,0x4CF9AA7E,0x1948C25C,0x02FB8A8C,0x01C36AE4,0xD6EBE1F9, + 0x90D4F869,0xA65CDEA0,0x3F09252D,0xC208E69F,0xB74E6132,0xCE77E25B,0x578FDFE3,0x3AC372E6}; + +void PSOBBEncryption::decrypt(void* vdata, size_t size) { + if (size & 7) { + throw invalid_argument("size must be a multiple of 8"); + } + size >>= 3; + + uint32_t* data = reinterpret_cast(vdata); + uint32_t eax, ecx, edx, ebx, ebp, esi, edi; + + edx = 0; + ecx = 0; + eax = 0; + while (edx < size) { + ebx = data[edx]; + ebx = ebx ^ this->stream[5]; + ebp = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312]; + ebp = ebp ^ this->stream[4]; + ebp ^= data[edx + 1]; + edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312]; + edi = edi ^ this->stream[3]; + ebx = ebx ^ edi; + esi = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312]; + ebp = ebp ^ esi ^ this->stream[2]; + edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312]; + edi = edi ^ this->stream[1]; + ebp = ebp ^ this->stream[0]; + ebx = ebx ^ edi; + data[edx] = ebp; + data[edx + 1] = ebx; + edx += 2; + } +} + + +void PSOBBEncryption::encrypt(void* vdata, size_t size) { + if (size & 7) { + throw invalid_argument("size must be a multiple of 8"); + } + size >>= 3; + + uint8_t* data = reinterpret_cast(vdata); + uint32_t eax, ecx, edx, ebx, ebp, esi, edi; + + edx = 0; + ecx = 0; + eax = 0; + while (edx < size) { + ebx = data[edx]; + ebx = ebx ^ this->stream[0]; + ebp = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312]; + ebp = ebp ^ this->stream[1]; + ebp ^= data[edx + 1]; + edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312]; + edi = edi ^ this->stream[2]; + ebx = ebx ^ edi; + esi = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312]; + ebp = ebp ^ esi ^ this->stream[3]; + edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112]) + ^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312]; + edi = edi ^ this->stream[4]; + ebp = ebp ^ this->stream[5]; + ebx = ebx ^ edi; + data[edx] = ebp; + data[edx + 1] = ebx; + edx += 2; + } +} + +PSOBBEncryption::PSOBBEncryption(const void* seed) : offset(0) { + uint32_t eax, ecx, edx, ebx, ebp, esi, edi, ou, x; + + uint8_t s[48]; + memcpy(s, seed, sizeof(s)); + for (size_t x = 0; x < 48; x += 3) { + s[x] ^= 0x19; + s[x + 1] ^= 0x16; + s[x + 2] ^= 0x18; + } + + this->stream[0] = 0x243F6A88; + this->stream[1] = 0x85A308D3; + this->stream[2] = 0x13198A2E; + this->stream[3] = 0x03707344; + this->stream[4] = 0xA4093822; + this->stream[5] = 0x299F31D0; + this->stream[6] = 0x082EFA98; + this->stream[7] = 0xEC4E6C89; + this->stream[8] = 0x452821E6; + this->stream[9] = 0x38D01377; + this->stream[10] = 0xBE5466CF; + this->stream[11] = 0x34E90C6C; + this->stream[12] = 0xC0AC29B7; + this->stream[13] = 0xC97C50DD; + this->stream[14] = 0x3F84D5B5; + this->stream[15] = 0xB5470917; + this->stream[16] = 0x9216D5D9; + this->stream[17] = 0x8979FB1B; + memcpy(&this->stream[18], psobb_private_key, sizeof(psobb_private_key)); + + ecx = 0; + ebx = 0; + + while (ebx < 0x12) { + ebp = static_cast(s[ecx]) << 0x18; + eax = ecx + 1; + edx = eax - ((eax / 48) * 48); + eax = (static_cast(s[edx]) << 0x10) & 0xFF0000; + ebp = (ebp | eax) & 0xffff00ff; + eax = ecx + 2; + edx = eax - ((eax / 48) * 48); + eax = (static_cast(s[edx]) << 0x08) & 0xFF00; + ebp = (ebp | eax) & 0xffffff00; + eax = ecx + 3; + ecx = ecx + 4; + edx = eax - ((eax / 48) * 48); + eax = static_cast(s[edx]); + ebp = ebp | eax; + eax = ecx; + edx = eax - ((eax / 48) * 48); + this->stream[ebx] ^= ebp; + ecx = edx; + ebx++; + } + + ebp = 0; + esi = 0; + ecx = 0; + edi = 0; + ebx = 0; + edx = 0x48; + + while (edi < edx) { + esi = esi ^ this->stream[0]; + eax = esi >> 0x18; + ebx = (esi >> 0x10) & 0xff; + eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112]; + ebx = (esi >> 8) & 0xFF; + eax = eax ^ this->stream[ebx + 0x212]; + ebx = esi & 0xFF; + eax = eax + this->stream[ebx + 0x312]; + + eax = eax ^ this->stream[1]; + ecx = ecx ^ eax; + ebx = ecx >> 0x18; + eax = (ecx >> 0x10) & 0xFF; + ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112]; + eax = (ecx >> 8) & 0xFF; + ebx = ebx ^ this->stream[eax + 0x212]; + eax = ecx & 0xFF; + ebx = ebx + this->stream[eax + 0x312]; + + for (x = 0; x <= 5; x++) { + ebx = ebx ^ this->stream[(x * 2) + 2]; + esi = esi ^ ebx; + ebx = esi >> 0x18; + eax = (esi >> 0x10) & 0xFF; + ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112]; + eax = (esi >> 8) & 0xFF; + ebx = ebx ^ this->stream[eax + 0x212]; + eax = esi & 0xFF; + ebx = ebx + this->stream[eax + 0x312]; + + ebx = ebx ^ this->stream[(x * 2) + 3]; + ecx = ecx ^ ebx; + ebx = ecx >> 0x18; + eax = (ecx >> 0x10) & 0xFF; + ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112]; + eax = (ecx >> 8) & 0xFF; + ebx = ebx ^ this->stream[eax + 0x212]; + eax = ecx & 0xff; + ebx = ebx + this->stream[eax + 0x312]; + } + + ebx = ebx ^ this->stream[14]; + esi = esi ^ ebx; + eax = esi >> 0x18; + ebx = (esi >> 0x10) & 0xFF; + eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112]; + ebx = (esi >> 8) & 0xff; + eax = eax ^ this->stream[ebx + 0x212]; + ebx = esi & 0xff; + eax = eax + this->stream[ebx + 0x312]; + + eax = eax ^ this->stream[15]; + eax = ecx ^ eax; + ecx = eax >> 0x18; + ebx = (eax >> 0x10) & 0xFF; + ecx = this->stream[ecx + 0x12] + this->stream[ebx + 0x112]; + ebx = (eax >> 8) & 0xFF; + ecx = ecx ^ this->stream[ebx + 0x212]; + ebx = eax & 0xFF; + ecx = ecx + this->stream[ebx + 0x312]; + + ecx = ecx ^ this->stream[16]; + ecx = ecx ^ esi; + esi = this->stream[17]; + esi = esi ^ eax; + this->stream[(edi / 4)] = esi; + this->stream[(edi / 4)+1] = ecx; + edi = edi + 8; + } + + eax = 0; + edx = 0; + ou = 0; + while (ou < 0x1000) { + edi = 0x48; + edx = 0x448; + + while (edi < edx) { + esi = esi ^ this->stream[0]; + eax = esi >> 0x18; + ebx = (esi >> 0x10) & 0xff; + eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112]; + ebx = (esi >> 8) & 0xFF; + eax = eax ^ this->stream[ebx + 0x212]; + ebx = esi & 0xFF; + eax = eax + this->stream[ebx + 0x312]; + + eax = eax ^ this->stream[1]; + ecx = ecx ^ eax; + ebx = ecx >> 0x18; + eax = (ecx >> 0x10) & 0xFF; + ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112]; + eax = (ecx >> 8) & 0xFF; + ebx = ebx ^ this->stream[eax + 0x212]; + eax = ecx & 0xFF; + ebx = ebx + this->stream[eax + 0x312]; + + for (x = 0; x <= 5; x++) { + ebx = ebx ^ this->stream[(x * 2) + 2]; + esi = esi ^ ebx; + ebx = esi >> 0x18; + eax = (esi >> 0x10) & 0xFF; + ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112]; + eax = (esi >> 8) & 0xFF; + ebx = ebx ^ this->stream[eax + 0x212]; + eax = esi & 0xFF; + ebx = ebx + this->stream[eax + 0x312]; + + ebx = ebx ^ this->stream[(x * 2) + 3]; + ecx = ecx ^ ebx; + ebx = ecx >> 0x18; + eax = (ecx >> 0x10) & 0xFF; + ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112]; + eax = (ecx >> 8) & 0xFF; + ebx = ebx ^ this->stream[eax + 0x212]; + eax = ecx & 0xFF; + ebx = ebx + this->stream[eax + 0x312]; + } + + ebx = ebx ^ this->stream[14]; + esi = esi ^ ebx; + eax = esi >> 0x18; + ebx = (esi >> 0x10) & 0xFF; + eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112]; + ebx = (esi >> 8) & 0xFF; + eax = eax ^ this->stream[ebx + 0x212]; + ebx = esi & 0xFF; + eax = eax + this->stream[ebx + 0x312]; + + eax = eax ^ this->stream[15]; + eax = ecx ^ eax; + ecx = eax >> 0x18; + ebx = (eax >> 0x10) & 0xFF; + ecx = this->stream[ecx + 0x12] + this->stream[ebx + 0x112]; + ebx = (eax >> 8) & 0xFF; + ecx = ecx ^ this->stream[ebx + 0x212]; + ebx = eax & 0xFF; + ecx = ecx + this->stream[ebx + 0x312]; + + ecx = ecx ^ this->stream[16]; + ecx = ecx ^ esi; + esi = this->stream[17]; + esi = esi ^ eax; + this->stream[(ou / 4) + (edi / 4)] = esi; + this->stream[(ou / 4) + (edi / 4) + 1] = ecx; + edi = edi + 8; + } + ou = ou + 0x400; + } +} diff --git a/PSOEncryption.hh b/PSOEncryption.hh new file mode 100644 index 00000000..e64389c5 --- /dev/null +++ b/PSOEncryption.hh @@ -0,0 +1,61 @@ +#include +#include + + + +#define PC_STREAM_LENGTH 57 +#define GC_STREAM_LENGTH 521 +#define BB_STREAM_LENGTH 1042 + +class PSOEncryption { +public: + virtual ~PSOEncryption() = default; + + virtual void encrypt(void* data, size_t size) = 0; + virtual void decrypt(void* data, size_t size); + +protected: + PSOEncryption() = default; +}; + +class PSOPCEncryption : public PSOEncryption { +public: + explicit PSOPCEncryption(uint32_t seed); + + virtual void encrypt(void* data, size_t size); + +private: + void update_stream(); + uint32_t next(); + + uint32_t stream[PC_STREAM_LENGTH]; + uint16_t offset; +}; + +class PSOGCEncryption : public PSOEncryption { +public: + explicit PSOGCEncryption(uint32_t key); + + virtual void encrypt(void* data, size_t size); + +private: + void update_stream(); + uint32_t next(); + + uint32_t stream[GC_STREAM_LENGTH]; + uint16_t offset; +}; + +class PSOBBEncryption : public PSOEncryption { +public: + explicit PSOBBEncryption(const void* key); + + virtual void encrypt(void* data, size_t size); + virtual void decrypt(void* data, size_t size); + +private: + void update_stream(); + + uint32_t stream[BB_STREAM_LENGTH]; + uint16_t offset; +}; diff --git a/PSOProtocol.cc b/PSOProtocol.cc new file mode 100644 index 00000000..22b8a65d --- /dev/null +++ b/PSOProtocol.cc @@ -0,0 +1,43 @@ +#include "PSOProtocol.hh" + + + +uint16_t PSOCommandHeader::command(GameVersion version) const { + switch (version) { + case GameVersion::DC: + case GameVersion::GC: + return reinterpret_cast(this)->command; + case GameVersion::PC: + case GameVersion::Patch: + return reinterpret_cast(this)->command; + case GameVersion::BB: + return reinterpret_cast(this)->command; + } +} + +uint16_t PSOCommandHeader::size(GameVersion version) const { + switch (version) { + case GameVersion::DC: + case GameVersion::GC: + return reinterpret_cast(this)->size; + case GameVersion::PC: + case GameVersion::Patch: + return reinterpret_cast(this)->size; + case GameVersion::BB: + return reinterpret_cast(this)->size; + } +} + +uint32_t PSOCommandHeader::flag(GameVersion version) const { + switch (version) { + case GameVersion::DC: + case GameVersion::GC: + return reinterpret_cast(this)->flag; + case GameVersion::PC: + case GameVersion::Patch: + return reinterpret_cast(this)->flag; + case GameVersion::BB: + return reinterpret_cast(this)->flag; + } +} + diff --git a/PSOProtocol.hh b/PSOProtocol.hh new file mode 100644 index 00000000..58da2a14 --- /dev/null +++ b/PSOProtocol.hh @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "Version.hh" + +struct PSOCommandHeaderPC { + uint16_t size; + uint8_t command; + uint8_t flag; +}; + +struct PSOCommandHeaderDCGC { + uint8_t command; + uint8_t flag; + uint16_t size; +}; + +struct PSOCommandHeaderBB { + uint16_t size; + uint16_t command; + uint32_t flag; +}; + +union PSOCommandHeader { + PSOCommandHeaderDCGC dc; + PSOCommandHeaderPC pc; + PSOCommandHeaderDCGC gc; + PSOCommandHeaderBB bb; + + uint16_t command(GameVersion version) const; + uint16_t size(GameVersion version) const; + uint32_t flag(GameVersion version) const; +}; + +union PSOSubcommand { + uint8_t byte[4]; + uint16_t word[2]; + uint32_t dword; +}; diff --git a/Player.cc b/Player.cc new file mode 100644 index 00000000..e18f36be --- /dev/null +++ b/Player.cc @@ -0,0 +1,648 @@ +#include "Player.hh" + +#include +#include +#include + +#include +#include + +#include "Text.hh" +#include "Version.hh" + +using namespace std; + + + +// originally there was going to be a language-based header, but then I decided against it. +// these strings were already in use for that parser, so I didn't bother changing them. +#define PLAYER_FILE_SIGNATURE "newserv player file format; 10 sections present; sequential;" +#define ACCOUNT_FILE_SIGNATURE "newserv account file format; 7 sections present; sequential;" + + + +// converts PC/GC player data to BB format +PlayerDispDataBB PlayerDispDataPCGC::to_bb() const { + PlayerDispDataBB bb; + bb.stats.atp = this->stats.atp; + bb.stats.mst = this->stats.mst; + bb.stats.evp = this->stats.evp; + bb.stats.hp = this->stats.hp; + bb.stats.dfp = this->stats.dfp; + bb.stats.ata = this->stats.ata; + bb.stats.lck = this->stats.lck; + bb.unknown1 = this->unknown1; + bb.unknown2[0] = this->unknown2[0]; + bb.unknown2[1] = this->unknown2[1]; + bb.level = this->level; + bb.experience = this->experience; + bb.meseta = this->meseta; + strcpy(bb.guild_card, " 0"); + bb.unknown3[0] = this->unknown3[0]; + bb.unknown3[1] = this->unknown3[1]; + bb.name_color = this->name_color; + bb.extra_model = this->extra_model; + memcpy(&bb.unused, &this->unused, 15); + bb.name_color_checksum = this->name_color_checksum; + bb.section_id = this->section_id; + bb.char_class = this->char_class; + bb.v2_flags = this->v2_flags; + bb.version = this->version; + bb.v1_flags = this->v1_flags; + bb.costume = this->costume; + bb.skin = this->skin; + bb.face = this->face; + bb.head = this->head; + bb.hair = this->hair; + bb.hair_r = this->hair_r; + bb.hair_g = this->hair_g; + bb.hair_b = this->hair_b; + bb.proportion_x = this->proportion_x; + bb.proportion_y = this->proportion_y; + decode_sjis(bb.name, this->name, 0x10); + add_language_marker_inplace(bb.name, 'J', 0x10); + memcpy(&bb.config, &this->config, 0x48); + memcpy(&bb.technique_levels, &this->technique_levels, 0x14); + return bb; +} + +// converts BB player data to PC/GC format +PlayerDispDataPCGC PlayerDispDataBB::to_pcgc() const { + PlayerDispDataPCGC pcgc; + pcgc.stats.atp = this->stats.atp; + pcgc.stats.mst = this->stats.mst; + pcgc.stats.evp = this->stats.evp; + pcgc.stats.hp = this->stats.hp; + pcgc.stats.dfp = this->stats.dfp; + pcgc.stats.ata = this->stats.ata; + pcgc.stats.lck = this->stats.lck; + pcgc.unknown1 = this->unknown1; + pcgc.unknown2[0] = this->unknown2[0]; + pcgc.unknown2[1] = this->unknown2[1]; + pcgc.level = this->level; + pcgc.experience = this->experience; + pcgc.meseta = this->meseta; + pcgc.unknown3[0] = this->unknown3[0]; + pcgc.unknown3[1] = this->unknown3[1]; + pcgc.name_color = this->name_color; + pcgc.extra_model = this->extra_model; + memcpy(&pcgc.unused, &this->unused, 15); + pcgc.name_color_checksum = this->name_color_checksum; + pcgc.section_id = this->section_id; + pcgc.char_class = this->char_class; + pcgc.v2_flags = this->v2_flags; + pcgc.version = this->version; + pcgc.v1_flags = this->v1_flags; + pcgc.costume = this->costume; + pcgc.skin = this->skin; + pcgc.face = this->face; + pcgc.head = this->head; + pcgc.hair = this->hair; + pcgc.hair_r = this->hair_r; + pcgc.hair_g = this->hair_g; + pcgc.hair_b = this->hair_b; + pcgc.proportion_x = this->proportion_x; + pcgc.proportion_y = this->proportion_y; + encode_sjis(pcgc.name, this->name, 0x10); + remove_language_marker_inplace(pcgc.name); + memcpy(&pcgc.config, &this->config, 0x48); + memcpy(&pcgc.technique_levels, &this->technique_levels, 0x14); + return pcgc; +} + +// creates a player preview, which can then be sent to a BB client for character select +PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { + PlayerDispDataBBPreview pre; + pre.level = this->level; + pre.experience = this->experience; + strcpy(pre.guild_card, this->guild_card); + pre.unknown3[0] = this->unknown3[0]; + pre.unknown3[1] = this->unknown3[1]; + pre.name_color = this->name_color; + pre.extra_model = this->extra_model; + memcpy(&pre.unused, &this->unused, 11); + pre.name_color_checksum = this->name_color_checksum; + pre.section_id = this->section_id; + pre.char_class = this->char_class; + pre.v2_flags = this->v2_flags; + pre.version = this->version; + pre.v1_flags = this->v1_flags; + pre.costume = this->costume; + pre.skin = this->skin; + pre.face = this->face; + pre.head = this->head; + pre.hair = this->hair; + pre.hair_r = this->hair_r; + pre.hair_g = this->hair_g; + pre.hair_b = this->hair_b; + pre.proportion_x = this->proportion_x; + pre.proportion_y = this->proportion_y; + char16cpy(pre.name, this->name, 16); + pre.play_time = this->play_time; + return pre; +} + +void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) { + this->level = pre.level; + this->experience = pre.experience; + strcpy(this->guild_card, pre.guild_card); + this->unknown3[0] = pre.unknown3[0]; + this->unknown3[1] = pre.unknown3[1]; + this->name_color = pre.name_color; + this->extra_model = pre.extra_model; + memcpy(&this->unused, &pre.unused, 11); + this->name_color_checksum = pre.name_color_checksum; + this->section_id = pre.section_id; + this->char_class = pre.char_class; + this->v2_flags = pre.v2_flags; + this->version = pre.version; + this->v1_flags = pre.v1_flags; + this->costume = pre.costume; + this->skin = pre.skin; + this->face = pre.face; + this->head = pre.head; + this->hair = pre.hair; + this->hair_r = pre.hair_r; + this->hair_g = pre.hair_g; + this->hair_b = pre.hair_b; + this->proportion_x = pre.proportion_x; + this->proportion_y = pre.proportion_y; + char16cpy(this->name, pre.name, 16); + this->play_time = 0; +} + + + +void PlayerBank::load(const string& filename) { + *this = load_object_file(filename); + for (uint32_t x = 0; x < this->num_items; x++) { + this->items[x].data.item_id = 0x0F010000 + x; + } +} + +void PlayerBank::save(const string& filename) const { + save_file(filename, this, sizeof(*this)); +} + + + +void Player::import(const PSOPlayerDataPC& pc) { + this->inventory = pc.inventory; + this->disp = pc.disp.to_bb(); + /* TODO: fix and re-enable this functionality + decode_sjis(this->info_board, pc->info_board); + memcpy(&this->blocked, pc->blocked, sizeof(uint32_t) * 30); + if (pc->auto_reply_enabled) { + decode_sjis(this->auto_reply, pc->auto_reply); + } else {*/ + this->auto_reply[0] = 0; + //} +} + +void Player::import(const PSOPlayerDataGC& gc) { + this->inventory = gc.inventory; + this->disp = gc.disp.to_bb(); + decode_sjis(this->info_board, gc.info_board, 0xAC); + memcpy(&this->blocked, gc.blocked, sizeof(uint32_t) * 30); + if (gc.auto_reply_enabled) { + decode_sjis(this->auto_reply, gc.auto_reply, 0xAC); + } else { + this->auto_reply[0] = 0; + } +} + +void Player::import(const PSOPlayerDataBB& bb) { + // note: we don't copy the inventory and disp here because we already have + // it (we sent the player data to the client in the first place) + char16cpy(this->info_board, bb.info_board, 0xAC); + memcpy(&this->blocked, bb.blocked, sizeof(uint32_t) * 30); + if (bb.auto_reply_enabled) { + char16cpy(this->auto_reply, bb.auto_reply, 0xAC); + } else { + this->auto_reply[0] = 0; + } +} + +// generates data for 65/67/68 commands (joining games/lobbies) +PlayerLobbyJoinDataPCGC Player::export_lobby_data_pc() const { + PlayerLobbyJoinDataPCGC pc; + pc.inventory = this->inventory; + pc.disp = this->disp.to_pcgc(); + + // PC has fewer classes, so we'll substitute some here + if (pc.disp.char_class == 11) { + pc.disp.char_class = 0; // fomar -> humar + } else if (pc.disp.char_class == 10) { + pc.disp.char_class = 1; // ramarl -> hunewearl + } else if (pc.disp.char_class == 9) { + pc.disp.char_class = 5; // hucaseal -> racaseal + } + + // if the player is still not a valid class, make them appear as the "ninja" NPC + if (pc.disp.char_class > 8) { + pc.disp.extra_model = 0; + pc.disp.v2_flags |= 2; + } + pc.disp.version = 2; + + return pc; +} + +PlayerLobbyJoinDataPCGC Player::export_lobby_data_gc() const { + PlayerLobbyJoinDataPCGC gc; + gc.inventory = this->inventory; + gc.disp = this->disp.to_pcgc(); + return gc; +} + +PlayerLobbyJoinDataBB Player::export_lobby_data_bb() const { + PlayerLobbyJoinDataBB bb; + bb.inventory = this->inventory; + bb.disp = this->disp; + return bb; +} + +PlayerBB Player::export_bb_player_data() const { + PlayerBB bb; + bb.inventory = this->inventory; + bb.disp = this->disp; + memset(bb.unknown, 0, 0x10); + bb.option_flags = this->option_flags; + memcpy(bb.quest_data1, &this->quest_data1, 0x0208); + bb.bank = this->bank; + bb.serial_number = this->serial_number; + char16cpy(bb.name, this->disp.name, 24); + char16cpy(bb.team_name, this->team_name, 16); + char16cpy(bb.guild_card_desc, this->guild_card_desc, 0x58); + bb.reserved1 = 0; + bb.reserved2 = 0; + bb.section_id = this->disp.section_id; + bb.char_class = this->disp.char_class; + bb.unknown3 = 0; + memcpy(bb.symbol_chats, this->symbol_chats, 0x04E0); + memcpy(bb.shortcuts, this->shortcuts, 0x0A40); + char16cpy(bb.auto_reply, this->auto_reply, 0xAC); + char16cpy(bb.info_board, this->info_board, 0xAC); + memset(bb.unknown5, 0, 0x1C); + memcpy(bb.challenge_data, this->challenge_data, 0x0140); + memcpy(bb.tech_menu_config, this->tech_menu_config, 0x0028); + memset(bb.unknown6, 0, 0x2C); + memcpy(bb.quest_data2, &this->quest_data2, 0x0058); + bb.key_config = this->key_config; + return bb; +} + +//////////////////////////////////////////////////////////////////////////////// + +// checksums the guild card file for BB player account data +uint32_t compute_guild_card_checksum(const void* vdata, size_t size) { + const uint8_t* data = reinterpret_cast(vdata); + + uint32_t cs = 0xFFFFFFFF; + for (size_t offset = 0; offset < size; offset++) { + cs ^= data[offset]; + for (size_t y = 0; y < 8; y++) { + if (!(cs & 1)) { + cs = (cs >> 1) & 0x7FFFFFFF; + } else { + cs = ((cs >> 1) & 0x7FFFFFFF) ^ 0xEDB88320; + } + } + } + return (cs ^ 0xFFFFFFFF); +} + +void Player::load_account_data(const string& filename) { + SavedAccountBB account = load_object_file(filename); + + if (strcmp(account.signature, ACCOUNT_FILE_SIGNATURE)) { + throw runtime_error("account data header is incorrect"); + } + + memcpy(&this->blocked, &account.blocked, sizeof(uint32_t) * 30); + this->guild_cards = account.guild_cards; + this->key_config = account.key_config; + this->option_flags = account.option_flags; + memcpy(&this->shortcuts, &account.shortcuts, 0x0A40); + memcpy(&this->symbol_chats, &account.symbol_chats, 0x04E0); + char16cpy(this->team_name, account.team_name, 16); +} + +void Player::save_account_data(const string& filename) const { + SavedAccountBB account; + + strcpy(account.signature, ACCOUNT_FILE_SIGNATURE); + memcpy(&account.blocked, &this->blocked, sizeof(uint32_t) * 30); + account.guild_cards = this->guild_cards; + account.key_config = this->key_config; + account.option_flags = this->option_flags; + memcpy(&account.shortcuts, &this->shortcuts, 0x0A40); + memcpy(&account.symbol_chats, &this->symbol_chats, 0x04E0); + char16cpy(account.team_name, this->team_name, 16); + + save_file(filename, &account, sizeof(account)); +} + +void Player::load_player_data(const string& filename) { + SavedPlayerBB player = load_object_file(filename); + + if (strcmp(player.signature, PLAYER_FILE_SIGNATURE)) { + throw runtime_error("account data header is incorrect"); + } + + char16cpy(this->auto_reply, player.auto_reply, 0xAC); + this->bank = player.bank; + memcpy(&this->challenge_data, &player.challenge_data, 0x0140); + this->disp = player.disp; + char16cpy(this->guild_card_desc,player.guild_card_desc, 0x58); + char16cpy(this->info_board,player.info_board, 0xAC); + this->inventory = player.inventory; + memcpy(&this->quest_data1,&player.quest_data1,0x0208); + memcpy(&this->quest_data2,&player.quest_data2,0x0058); + memcpy(&this->tech_menu_config,&player.tech_menu_config,0x0028); +} + +void Player::save_player_data(const string& filename) const { + SavedPlayerBB player; + + strcpy(player.signature, PLAYER_FILE_SIGNATURE); + player.preview = this->disp.to_preview(); + char16cpy(player.auto_reply, this->auto_reply, 0xAC); + player.bank = this->bank; + memcpy(&player.challenge_data, &this->challenge_data, 0x0140); + player.disp = this->disp; + char16cpy(player.guild_card_desc,this->guild_card_desc, 0x58); + char16cpy(player.info_board,this->info_board, 0xAC); + player.inventory = this->inventory; + memcpy(&player.quest_data1,&this->quest_data1,0x0208); + memcpy(&player.quest_data2,&this->quest_data2,0x0058); + memcpy(&player.tech_menu_config,&this->tech_menu_config,0x0028); + + save_file(filename, &player, sizeof(player)); +} + + + +//////////////////////////////////////////////////////////////////////////////// + +static const unordered_map combine_item_to_max({ + {0x030000, 10}, + {0x030001, 10}, + {0x030002, 10}, + {0x030100, 10}, + {0x030101, 10}, + {0x030102, 10}, + {0x030300, 10}, + {0x030400, 10}, + {0x030500, 10}, + {0x030600, 10}, + {0x030601, 10}, + {0x030700, 10}, + {0x030800, 10}, + {0x031000, 99}, + {0x031001, 99}, + {0x031002, 99}, +}); + +const uint32_t meseta_identifier = 0x00000004; + +uint32_t ItemData::primary_identifier() const { + return (this->item_data1[0] << 16) || (this->item_data1[1] << 8) | this->item_data1[2]; +} + +PlayerBankItem PlayerInventoryItem::to_bank_item() const { + PlayerBankItem bank_item; + bank_item.data = this->data; + + if (combine_item_to_max.count(this->data.primary_identifier())) { + bank_item.amount = this->data.item_data1[5]; + } else { + bank_item.amount = 1; + } + bank_item.show_flags = 1; + + return bank_item; +} + +PlayerInventoryItem PlayerBankItem::to_inventory_item() const { + PlayerInventoryItem item; + item.data = this->data; + if (item.data.item_data1[0] > 2) { + item.equip_flags = 0x0044; + } else { + item.equip_flags = 0x0050; + } + item.equip_flags = 0x0001; // TODO: is this a bug? + item.tech_flag = 0x0001; + return item; +} + +void Player::add_item(const PlayerInventoryItem& item) { + uint32_t pid = item.data.primary_identifier(); + + // is it meseta? then just add to the meseta total + if (pid == meseta_identifier) { + this->disp.meseta += item.data.item_data2d; + if (this->disp.meseta > 999999) { + this->disp.meseta = 999999; + } + return; + } + + // is it a combine item? + try { + uint32_t combine_max = combine_item_to_max.at(pid); + + // is there already a stack of it in the player's inventory? + size_t y; + for (y = 0; y < this->inventory.num_items; y++) { + if (this->inventory.items[y].data.primary_identifier() == item.data.primary_identifier()) { + break; + } + } + + // if there's already a stack, add to the stack and return + if (y < this->inventory.num_items) { + this->inventory.items[y].data.item_data1[5] += item.data.item_data1[5]; + if (this->inventory.items[y].data.item_data1[5] > combine_max) { + this->inventory.items[y].data.item_data1[5] = combine_max; + } + return; + } + } catch (const out_of_range&) { } + + // else, just add the item if there's room + if (this->inventory.num_items >= 30) { + throw runtime_error("inventory is full"); + } + this->inventory.items[this->inventory.num_items] = item; + this->inventory.num_items++; +} + +// adds an item to a bank +void PlayerBank::add_item(const PlayerBankItem& item) { + uint32_t pid = item.data.primary_identifier(); + + // is it meseta? then just add to the meseta total + if (pid == meseta_identifier) { + this->meseta += item.data.item_data2d; + if (this->meseta > 999999) { + this->meseta = 999999; + } + return; + } + + // is it a combine item? + try { + uint32_t combine_max = combine_item_to_max.at(pid); + + // is there already a stack of it in the player's inventory? + size_t y; + for (y = 0; y < this->num_items; y++) { + if (this->items[y].data.primary_identifier() == item.data.primary_identifier()) { + break; + } + } + + // if there's already a stack, add to the stack and return + if (y < this->num_items) { + this->items[y].data.item_data1[5] += item.data.item_data1[5]; + if (this->items[y].data.item_data1[5] > combine_max) { + this->items[y].data.item_data1[5] = combine_max; + } + this->items[y].amount = this->items[y].data.item_data1[5]; + return; + } + } catch (const out_of_range&) { } + + // else, just add the item if there's room + if (this->num_items >= 200) { + throw runtime_error("bank is full"); + } + this->items[this->num_items] = item; + this->num_items++; +} + +void Player::remove_item(uint32_t item_id, uint32_t amount, + PlayerInventoryItem* item) { + + // are we removing meseta? then create a meseta item + if (item_id == 0xFFFFFFFF) { + if (amount > this->disp.meseta) { + throw out_of_range("player does not have enough meseta"); + } + if (item) { + memset(item, 0, sizeof(*item)); + item->data.item_data1[0] = 0x04; + item->data.item_data2d = amount; + } + this->disp.meseta -= amount; + return; + } + + // find this item + size_t index = this->inventory.find_item(item_id); + auto& inventory_item = this->inventory.items[index]; + + // is it a combine item, and are we removing less than we have of it? + // (amount == 0 means remove all of it) + if (amount && (amount < inventory_item.data.item_data1[5]) && + combine_item_to_max.count(inventory_item.data.primary_identifier())) { + if (item) { + *item = inventory_item; + item->data.item_data1[5] = amount; + item->data.item_id = 0xFFFFFFFF; + } + inventory_item.data.item_data1[5] -= amount; + return; + } + + // not a combine item, or we're removing the whole stack? then just remove the item + if (item) { + *item = inventory_item; + } + this->inventory.num_items--; + memcpy(&this->inventory.items[index], &this->inventory.items[index + 1], + sizeof(PlayerInventoryItem) * (this->inventory.num_items - index)); +} + +// removes an item from a bank. works just like RemoveItem for inventories; I won't comment it +void PlayerBank::remove_item(uint32_t item_id, uint32_t amount, + PlayerBankItem* item) { + + // are we removing meseta? then create a meseta item + if (item_id == 0xFFFFFFFF) { + if (amount > this->meseta) { + throw out_of_range("player does not have enough meseta"); + } + if (item) { + memset(item, 0, sizeof(*item)); + item->data.item_data1[0] = 0x04; + item->data.item_data2d = amount; + } + this->meseta -= amount; + return; + } + + // find this item + size_t index = this->find_item(item_id); + auto& bank_item = this->items[index]; + + // is it a combine item, and are we removing less than we have of it? + // (amount == 0 means remove all of it) + if (amount && (amount < bank_item.data.item_data1[5]) && + combine_item_to_max.count(bank_item.data.primary_identifier())) { + if (item) { + *item = bank_item; + item->data.item_data1[5] = amount; + item->amount = amount; + } + bank_item.data.item_data1[5] -= amount; + bank_item.amount -= amount; + return; + } + + // not a combine item, or we're removing the whole stack? then just remove the item + if (item) { + *item = bank_item; + } + this->num_items--; + memcpy(&this->items[index], &this->items[index + 1], + sizeof(PlayerBankItem) * (this->num_items - index)); +} + +size_t PlayerInventory::find_item(uint32_t item_id) { + for (size_t x = 0; x < this->num_items; x++) { + if (this->items[x].data.item_id == item_id) { + return x; + } + } + throw out_of_range("item not present"); +} + +size_t PlayerBank::find_item(uint32_t item_id) { + for (size_t x = 0; x < this->num_items; x++) { + if (this->items[x].data.item_id == item_id) { + return x; + } + } + throw out_of_range("item not present"); +} + +string filename_for_player_bb(const string& username, uint8_t player_index) { + return string_printf("system/players/player_%s_%ld.nsc", username.c_str(), + player_index + 1); +} + +string filename_for_bank_bb(const string& username, const char* bank_name) { + return string_printf("system/players/bank_%s_%s.nsb", username.c_str(), + bank_name); +} + +string filename_for_class_template_bb(uint8_t char_class) { + return string_printf("system/blueburst/player_class_%hhu.nsc", char_class); +} + +string filename_for_account_bb(const string& username) { + return string_printf("system/players/account_%s.nsa", username.c_str()); +} diff --git a/Player.hh b/Player.hh new file mode 100644 index 00000000..d494978a --- /dev/null +++ b/Player.hh @@ -0,0 +1,435 @@ +#pragma once + +#include +#include + +#include + +#include "Version.hh" + + + +// raw item data +struct ItemData { + union { + uint8_t item_data1[12]; + uint16_t item_data1w[6]; + uint32_t item_data1d[3]; + }; + uint32_t item_id; + union { + uint8_t item_data2[4]; + uint16_t item_data2w[2]; + uint32_t item_data2d; + }; + + uint32_t primary_identifier() const; +}; + +struct PlayerBankItem; + +// an item in a player's inventory +struct PlayerInventoryItem { + uint16_t equip_flags; + uint16_t tech_flag; + uint32_t game_flags; + ItemData data; + + PlayerBankItem to_bank_item() const; +}; + +// an item in a player's bank +struct PlayerBankItem { + ItemData data; + uint16_t amount; + uint16_t show_flags; + + PlayerInventoryItem to_inventory_item() const; +}; + +// a player's inventory (remarkably, the format is the same in all versions of PSO) +struct PlayerInventory { + uint8_t num_items; + uint8_t hp_materials_used; + uint8_t tp_materials_used; + uint8_t language; + PlayerInventoryItem items[30]; + + size_t find_item(uint32_t item_id); +}; + +// a player's bank +struct PlayerBank { + uint32_t num_items; + uint32_t meseta; + PlayerBankItem items[200]; + + void load(const std::string& filename); + void save(const std::string& filename) const; + + bool switch_with_file(const char* save_filename, const char* load_filename); + + void add_item(const PlayerBankItem& item); + void remove_item(uint32_t item_id, uint32_t amount, PlayerBankItem* item); + size_t find_item(uint32_t item_id); +}; + + + +// simple player stats +struct PlayerStats { + uint16_t atp; + uint16_t mst; + uint16_t evp; + uint16_t hp; + uint16_t dfp; + uint16_t ata; + uint16_t lck; +}; + +struct PlayerDispDataBB; + +// PC/GC player appearance and stats data +struct PlayerDispDataPCGC { // 0xD0 in size + PlayerStats stats; + uint16_t unknown1; + uint32_t unknown2[2]; + uint32_t level; + uint32_t experience; + uint32_t meseta; + char name[16]; + uint32_t unknown3[2]; + uint32_t name_color; + uint8_t extra_model; + uint8_t unused[15]; + uint32_t name_color_checksum; + uint8_t section_id; + uint8_t char_class; + uint8_t v2_flags; + uint8_t version; + uint32_t v1_flags; + uint16_t costume; + uint16_t skin; + uint16_t face; + uint16_t head; + uint16_t hair; + uint16_t hair_r; + uint16_t hair_g; + uint16_t hair_b; + float proportion_x; + float proportion_y; + uint8_t config[0x48]; + uint8_t technique_levels[0x14]; + + PlayerDispDataBB to_bb() const; +}; + +// BB player preview format +struct PlayerDispDataBBPreview { + uint32_t experience; + uint32_t level; + char guild_card[16]; + uint32_t unknown3[2]; + uint32_t name_color; + uint8_t extra_model; + uint8_t unused[15]; + uint32_t name_color_checksum; + uint8_t section_id; + uint8_t char_class; + uint8_t v2_flags; + uint8_t version; + uint32_t v1_flags; + uint16_t costume; + uint16_t skin; + uint16_t face; + uint16_t head; + uint16_t hair; + uint16_t hair_r; + uint16_t hair_g; + uint16_t hair_b; + float proportion_x; + float proportion_y; + char16_t name[16]; + uint32_t play_time; +}; + +// BB player appearance and stats data +struct PlayerDispDataBB { + PlayerStats stats; + uint16_t unknown1; + uint32_t unknown2[2]; + uint32_t level; + uint32_t experience; + uint32_t meseta; + char guild_card[16]; + uint32_t unknown3[2]; + uint32_t name_color; + uint8_t extra_model; + uint8_t unused[11]; + uint32_t play_time; // not actually a game field; used only by my server + uint32_t name_color_checksum; + uint8_t section_id; + uint8_t char_class; + uint8_t v2_flags; + uint8_t version; + uint32_t v1_flags; + uint16_t costume; + uint16_t skin; + uint16_t face; + uint16_t head; + uint16_t hair; + uint16_t hair_r; + uint16_t hair_g; + uint16_t hair_b; + float proportion_x; + float proportion_y; + char16_t name[0x10]; + uint8_t config[0xE8]; + uint8_t technique_levels[0x14]; + + PlayerDispDataPCGC to_pcgc() const; + PlayerDispDataBBPreview to_preview() const; + void apply_preview(const PlayerDispDataBBPreview&); +}; + + + +struct GuildCardGC { + uint32_t player_tag; + uint32_t serial_number; + char name[0x18]; + char desc[0x6C]; + uint8_t reserved1; // should be 1 + uint8_t reserved2; // should be 1 + uint8_t section_id; + uint8_t char_class; +}; + +// BB guild card format +struct GuildCardBB { + uint32_t serial_number; + char16_t name[0x18]; + char16_t teamname[0x10]; + char16_t desc[0x58]; + uint8_t reserved1; // should be 1 + uint8_t reserved2; // should be 1 + uint8_t section_id; + uint8_t char_class; +}; + +// an entry in the BB guild card file +struct GuildCardEntryBB { + GuildCardBB data; + uint8_t unknown[0xB4]; +}; + +// the format of the BB guild card file +struct GuildCardFileBB { + uint8_t unknown[0x1F84]; + GuildCardEntryBB entry[0x0068]; // that's 104 of them in decimal + uint8_t unknown2[0x01AC]; +}; + +// PSOBB key config and team info +struct KeyAndTeamConfigBB { + uint8_t unknown[0x0114]; // 0000 + uint8_t key_config[0x016C]; // 0114 + uint8_t joystick_config[0x0038]; // 0280 + uint32_t serial_number; // 02B8 + uint32_t team_id; // 02BC + uint32_t team_info[2]; // 02C0 + uint16_t team_privilege_level; // 02C8 + uint16_t reserved; // 02CA + char16_t team_name[0x0010]; // 02CC + uint8_t team_flag[0x0800]; // 02EC + uint32_t team_rewards[2]; // 0AEC +}; + +// BB account data +struct PlayerAccountDataBB { + uint8_t symbol_chats[0x04E0]; + KeyAndTeamConfigBB key_config; + GuildCardFileBB guild_cards; + uint32_t options; + uint8_t shortcuts[0x0A40]; // chat shortcuts (@1FB4 in E7 command) +}; + + + +struct PlayerLobbyDataPC { + uint32_t player_tag; + uint32_t guild_card; + uint32_t ip_address; + uint32_t client_id; + char16_t name[16]; +}; + +struct PlayerLobbyDataGC { + uint32_t player_tag; + uint32_t guild_card; + uint32_t ip_address; + uint32_t client_id; + char name[16]; +}; + +struct PlayerLobbyDataBB { + uint32_t player_tag; + uint32_t guild_card; + uint32_t unknown1[5]; + uint32_t client_id; + char16_t name[16]; + uint32_t unknown2; +}; + + + +struct PSOPlayerDataPC { // for command 0x61 + PlayerInventory inventory; + PlayerDispDataPCGC disp; +}; + +struct PSOPlayerDataGC { // for command 0x61 + PlayerInventory inventory; + PlayerDispDataPCGC disp; + char unknown[0x134]; + char info_board[0xAC]; + uint32_t blocked[0x1E]; + uint32_t auto_reply_enabled; + char auto_reply[0]; +}; + +struct PSOPlayerDataBB { // for command 0x61 + PlayerInventory inventory; + PlayerDispDataBB disp; + char unused[0x174]; + char16_t info_board[0xAC]; + uint32_t blocked[0x1E]; + uint32_t auto_reply_enabled; + char16_t auto_reply[0]; +}; + +// PC/GC lobby player data (used in lobby/game join commands) +struct PlayerLobbyJoinDataPCGC { + PlayerInventory inventory; + PlayerDispDataPCGC disp; +}; + +// BB lobby player data (used in lobby/game join commands) +struct PlayerLobbyJoinDataBB { + PlayerInventory inventory; + PlayerDispDataBB disp; +}; + +// complete BB player data format (used in E7 command) +struct PlayerBB { + PlayerInventory inventory; // 0000 // player + PlayerDispDataBB disp; // 034C // player + uint8_t unknown[0x0010]; // 04DC // + uint32_t option_flags; // 04EC // account + uint8_t quest_data1[0x0208]; // 04F0 // player + PlayerBank bank; // 06F8 // player + uint32_t serial_number; // 19C0 // player + char16_t name[0x18]; // 19C4 // player + char16_t team_name[0x10]; // 19C4 // player + char16_t guild_card_desc[0x58]; // 1A14 // player + uint8_t reserved1; // 1AC4 // player + uint8_t reserved2; // 1AC5 // player + uint8_t section_id; // 1AC6 // player + uint8_t char_class; // 1AC7 // player + uint32_t unknown3; // 1AC8 // + uint8_t symbol_chats[0x04E0]; // 1ACC // account + uint8_t shortcuts[0x0A40]; // 1FAC // account + char16_t auto_reply[0x00AC]; // 29EC // player + char16_t info_board[0x00AC]; // 2B44 // player + uint8_t unknown5[0x001C]; // 2C9C // + uint8_t challenge_data[0x0140]; // 2CB8 // player + uint8_t tech_menu_config[0x0028]; // 2DF8 // player + uint8_t unknown6[0x002C]; // 2E20 // + uint8_t quest_data2[0x0058]; // 2E4C // player + KeyAndTeamConfigBB key_config; // 2EA4 // account +}; // total size: 39A0 + + + +struct SavedPlayerBB { // .nsc file format + char signature[0x40]; + PlayerDispDataBBPreview preview; + + char16_t auto_reply[0x00AC]; + PlayerBank bank; + uint8_t challenge_data[0x0140]; + PlayerDispDataBB disp; + char16_t guild_card_desc[0x58]; + char16_t info_board[0x00AC]; + PlayerInventory inventory; + uint8_t quest_data1[0x0208]; + uint8_t quest_data2[0x0058]; + uint8_t tech_menu_config[0x0028]; +}; + +struct SavedAccountBB { // .nsa file format + char signature[0x40]; + uint32_t blocked[0x001E]; + GuildCardFileBB guild_cards; + KeyAndTeamConfigBB key_config; + uint32_t option_flags; + uint8_t shortcuts[0x0A40]; + uint8_t symbol_chats[0x04E0]; + char16_t team_name[0x0010]; +}; + +// complete player info stored by the server +struct Player { + uint32_t loaded_from_shipgate_time; + + char16_t auto_reply[0x00AC]; // player + PlayerBank bank; // player + char bank_name[0x20]; + uint32_t blocked[0x001E]; // account + uint8_t challenge_data[0x0140]; // player + PlayerDispDataBB disp; // player + uint8_t ep3_config[0x2408]; + char16_t guild_card_desc[0x58]; // player + GuildCardFileBB guild_cards; // account + PlayerInventoryItem identify_result; + char16_t info_board[0x00AC]; // player + PlayerInventory inventory; // player + KeyAndTeamConfigBB key_config; // account + uint32_t option_flags; // account + uint8_t quest_data1[0x0208]; // player + uint8_t quest_data2[0x0058]; // player + uint32_t serial_number; + uint8_t shortcuts[0x0A40]; // account + uint8_t symbol_chats[0x04E0]; // account + char16_t team_name[0x0010]; // account + uint8_t tech_menu_config[0x0028]; // player + + void load_player_data(const std::string& filename); + void save_player_data(const std::string& filename) const; + + void load_account_data(const std::string& filename); + void save_account_data(const std::string& filename) const; + + void import(const PSOPlayerDataPC& pd); + void import(const PSOPlayerDataGC& pd); + void import(const PSOPlayerDataBB& pd); + PlayerLobbyJoinDataPCGC export_lobby_data_pc() const; + PlayerLobbyJoinDataPCGC export_lobby_data_gc() const; + PlayerLobbyJoinDataBB export_lobby_data_bb() const; + PlayerBB export_bb_player_data() const; + + void add_item(const PlayerInventoryItem& item); + void remove_item(uint32_t item_id, uint32_t amount, PlayerInventoryItem* item); + size_t find_item(uint32_t item_id); +}; + + + +#define ERROR_COMBINE_ITEM_SPLIT 0x5FFFFFFE // this error code is used to tell calling functions that a combine item needs to be split + +uint32_t compute_guild_card_checksum(const void* data, size_t size); + +std::string filename_for_player_bb(const std::string& username, uint8_t player_index); +std::string filename_for_bank_bb(const std::string& username, const char* bank_name); +std::string filename_for_class_template_bb(uint8_t char_class); +std::string filename_for_account_bb(const std::string& username); diff --git a/Quest.cc b/Quest.cc new file mode 100644 index 00000000..474e7aa8 --- /dev/null +++ b/Quest.cc @@ -0,0 +1,364 @@ +#include "Quest.hh" + +#include +#include +#include +#include + +#include "Compression.hh" +#include "Text.hh" + +using namespace std; + + + +bool category_is_mode(QuestCategory category) { + return (category == QuestCategory::Battle) || + (category == QuestCategory::Challenge) || + (category == QuestCategory::Episode3); +} + +const char* name_for_category(QuestCategory category) { + switch (category) { + case QuestCategory::Retrieval: + return "Retrieval"; + case QuestCategory::Extermination: + return "Extermination"; + case QuestCategory::Event: + return "Event"; + case QuestCategory::Shop: + return "Shop"; + case QuestCategory::VR: + return "VR"; + case QuestCategory::Tower: + return "Tower"; + case QuestCategory::Government: + return "Government"; + case QuestCategory::Download: + return "Download"; + case QuestCategory::Battle: + return "Battle"; + case QuestCategory::Challenge: + return "Challenge"; + case QuestCategory::Solo: + return "Solo"; + case QuestCategory::Episode3: + return "Episode3"; + default: + return "Unknown"; + } +} + + + +struct PSOQuestHeaderDC { // same for dc v1 and v2, thankfully + uint32_t start_offset; + uint32_t unknown_offset1; + uint32_t size; + uint32_t unused; + uint8_t unknown1; + uint8_t unknown2; + uint16_t quest_number; // 0xFFFF for challenge quests + char name[0x20]; + char short_description[0x80]; + char long_description[0x120]; +}; + +struct PSOQuestHeaderPC { + uint32_t start_offset; + uint32_t unknown_offset1; + uint32_t size; + uint32_t unused; + uint8_t unknown1; + uint8_t unknown2; + uint16_t quest_number; // 0xFFFF for challenge quests + char16_t name[0x20]; + char16_t short_description[0x80]; + char16_t long_description[0x120]; +}; + +struct PSOQuestHeaderGC { + uint32_t start_offset; + uint32_t unknown_offset1; + uint32_t size; + uint32_t unused; + uint16_t unknown1; + uint8_t quest_number; + uint8_t episode; // 1 = ep2. apparently some quests have 0xFF here, which means ep1 (?) + char name[0x20]; + char short_description[0x80]; + char long_description[0x120]; +}; + +struct PSOQuestHeaderGCEpisode3 { + // there's actually a lot of other important stuff in here but I'm lazy. it + // looks like map data, cutscene data, and maybe special cards used during + // the quest + uint8_t unused[0x1DF0]; + char name[0x14]; + char location[0x14]; + char location2[0x3C]; + char description[0x190]; + uint8_t unused2[0x3A34]; +}; + +struct PSOQuestHeaderBB { + uint32_t start_offset; + uint32_t unknown_offset1; + uint32_t size; + uint32_t unused; + uint16_t quest_number; // 0xFFFF for challenge quests + uint16_t unused2; + uint8_t episode; // 0 = ep1, 1 = ep2, 2 = ep4 + uint8_t max_players; + uint8_t joinable_in_progress; + uint8_t unknown; + char16_t name[0x20]; + char16_t short_description[0x80]; + char16_t long_description[0x120]; +}; + + + +Quest::Quest(const string& bin_filename) : quest_id(-1), + category(QuestCategory::Unknown), episode(0), is_dcv1(false), + joinable(false), + file_basename(bin_filename.substr(0, bin_filename.size() - 4)) { + + string bin_basename; + { + size_t slash_pos = bin_filename.rfind('/'); + if (slash_pos != string::npos) { + bin_basename = bin_filename.substr(slash_pos + 1); + } else { + bin_basename = bin_filename; + } + } + + // quest filenames are like: + // b###-VV.bin for battle mode + // c###-VV.bin for challenge mode + // e###-gc3.bin for episode 3 + // q###-CAT-VV.bin for normal quests + + if (bin_basename.empty()) { + throw invalid_argument("empty filename"); + } + + if (bin_basename[0] == 'b') { + this->category = QuestCategory::Battle; + } else if (bin_basename[0] == 'c') { + this->category = QuestCategory::Challenge; + } else if (bin_basename[0] == 'e') { + this->category = QuestCategory::Episode3; + } else if (bin_basename[0] != 'q') { + throw invalid_argument("filename does not indicate mode"); + } + + // if the quest category is still unknown, expect 3 tokens (one of them will + // tell us the category) + vector tokens = split(bin_basename, '-'); + if (tokens.size() != (2 + (this->category == QuestCategory::Unknown))) { + throw invalid_argument("incorrect filename format"); + } + + // parse the number out of the first token + this->quest_id = strtoull(tokens[0].c_str() + 1, NULL, 10); + + // get the category from the second token if needed + if (this->category == QuestCategory::Unknown) { + static const unordered_map name_to_category({ + {"ret", QuestCategory::Retrieval}, + {"ext", QuestCategory::Extermination}, + {"evt", QuestCategory::Event}, + {"shp", QuestCategory::Shop}, + {"vr", QuestCategory::VR}, + {"twr", QuestCategory::Tower}, + {"gov", QuestCategory::Government}, + {"dl", QuestCategory::Download}, + {"1p", QuestCategory::Solo}, + }); + this->category = name_to_category.at(tokens[1]); + tokens.erase(tokens.begin() + 1); + } + + static const unordered_map name_to_version({ + {"dc1.bin", GameVersion::DC}, + {"dc.bin", GameVersion::DC}, + {"pc.bin", GameVersion::PC}, + {"gc.bin", GameVersion::GC}, + {"gc3.bin", GameVersion::GC}, + {"bb.bin", GameVersion::BB}, + }); + this->version = name_to_version.at(tokens[1]); + + // the rest of the information needs to be fetched from the .bin file's + // contents + + auto bin_compressed = this->bin_contents(); + auto bin_decompressed = prs_decompress(*bin_compressed); + + switch (this->version) { + case GameVersion::Patch: + throw invalid_argument("patch server quests are not valid"); + break; + + case GameVersion::DC: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + this->joinable = false; + this->episode = 0; + this->name = decode_sjis(header->name); + this->short_description = decode_sjis(header->short_description); + this->long_description = decode_sjis(header->long_description); + this->is_dcv1 = (tokens[1] == "dc1.bin"); + break; + } + + case GameVersion::PC: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + this->joinable = false; + this->episode = 0; + this->name = header->name; + this->short_description = header->short_description; + this->long_description = header->long_description; + break; + } + + case GameVersion::GC: { + if (this->category == QuestCategory::Episode3) { + // these all appear to be the same size + if (bin_decompressed.size() != sizeof(PSOQuestHeaderGCEpisode3)) { + throw invalid_argument("file is incorrect size"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + this->joinable = false; + this->episode = 0xFF; + this->name = decode_sjis(header->name); + this->short_description = decode_sjis(header->location2); + this->long_description = decode_sjis(header->description); + } else { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + this->joinable = false; + this->episode = (header->episode == 1); + this->name = decode_sjis(header->name); + this->short_description = decode_sjis(header->short_description); + this->long_description = decode_sjis(header->long_description); + } + break; + } + + case GameVersion::BB: { + if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) { + throw invalid_argument("file is too small for header"); + } + auto* header = reinterpret_cast(bin_decompressed.data()); + this->joinable = header->joinable_in_progress; + this->episode = header->episode; + this->name = header->name; + this->short_description = header->short_description; + this->long_description = header->long_description; + break; + + } + } +} + +static string basename_for_filename(const std::string& filename) { + size_t slash_pos = filename.rfind('/'); + if (slash_pos != string::npos) { + return filename.substr(slash_pos + 1); + } + return filename; +} + +std::string Quest::bin_filename() const { + return basename_for_filename(this->file_basename + ".bin"); +} + +std::string Quest::dat_filename() const { + return basename_for_filename(this->file_basename + ".dat"); +} + +shared_ptr Quest::bin_contents() const { + if (!this->bin_contents_ptr) { + this->bin_contents_ptr.reset(new string(load_file(this->file_basename + ".bin"))); + } + return this->bin_contents_ptr; +} + +shared_ptr Quest::dat_contents() const { + if (!this->bin_contents_ptr) { + this->bin_contents_ptr.reset(new string(load_file(this->file_basename + ".dat"))); + } + return this->bin_contents_ptr; +} + + + +QuestIndex::QuestIndex(const char* directory) : directory(directory) { + for (const auto& filename : list_directory(this->directory)) { + string full_path = this->directory + "/" + filename; + + if (ends_with(filename, ".gba")) { + this->gba_file_contents.emplace(filename, new string(load_file(full_path))); + continue; + } + + if (ends_with(filename, ".bin")) { + try { + shared_ptr q(new Quest(full_path)); + this->version_id_to_quest.emplace(make_pair(q->version, q->quest_id), q); + this->version_name_to_quest.emplace(make_pair(q->version, q->name), q); + string ascii_name = encode_sjis(q->name); + log(INFO, "indexed quest %s (%s-%" PRId64 ", %s, episode=%hhu, joinable=%s, dcv1=%s)", + ascii_name.c_str(), name_for_version(q->version), q->quest_id, + name_for_category(q->category), q->episode, + q->joinable ? "true" : "false", q->is_dcv1 ? "true" : "false"); + } catch (const exception& e) { + log(WARNING, "failed to parse quest file %s (%s)", filename.c_str(), e.what()); + } + } + } +} + +shared_ptr QuestIndex::get(GameVersion version, + uint32_t id) const { + return this->version_id_to_quest.at(make_pair(version, id)); +} + +shared_ptr QuestIndex::get_gba(const string& name) const { + return this->gba_file_contents.at(name); +} + +vector> QuestIndex::filter(GameVersion version, + bool is_dcv1, QuestCategory category, uint8_t episode) const { + auto it = this->version_id_to_quest.lower_bound(make_pair(version, 0)); + auto end_it = this->version_id_to_quest.upper_bound(make_pair(version, 0xFFFFFFFF)); + + vector> ret; + for (; it != end_it; it++) { + shared_ptr q = it->second; + if ((q->is_dcv1 != is_dcv1) || (q->category != category)) { + continue; + } + + // only check episode and solo if the category isn't a mode (that is, ignore + // episode if querying for battle/challange/solo quests) + if (!category_is_mode(category) && ((q->episode != episode))) { + continue; + } + + ret.emplace_back(q); + } + + return ret; +} diff --git a/Quest.hh b/Quest.hh new file mode 100644 index 00000000..04934eba --- /dev/null +++ b/Quest.hh @@ -0,0 +1,76 @@ +#pragma once + +#include + +#include +#include + +#include "Version.hh" + + + +enum class QuestCategory { + Unknown = -1, + Retrieval = 0, + Extermination, + Event, + Shop, + VR, + Tower, + Government, + Download, + Battle, + Challenge, + Solo, + Episode3, +}; + +bool category_is_mode(QuestCategory category); +const char* name_for_category(QuestCategory category); + + + +struct Quest { + int64_t quest_id; + QuestCategory category; + uint8_t episode; // 0 = ep1, 1 = ep2, 2 = ep4, 0xFF = ep3 + bool is_dcv1; + bool joinable; + GameVersion version; + std::string file_basename; // we append -. when reading + std::u16string name; + std::u16string short_description; + std::u16string long_description; + + // these are populated when requested + mutable std::shared_ptr bin_contents_ptr; + mutable std::shared_ptr dat_contents_ptr; + + Quest(const std::string& file_basename); + + std::string bin_filename() const; + std::string dat_filename() const; + + std::shared_ptr bin_contents() const; + std::shared_ptr dat_contents() const; +}; + +struct QuestIndex { + std::string directory; + + std::map, std::shared_ptr> version_id_to_quest; + std::map, std::shared_ptr> version_name_to_quest; + + std::map>> category_to_quests; + + std::map> gba_file_contents; + + QuestIndex(const char* directory); + + std::shared_ptr get(GameVersion version, uint32_t id) const; + std::shared_ptr get_gba(const std::string& name) const; + std::vector> filter(GameVersion version, + bool is_dcv1, QuestCategory category, uint8_t episode) const; +}; + +Quest create_download_quest(const Quest& src, size_t version); diff --git a/README.md b/README.md new file mode 100644 index 00000000..e35f98ac --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# newserv + +newserv is a game server for Phantasy Star Online (PSO). + +This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself; this data was originally created by Sega. + +This project is a rewrite of a rewrite of a game server that I wrote many years ago. In its current state, do not expect this server to work - it builds on Mac OS, but I haven't found the time to set up a test environment where I can make the game connect to newserv. + +## History + +In ages long past (probably 2005? I honestly can't remember), I began writing a PSO server. This project became known as khyller, evolving into a full-featured environment supporting all versions of the game that I had access to - PC, GC, and BB. But as this evolution occurred, the code became increasingly ugly and hard to work with, littered with debugging filth that I never cleaned up and odd coding patterns that I had picked up over the years. + +Sometime in 2006 or 2007, I abandoned khyller and rebuilt the entire thing from scratch, resulting in newserv. But this newserv was not the project you're looking at now; 2007's newserv was substantially cleaner in code than khyller but was still quite ugly, and it lacked a few of the more esoteric features I had originally written (for example, the ability to convert any quest into a download quest). I felt better about working with this code, but it still had some stability problems. It turns out that 2007's newserv's concurrency implementation was simply incorrect - I had derived the concept of a mutex myself (before taking any real computer engineering classes) but implemented it incorrectly. No wonder newserv would randomly crash after running seemingly fine for a few days. + +Last weekend (October 2018), I had some random cause to reminisce. I looked back in my old code archives and came across newserv. Somehow inspired, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and the concurrency primitives it uses are correct (thought I haven't audited where exactly they're used; there are likely some missing lock contexts still). + +## Future + +Really, this project is mostly for my own nostalgia. Feel free to peruse if you'd like. If I'm suitably inspired again, I may boot up my copies of PSO and play around with it, and I'm sure I'll find numerous bugs the first time I do so. But I offer no guarantees on when this will happen, or if it will happen at all. diff --git a/RareItemSet.cc b/RareItemSet.cc new file mode 100644 index 00000000..00337390 --- /dev/null +++ b/RareItemSet.cc @@ -0,0 +1,25 @@ +#include "RareItemSet.hh" + +#include + +using namespace std; + + + +RareItemSet::RareItemSet(const char* filename, uint8_t episode, + uint8_t difficulty, uint8_t secid) { + scoped_fd fd(filename, O_RDONLY); + size_t offset = (episode * 0x6400) + (difficulty * 0x1900) + (secid * 0x0280); + preadx(fd, this, sizeof(*this), offset); +} + +bool sample_rare_item(uint8_t pc) { + int8_t shift = ((pc >> 3) & 0x1F) - 4; + if (shift < 0) { + shift = 0; + } + uint32_t rate = ((2 << shift) * ((pc & 7) + 7)); + + uint32_t x = ((rand() << 30) | (rand() << 15) | rand()); + return (x < rate); +} diff --git a/RareItemSet.hh b/RareItemSet.hh new file mode 100644 index 00000000..4057ddce --- /dev/null +++ b/RareItemSet.hh @@ -0,0 +1,22 @@ +#pragma once + +#include + + + +struct RareItemDrop { + uint8_t probability; + uint8_t item_code[3]; +}; + +struct RareItemSet { + RareItemDrop rares[0x65]; // 0000 - 0194 in file + uint8_t box_areas[0x1E]; // 0194 - 01B2 in file + RareItemDrop box_rares[0x1E]; // 01B2 - 022A in file + uint8_t unused[0x56]; + + RareItemSet(const char* filename, uint8_t episode, uint8_t difficulty, + uint8_t secid); +}; // 0x280 in size; describes one difficulty, section ID, and episode + +bool sample_rare_item(uint8_t pc); diff --git a/ReceiveCommands.cc b/ReceiveCommands.cc new file mode 100644 index 00000000..f4b42e5e --- /dev/null +++ b/ReceiveCommands.cc @@ -0,0 +1,2049 @@ +#include "SendCommands.hh" + +#include + +#include +#include +#include +#include + +#include "PSOProtocol.hh" +#include "FileContentsCache.hh" +#include "Text.hh" +#include "SendCommands.hh" +#include "ReceiveSubcommands.hh" +#include "ChatCommands.hh" + +using namespace std; + +#define CONFIG_MAGIC 0x48615467 + + + +enum ClientStateBB { + // initial connection. server will redirect client to another port. + InitialLogin = 0x00, + // second connection. server will send client game data and account data. + DownloadData = 0x01, + // third connection. choose character menu + ChoosePlayer = 0x02, + // fourth connection, used for saving characters only. if you do not create a + // character, server sets this state in order to skip it. + SavePlayer = 0x03, + // last connection. redirects client to login server. + ShipSelect = 0x04, +}; + + + +//////////////////////////////////////////////////////////////////////////////// + +void process_connect(std::shared_ptr s, std::shared_ptr c) { + switch (c->server_behavior) { + case ServerBehavior::SplitReconnect: { + uint16_t pc_port = s->port_configuration.at("pc-login").port; + send_pc_gc_split_reconnect(c, 0, pc_port); + c->server_behavior = ServerBehavior::LoginServer; + // intentional fallthrough + } + + case ServerBehavior::LoginServer: + case ServerBehavior::LobbyServer: + send_server_init(c, true); + break; + + case ServerBehavior::DataServerBB: + case ServerBehavior::PatchServer: + send_server_init(c, false); + break; + } +} + +void process_login_complete(shared_ptr s, shared_ptr c) { + if (c->server_behavior == ServerBehavior::LoginServer) { + // on the login server, send the ep3 updates and the main menu + if (c->flags & ClientFlag::Episode3Games) { + send_ep3_card_list_update(c); + send_ep3_rank_update(c); + } + + send_menu(c, u"Main menu", MAIN_MENU_ID, s->main_menu, false); + + } else if (c->server_behavior == ServerBehavior::LobbyServer) { + + // if the client is BB, load thair player and account data + if (c->version == GameVersion::BB) { + string account_filename = filename_for_account_bb(c->license->username); + try { + c->player.load_account_data(account_filename); + } catch (const exception& e) { + c->player.load_account_data("system/blueburst/default.nsa"); + } + + sprintf(c->player.bank_name, "player%d", c->config.cfg.bb_player_index + 1); + + string player_filename = filename_for_player_bb(c->license->username, + c->config.cfg.bb_player_index); + try { + c->player.load_player_data(player_filename); + } catch (const exception&) { + send_message_box(c, u"$C6Your player data cannot be found."); + c->should_disconnect = true; + return; + } + + string bank_filename = filename_for_bank_bb(c->license->username, + c->player.bank_name); + try { + c->player.bank.load(bank_filename); + } catch (const exception&) { + send_message_box(c, u"$C6Your bank data cannot be found."); + c->should_disconnect = true; + return; + } + + send_complete_player_bb(c); + } + + send_lobby_list(c, s); + send_get_player_info(c); + } +} + + + +void process_disconnect(shared_ptr s, shared_ptr c) { + // if the client was in a lobby, remove them and notify the other clients + if (c->lobby_id) { + s->remove_client_from_lobby(c); + } + + if (c->version == GameVersion::BB) { + c->player.disp.play_time += ((now() - c->play_time_begin) / 1000000); + string account_filename = filename_for_account_bb(c->license->username); + string player_filename = filename_for_player_bb(c->license->username, + c->config.cfg.bb_player_index); + string bank_filename = filename_for_bank_bb(c->license->username, + c->player.bank_name); + c->player.save_account_data(account_filename); + c->player.save_player_data(player_filename); + c->player.bank.save(bank_filename); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// + +void process_verify_license_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // DB + struct Cmd { + char unused[0x20]; + char serial_number[0x10]; + char access_key[0x10]; + char unused2[0x08]; + uint32_t sub_version; + char unused3[0x60]; + char password[0x10]; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + try { + uint32_t serial_number; + sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, + cmd->password); + } catch (const exception& e) { + u16string message = u"Login failed: " + decode_sjis(e.what()); + send_message_box(c, message.c_str()); + c->should_disconnect = true; + return; + } + + c->flags |= flags_for_version(c->version, cmd->sub_version); + send_command(c, 0x9A, 0x01); +} + +void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 9A + struct Cmd { + char unused[0x20]; + char serial_number[0x10]; + char access_key[0x10]; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + try { + uint32_t serial_number; + sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + if (c->version == GameVersion::GC) { + c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, + NULL); + } else { + c->license = s->license_manager->verify_pc(serial_number, cmd->access_key, + NULL); + } + } catch (const exception& e) { + u16string message = u"Login failed: " + decode_sjis(e.what()); + send_message_box(c, message.c_str()); + c->should_disconnect = true; + return; + } + + send_command(c, 0x9C, 0x01); +} + +void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 9C + struct Cmd { + char unused[8]; + uint32_t sub_version; + uint32_t unused2; + char serial_number[0x30]; + char access_key[0x30]; + char password[0x30]; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + c->flags |= flags_for_version(c->version, cmd->sub_version); + + try { + uint32_t serial_number; + sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + if (c->version == GameVersion::GC) { + c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, + cmd->password); + } else { + c->license = s->license_manager->verify_pc(serial_number, cmd->access_key, + cmd->password); + } + } catch (const exception& e) { + u16string message = u"Login failed: " + decode_sjis(e.what()); + send_message_box(c, message.c_str()); + c->should_disconnect = true; + return; + } + + send_command(c, 0x9C, 0x01); +} + +void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 9D 9E + struct Cmd { + char unused[0x10]; + uint8_t sub_version; + uint8_t unused2[0x27]; + char serial_number[0x10]; + char access_key[0x10]; + char unused3[0x60]; + char name[0x10]; + ClientConfig cfg; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + c->flags |= flags_for_version(c->version, cmd->sub_version); + + try { + uint32_t serial_number; + sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + if (c->version == GameVersion::GC) { + c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, + NULL); + } else { + c->license = s->license_manager->verify_pc(serial_number, cmd->access_key, + NULL); + } + } catch (const exception& e) { + u16string message = u"Login failed: " + decode_sjis(e.what()); + send_message_box(c, message.c_str()); + c->should_disconnect = true; + return; + } + + memcpy(&c->config.cfg, &cmd->cfg, sizeof(ClientConfig)); + + if (c->config.cfg.magic != CONFIG_MAGIC) { + memset(&c->config, 0, sizeof(ClientConfigBB)); + c->config.cfg.magic = CONFIG_MAGIC; + } + + send_update_client_config(c); + + process_login_complete(s, c); +} + +void process_login_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 93 + struct Cmd { + char unused[0x14]; + char username[0x10]; + char unused2[0x20]; + char password[0x10]; + char unused3[0x30]; + ClientConfigBB cfg; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + c->flags |= flags_for_version(c->version, 0); + + try { + c->license = s->license_manager->verify_bb(cmd->username, cmd->password); + } catch (const exception& e) { + u16string message = u"Login failed: " + decode_sjis(e.what()); + send_message_box(c, message.c_str()); + c->should_disconnect = true; + return; + } + + memcpy(&c->config, &cmd->cfg, sizeof(ClientConfigBB)); + + if (c->config.cfg.magic != CONFIG_MAGIC) { + memset(&c->config, 0, sizeof(ClientConfigBB)); + c->config.cfg.magic = CONFIG_MAGIC; + } else { + c->config.cfg.bb_game_state++; + } + + send_client_init_bb(c, 0); + + switch (c->config.cfg.bb_game_state) { + case ClientStateBB::InitialLogin: + // first login? send them to the other port + send_reconnect(c, 0, s->port_configuration.at("bb-data1").port); + break; + + case ClientStateBB::DownloadData: { + // download data? send them their account data and player previews + string account_filename = filename_for_account_bb(c->license->username); + try { + c->player.load_account_data(account_filename); + } catch (const exception& e) { + c->player.load_account_data("system/blueburst/default.nsa"); + } + break; + } + + case ClientStateBB::ChoosePlayer: + case ClientStateBB::SavePlayer: + // just wait; the command handlers will handle it + break; + + case ClientStateBB::ShipSelect: + // this happens on the login server and later + process_login_complete(s, c); + break; + + default: + send_reconnect(c, 0, s->port_configuration.at("bb-login").port); + } +} + +void process_client_checksum(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 96 + check_size(size, 0); + send_command(c, 0x97, 0x01); +} + +void process_server_time_request(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // B1 + check_size(size, 0); + send_server_time(c); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// Ep3 commands. Note that these commands are not at all functional. The command +// handlers that partially worked were lost in a dead hard drive, unfortunately. + +void process_ep3_jukebox(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + struct Cmd { + uint32_t unknown[3]; // should be FFFFFFFF 00000000 + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + auto l = s->find_lobby(c->lobby_id); + if (!l || !(l->flags & LobbyFlag::Episode3)) { + return; + } + + send_command(l, command, 0x03, cmd); +} + +void process_ep3_menu_challenge(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // DC + check_size(size, 0); + send_command(c, 0xDC); +} + +void process_ep3_server_data_request(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // CA + check_size(size, 8, 0xFFFF); + const PSOSubcommand* cmds = reinterpret_cast(data); + + auto l = s->find_lobby(c->lobby_id); + if (!l || !(l->flags & LobbyFlag::Episode3) || !l->is_game()) { + c->should_disconnect = true; + return; + } + + if (cmds[0].byte[0] != 0xB3) { + c->should_disconnect = true; + return; + } + + switch (cmds[1].byte[0]) { + // phase 1: map select + case 0x40: + send_ep3_map_list(l); + break; + case 0x41: + send_ep3_map_data(l, cmds[4].dword); + break; + /*// phase 2: deck/name entry + case 0x13: + ti = FindTeam(s, c->teamID); + memcpy(&ti->ep3game, ((DWORD)c->bufferin + 0x14), 0x2AC); + CommandEp3InitChangeState(s, c, 1); + break; + case 0x1B: + ti = FindTeam(s, c->teamID); + memcpy(&ti->ep3names[*(BYTE*)((DWORD)c->bufferin + 0x24)], ((DWORD)c->bufferin + 0x14), 0x14); // NOTICE: may be 0x26 instead of 0x24 + CommandEp3InitSendNames(s, c); + break; + case 0x14: + ti = FindTeam(s, c->teamID); + memcpy(&ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], ((DWORD)c->bufferin + 0x18), 0x58); // NOTICE: may be 0x16 instead of 0x14 + Ep3FillHand(&ti->ep3game, &ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]); + //Ep3RollDice(&ti->ep3game, &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]); + CommandEp3InitSendDecks(s, c); + CommandEp3InitSendMapLayout(s, c); + for (x = 0, param = 0; x < 4; x++) if ((ti->ep3decks[x].clientID != 0xFFFFFFFF) && (ti->ep3names[x].clientID != 0xFF)) param++; + if (param >= ti->ep3game.numPlayers) CommandEp3InitChangeState(s, c, 3); + break; + // phase 3: hands & game states + case 0x1D: + ti = FindTeam(s, c->teamID); + Ep3ReprocessMap(&ti->ep3game); + CommandEp3SendMapData(s, c, ti->ep3game.mapID); + for (y = 0, x = 0; x < 4; x++) + { + if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; + Ep3EquipCard(&ti->ep3game, &ti->ep3decks[x], &ti->ep3pcs[x], 0); // equip SC card + CommandEp3InitHandUpdate(s, c, x); + CommandEp3InitStatUpdate(s, c, x); + y++; + } + CommandEp3Init_B4_06(s, c, (y == 4) ? true : false); + CommandEp3InitSendMapLayout(s, c); + for (x = 0; x < 4; x++) + { + if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; + CommandEp3Init_B4_4E(s, c, x); + CommandEp3Init_B4_4C(s, c, x); + CommandEp3Init_B4_4D(s, c, x); + CommandEp3Init_B4_4F(s, c, x); + } + CommandEp3InitSendDecks(s, c); + CommandEp3InitSendMapLayout(s, c); + for (x = 0; x < 4; x++) + { + if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; + CommandEp3InitHandUpdate(s, c, x); + } + CommandEp3InitSendNames(s, c); + CommandEp3InitChangeState(s, c, 4); + CommandEp3Init_B4_50(s, c); + CommandEp3InitSendMapLayout(s, c); + CommandEp3Init_B4_39(s, c); // MISSING: 60 00 AC 00 B4 2A 00 00 39 56 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + CommandEp3InitBegin(s, c); + break; */ + default: + log(WARNING, "unknown Ep3 server data request: %02X", cmds[1].byte[0]); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// menu commands + +void process_message_box_closed(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // D6 + if (c->in_information_menu) { + // add a reference to ensure it's not destroyed by another thread + auto info_menu = s->information_menu; + send_menu(c, u"Information", INFORMATION_MENU_ID, *info_menu, true); + return; + } +} + +void process_menu_selection(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 09 10 + bool uses_unicode = ((c->version == GameVersion::PC) || (c->version == GameVersion::BB)); + + struct Cmd { + uint32_t menu_id; + uint32_t item_id; + union { + char16_t password_pc_bb[0]; + char password_dc_gc[0]; + }; + }; + check_size(size, sizeof(Cmd), sizeof(Cmd) + 0x10 * (1 + uses_unicode)); + const auto* cmd = reinterpret_cast(data); + + switch (cmd->menu_id) { + case MAIN_MENU_ID: { + switch (cmd->item_id) { + case MAIN_MENU_GO_TO_LOBBY: { + static const vector version_to_port_name({ + "dc-lobby", "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "bb-lobby"}); + const auto& port_name = version_to_port_name.at(static_cast(c->version)); + + send_reconnect(c, 0, s->port_configuration.at(port_name).port); + break; + } + + case MAIN_MENU_INFORMATION: { + auto info_menu = s->information_menu; + send_menu(c, u"Information", INFORMATION_MENU_ID, *info_menu, true); + c->in_information_menu = true; + break; + } + + case MAIN_MENU_DISCONNECT: + c->should_disconnect = true; + break; + + default: + send_message_box(c, u"Incorrect menu item ID."); + break; + } + } + + case INFORMATION_MENU_ID: { + if (cmd->item_id == 0) { + c->in_information_menu = false; + send_menu(c, u"Main menu", MAIN_MENU_ID, s->main_menu, false); + + } else { + try { + // add a reference to ensure it's not destroyed by another thread + auto info_menu = s->id_to_information_contents; + send_message_box(c, info_menu->at(cmd->item_id).c_str()); + } catch (const out_of_range&) { + send_message_box(c, u"$C6No such information exists."); + } + } + break; + } + + case GAME_MENU_ID: { + auto game = s->find_lobby(cmd->item_id); + if (!game) { + send_message_box(c, u"$C6This game is no\nlonger active."); + break; + } + + if (!game->is_game()) { + send_message_box(c, u"$C6This game is not\na game."); + break; + } + + if (game->count_clients() >= game->max_clients) { + send_message_box(c, u"$C6This game is full."); + break; + } + + if ((game->version != c->version) || + (!(game->flags & LobbyFlag::Episode3) != !(c->flags & LobbyFlag::Episode3))) { + send_message_box(c, u"$C6This game is for\n a different version\nof PSO."); + break; + } + + if ((game->version != c->version) || + (!(game->flags & LobbyFlag::Episode3) != !(c->flags & LobbyFlag::Episode3))) { + send_message_box(c, u"$C6A quest is in progress\nin this game."); + break; + } + + if (game->any_client_loading()) { + send_message_box(c, u"$C6A player is currently\nloading."); + break; + } + + if (game->any_client_loading()) { + send_message_box(c, u"$C6You cannot join a\nSolo Mode game."); + break; + } + + if (!(c->license->privileges & Privilege::FreeJoinGames)) { + char16_t password[0x10]; + if (size > sizeof(Cmd)) { + if (uses_unicode) { + char16cpy(password, cmd->password_pc_bb, 0x10); + } else { + decode_sjis(password, cmd->password_dc_gc, 0x10); + } + } + + if (game->password[0] && char16cmp(game->password, password, 0x10)) { + send_message_box(c, u"$C6Incorrect password."); + break; + } + if (c->player.disp.level < game->min_level) { + send_message_box(c, u"$C6Your level is too\nlow to join this\ngame."); + break; + } + if (c->player.disp.level < game->max_level) { + send_message_box(c, u"$C6Your level is too\nhigh to join this\ngame."); + break; + } + } + + s->change_client_lobby(c, game); + c->flags |= ClientFlag::Loading; + if (c->version == GameVersion::BB) { + game->assign_item_ids_for_player(c->lobby_client_id, c->player.inventory); + } + break; + } + + case QUEST_FILTER_MENU_ID: { + if (!s->quest_index) { + send_lobby_message_box(c, u"$C6Quests are not available."); + break; + } + auto l = s->find_lobby(c->lobby_id); + auto quests = s->quest_index->filter(c->version, + c->flags & ClientFlag::IsDCv1, + static_cast(cmd->item_id), l->episode); + if (quests.empty()) { + send_lobby_message_box(c, u"$C6There are no quests\navailable in that\ncategory."); + break; + } + + send_quest_menu(c, QUEST_MENU_ID, quests, false); + break; + } + + case QUEST_MENU_ID: { + if (!s->quest_index) { + send_lobby_message_box(c, u"$C6Quests are not available."); + break; + } + auto q = s->quest_index->get(c->version, cmd->item_id); + if (!q) { + send_lobby_message_box(c, u"$C6Quest does not exist."); + break; + } + + auto l = s->find_lobby(c->lobby_id); + if (!l->is_game()) { + send_lobby_message_box(c, u"$C6Quests cannot be loaded\nin lobbies."); + break; + } + + auto bin_basename = q->bin_filename(); + auto dat_basename = q->dat_filename(); + auto bin_contents = q->bin_contents(); + auto dat_contents = q->dat_contents(); + + rw_guard g(l->lock, true); + if (q->joinable) { + l->flags |= LobbyFlag::JoinableQuestInProgress; + } else { + l->flags |= LobbyFlag::QuestInProgress; + } + l->loading_quest_id = q->quest_id; + for (size_t x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + + send_quest_file(c, bin_basename, *bin_contents, false, false); + send_quest_file(c, dat_basename, *dat_contents, false, false); + + rw_guard g(l->clients[x]->lock, true); + l->clients[x]->flags |= ClientFlag::Loading; + } + break; + } + + case LOBBY_MENU_ID: + // TODO; + break; + + default: + send_message_box(c, u"Incorrect menu ID."); + break; + } +} + +void process_change_lobby(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 84 + struct Cmd { + uint32_t menu_id; + uint32_t item_id; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + shared_ptr new_lobby; + try { + new_lobby = s->find_lobby(cmd->item_id); + } catch (const out_of_range&) { + send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby does not exist."); + return; + } + + if ((new_lobby->flags & LobbyFlag::Episode3) && !(c->flags & ClientFlag::Episode3Games)) { + send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby is for\nEpisode 3 only."); + return; + } + + s->change_client_lobby(c, new_lobby); +} + +void process_game_list_request(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 08 + check_size(size, 0); + send_game_menu(c, s); +} + +void process_change_ship(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // A0 + check_size(size, 0); + send_message_box(c, u""); // we do this to avoid the "log window in message box" bug + + static const vector version_to_port_name({ + "dc-login", "dc-login", "pc-login", "bb-patch", "gc-login", "bb-login"}); + const auto& port_name = version_to_port_name.at(static_cast(c->version)); + + send_reconnect(c, 0, s->port_configuration.at(port_name).port); +} + +void process_change_block(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // A1 + // this server doesn't have blocks; treat block change as ship change + process_change_ship(s, c, command, flag, size, data); +} + +//////////////////////////////////////////////////////////////////////////////// +// Quest commands + +vector quest_categories_menu({ + MenuItem(0x01000000, u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0), + MenuItem(0x01000001, u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0), + MenuItem(0x01000002, u"Events", u"$E$C6Quests that are part\nof an event", 0), + MenuItem(0x01000003, u"Shops", u"$E$C6Quests that contain\nshops", 0), + MenuItem(0x01000004, u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItemFlag::InvisibleOnDC | MenuItemFlag::InvisibleOnPC), + MenuItem(0x01000005, u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItemFlag::InvisibleOnDC | MenuItemFlag::InvisibleOnPC), +}); + +vector quest_battle_menu({ + MenuItem(0x01000100, u"Battle", u"$E$C6Battle mode rule\nsets", 0), +}); + +vector quest_challenge_menu({ + MenuItem(0x01000200, u"Challenge", u"$E$C6Challenge mode\nquests", 0), +}); + +vector quest_solo_menu({ + MenuItem(0x01000200, u"Solo Quests", u"$E$C6Quests that require\na single player", 0), +}); + +vector quest_government_menu({ + MenuItem(0x01000300, u"Hero in Red",u"$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline", 0), + MenuItem(0x01000301, u"The Military's Hero",u"$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline", 0), + MenuItem(0x01000302, u"The Meteor Impact Incident", u"$E$C6Quests that follow\nthe Episode 4\nstoryline", 0), +}); + +vector quest_download_menu({ + MenuItem(0x01000400, u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0), + MenuItem(0x01000401, u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0), + MenuItem(0x01000402, u"Events", u"$E$C6Quests that are part\nof an event", 0), + MenuItem(0x01000403, u"Shops", u"$E$C6Quests that contain\nshops", 0), + MenuItem(0x01000404, u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItemFlag::InvisibleOnDC | MenuItemFlag::InvisibleOnPC), + MenuItem(0x01000405, u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItemFlag::InvisibleOnDC | MenuItemFlag::InvisibleOnPC), + MenuItem(0x01000406, u"Download", u"$E$C6Quests to download\nto your Memory Card", 0), +}); + +void process_quest_list_request(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // A2 + check_size(size, 0); + + if (!s->quest_index) { + send_lobby_message_box(c, u"$C6Quests are not available."); + return; + } + + auto l = s->find_lobby(c->lobby_id); + if (!l || !l->is_game()) { + send_lobby_message_box(c, u"$C6Quests are not available\nin lobbies."); + return; + } + + vector* menu = NULL; + if ((c->version == GameVersion::BB) && flag) { + menu = &quest_government_menu; + } else { + if (l->mode == 0) { + menu = &quest_categories_menu; + } else if (l->mode == 1) { + menu = &quest_battle_menu; + } else if (l->mode == 1) { + menu = &quest_challenge_menu; + } else if (l->mode == 1) { + menu = &quest_solo_menu; + } else { + throw logic_error("no quest menu available for mode"); + } + } + + send_quest_menu(c, QUEST_FILTER_MENU_ID, *menu, false); +} + +void process_quest_ready(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // AC + check_size(size, 0); + + auto l = s->find_lobby(c->lobby_id); + if (!l || !l->is_game()) { + return; + } + + // check if any client is still loading + // TODO: we need to handle clients disconnecting while loading. probably + // process_client_disconnect needs to check for this case or something + size_t x; + { + rw_guard g(l->lock, true); + + for (x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + if (l->clients[x]->flags & ClientFlag::Loading) { + break; + } + } + } + + // if they're all done, start the quest + if (x == l->max_clients) { + send_command(l, 0xAC); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// player data commands + +void process_player_data(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 61 98 + + { + rw_guard g(c->lock, true); + switch (c->version) { + case GameVersion::PC: + check_size(size, sizeof(PSOPlayerDataPC)); + c->player.import(*reinterpret_cast(data)); + break; + case GameVersion::GC: + check_size(size, sizeof(PSOPlayerDataGC)); + c->player.import(*reinterpret_cast(data)); + break; + case GameVersion::BB: + check_size(size, sizeof(PSOPlayerDataBB)); + c->player.import(*reinterpret_cast(data)); + break; + default: + throw logic_error("player data command not implemented for version"); + } + } + + if (command == 0x61 && !c->pending_bb_save_username.empty()) { + bool failure = false; + try { + string filename = filename_for_player_bb(c->pending_bb_save_username, + c->pending_bb_save_player_index + 1); + c->player.save_player_data(filename); + } catch (const exception& e) { + u16string buffer = u"$C6PSOBB player data could\nnot be saved:\n" + decode_sjis(e.what()); + send_text_message(c, buffer.c_str()); + failure = true; + } + + try { + string filename = string_printf("system/players/player_%s_player%ld.nsb", + c->pending_bb_save_username.c_str(), c->pending_bb_save_player_index + 1); + c->player.bank.save(filename); + } catch (const exception& e) { + u16string buffer = u"$C6PSOBB bank data could\nnot be saved:\n" + decode_sjis(e.what()); + send_text_message(c, buffer.c_str()); + failure = true; + } + + if (!failure) { + send_text_message_printf(c, + "$C6PSOBB player data saved\nas player %hhu for user\n%s", + c->pending_bb_save_player_index + 1, c->pending_bb_save_username.c_str()); + } + + c->pending_bb_save_username.clear(); + } + + // if it's 61 and the client isn't in a lobby, add them to an available lobby + if ((command == 0x61) && !c->lobby_id && + (c->server_behavior == ServerBehavior::LobbyServer)) { + s->add_client_to_available_lobby(c); + } + + if (command == 0x98) { + s->change_client_lobby(c, NULL); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// subcommands + +void process_game_command(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 60 62 6C 6D C9 CB (C9 CB are ep3 only) + check_size(size, 4, 0xFFFF); + const PSOSubcommand* sub = reinterpret_cast(data); + + auto l = s->find_lobby(c->lobby_id); + if (!l) { + return; + } + + size_t count = size / 4; + process_subcommand(s, l, c, command, flag, sub, count); +} + +//////////////////////////////////////////////////////////////////////////////// +// chat commands + +void process_chat_generic(shared_ptr s, shared_ptr c, + const u16string& text) { // 06 + + if (!c->can_chat) { + return; + } + + u16string processed_text = remove_language_marker(text); + if (processed_text.empty()) { + return; + } + + if (processed_text[0] == L'$') { + auto l = s->find_lobby(c->lobby_id); + if (l) { + process_chat_command(s, l, c, &text[1]); + } + } else { + if (!c->can_chat) { + return; + } + + auto l = s->find_lobby(c->lobby_id); + if (!l) { + return; + } + + rw_guard g(l->lock, false); + for (size_t x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + send_chat_message(l->clients[x], c->license->serial_number, + c->player.disp.name, processed_text.c_str()); + } + } +} + +void process_chat_pc_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 06 + struct Cmd { + uint32_t unused[2]; + char16_t text[0]; + }; + check_size(size, sizeof(Cmd), 0xFFFF); + const auto* cmd = reinterpret_cast(data); + + process_chat_generic(s, c, cmd->text); +} + +void process_chat_dc_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + struct Cmd { + uint32_t unused[2]; + char text[0]; + }; + check_size(size, sizeof(Cmd), 0xFFFF); + const auto* cmd = reinterpret_cast(data); + + u16string decoded_s = decode_sjis(cmd->text); + process_chat_generic(s, c, decoded_s); +} + +//////////////////////////////////////////////////////////////////////////////// +// BB commands + +void process_key_config_request_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0); + send_team_and_key_config_bb(c); +} + +void process_player_preview_request_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + struct Cmd { + uint32_t player_index; + uint32_t unused; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + if (c->config.cfg.bb_game_state == ClientStateBB::ChoosePlayer) { + c->config.cfg.bb_player_index = cmd->player_index; + c->config.cfg.bb_game_state++; + send_client_init_bb(c, 0); + send_approve_player_choice_bb(c); + } else { + if (!c->license) { + c->should_disconnect = true; + return; + } + string filename = filename_for_player_bb(c->license->username, + c->config.cfg.bb_player_index); + + try { + // generate a preview + Player p; + p.load_player_data(filename); + auto preview = p.disp.to_preview(); + send_player_preview_bb(c, cmd->player_index, &preview); + + } catch (const exception&) { + // player doesn't exist + send_player_preview_bb(c, cmd->player_index, NULL); + } + } +} + +void process_client_checksum_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0); + + if (command == 0x01E8) { + send_accept_client_checksum_bb(c); + } else if (command == 0x03E8) { + send_guild_card_header_bb(c); + } else { + throw invalid_argument("unimplemented command"); + } +} + +void process_guild_card_data_request_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + struct Cmd { + uint32_t unknown; + uint32_t chunk_index; + uint32_t cont; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + if (cmd->cont) { + send_guild_card_chunk_bb(c, cmd->chunk_index); + } +} + +void process_stream_file_request_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0); + + if (command == 0x04EB) { + send_stream_file_bb(c); + } else { + throw invalid_argument("unimplemented command"); + } +} + +void process_create_character_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + struct Cmd { + uint32_t player_index; + PlayerDispDataBBPreview preview; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + if (!c->license) { + send_message_box(c, u"$C6You are not logged in."); + return; + } + if (c->player.disp.name[0]) { + send_message_box(c, u"$C6You have already loaded a character."); + return; + } + + c->config.cfg.bb_player_index = cmd->player_index; + snprintf(c->player.bank_name, 0x20, "player%" PRIu32, cmd->player_index + 1); + string player_filename = filename_for_player_bb(c->license->username, cmd->player_index); + string bank_filename = filename_for_bank_bb(c->license->username, c->player.bank_name); + string template_filename = filename_for_class_template_bb(cmd->preview.char_class); + + Player p; + try { + p.load_player_data(template_filename); + } catch (const exception& e) { + send_message_box(c, u"$C6New character could not be created.\n\nA server file is missing."); + return; + } + + try { + p.disp.apply_preview(cmd->preview); + c->player.disp.stats = s->level_table->base_stats_for_class(c->player.disp.char_class); + } catch (const exception& e) { + send_message_box(c, u"$C6New character could not be created.\n\nTemplate application failed."); + return; + } + + try { + p.save_player_data(player_filename); + } catch (const exception& e) { + send_message_box(c, u"$C6New character could not be created.\n\nThe disk is full or write-protected."); + return; + } + + try { + p.bank.save(bank_filename); + } catch (const exception& e) { + unlink(player_filename); + send_message_box(c, u"$C6New bank could not be created.\n\nThe disk is full or write-protected."); + return; + } + + send_client_init_bb(c, 0); + send_approve_player_choice_bb(c); +} + +void process_change_account_data_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + union Cmd { + uint32_t option; // 01ED + uint8_t symbol_chats[0x4E0]; // 02ED + uint8_t chat_shortcuts[0xA40]; // 03ED + uint8_t key_config[0x16C]; // 04ED + uint8_t pad_config[0x38]; // 05ED + uint8_t tech_menu[0x28]; // 06ED + uint8_t customize[0xE8]; // 07ED + }; + const auto* cmd = reinterpret_cast(data); + + switch (command) { + case 0x01ED: + check_size(size, sizeof(cmd->option)); + c->player.option_flags = cmd->option; + break; + case 0x02ED: + check_size(size, sizeof(cmd->symbol_chats)); + memcpy(c->player.symbol_chats, cmd->symbol_chats, 0x04E0); + break; + case 0x03ED: + check_size(size, sizeof(cmd->chat_shortcuts)); + memcpy(c->player.shortcuts, cmd->chat_shortcuts, 0x0A40); + break; + case 0x04ED: + check_size(size, sizeof(cmd->key_config)); + memcpy(&c->player.key_config.key_config, cmd->key_config, 0x016C); + break; + case 0x05ED: + check_size(size, sizeof(cmd->pad_config)); + memcpy(&c->player.key_config.joystick_config, cmd->pad_config, 0x0038); + break; + case 0x06ED: + check_size(size, sizeof(cmd->tech_menu)); + memcpy(&c->player.tech_menu_config, cmd->tech_menu, 0x0028); + break; + case 0x07ED: + check_size(size, sizeof(cmd->customize)); + memcpy(c->player.disp.config, cmd->customize, 0xE8); + break; + default: + throw invalid_argument("unknown account command"); + } +} + +void process_return_player_data_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, sizeof(PlayerBB)); + const PlayerBB* cmd = reinterpret_cast(data); + + // we only trust the player's quest data and challenge data. + memcpy(&c->player.challenge_data, &cmd->challenge_data, sizeof(cmd->challenge_data)); + memcpy(&c->player.quest_data1, &cmd->quest_data1, sizeof(cmd->quest_data1)); + memcpy(&c->player.quest_data2, &cmd->quest_data2, sizeof(cmd->quest_data2)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Lobby commands + +void process_change_arrow_color(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0); + + c->lobby_arrow_color = flag; + auto l = s->find_lobby(c->lobby_id); + if (l) { + send_arrow_update(l); + } +} + +void process_card_search(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 40 + struct Cmd { + uint32_t player_tag; + uint32_t searcher_serial_number; + uint32_t target_serial_number; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + try { + auto result = s->find_client(NULL, cmd->target_serial_number); + auto result_lobby = s->find_lobby(result->lobby_id); + send_card_search_result(s, c, result, result_lobby); + } catch (const out_of_range&) { } +} + +//////////////////////////////////////////////////////////////////////////////// +// Info board commands + +void process_info_board_request(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // D8 + check_size(size, 0); + auto l = s->find_lobby(c->lobby_id); + send_info_board(c, l); +} + +void process_write_info_board_pc_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // D9 + check_size(size, 0, 2 * 0xAC); + char16cpy(c->player.info_board, reinterpret_cast(data), 0xAC); +} + +void process_write_info_board_dc_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // D9 + check_size(size, 0, 0xAC); + decode_sjis(c->player.info_board, reinterpret_cast(data), 0xAC); +} + +void process_set_auto_reply_pc_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0, 2 * 0xAC); + if (size == 0) { + c->player.auto_reply[0] = 0; + } else { + char16cpy(c->player.auto_reply, reinterpret_cast(data), 0xAC); + } +} + +void process_set_auto_reply_dc_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0, 0xAC); + if (size == 0) { + c->player.auto_reply[0] = 0; + } else { + decode_sjis(c->player.auto_reply, reinterpret_cast(data), 0xAC); + } +} + +void process_disable_auto_reply(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0); + c->player.auto_reply[0] = 0; +} + +void process_set_blocked_list(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0x78); + memcpy(c->player.blocked, data, 0x78); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// Game commands + +shared_ptr create_game_generic(shared_ptr s, + shared_ptr c, const char16_t* name, const char16_t* password, + uint8_t episode, uint8_t difficulty, uint8_t battle, uint8_t challenge, + uint8_t solo) { + + static const uint32_t variation_maxes_online[3][0x20] = { + {1, 1, 1, 5, 1, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, + 3, 2, 3, 2, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + {1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 3, 1, 3, 1, 3, + 2, 2, 1, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1}, + {1, 1, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 1, 1, 3, + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}; + + static const uint32_t variation_maxes_solo[3][0x20] = { + {1, 1, 1, 3, 1, 3, 3, 1, 3, 1, 3, 1, 3, 2, 3, 2, + 3, 2, 3, 2, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + {1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 3, 1, 3, 1, 3, + 2, 2, 1, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + {1, 1, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 1, 1, 3, + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}; + + static const uint32_t default_minimum_levels[3][4] = { + {0, 19, 39, 79}, // episode 1 + {0, 29, 49, 89}, // episode 2 + {0, 39, 79, 109}}; // episode 4 + + if (episode == 0) { + episode = 0xFF; + } + if (((episode != 0xFF) && (episode > 3)) || (episode == 0)) { + throw invalid_argument("incorrect episode number"); + } + if (difficulty > 3) { + throw invalid_argument("incorrect difficulty level"); + } + + auto current_lobby = s->find_lobby(c->lobby_id); + if (!current_lobby) { + throw invalid_argument("cannot make a game from outside any lobby"); + } + + uint8_t min_level = ((episode == 0xFF) ? 0 : default_minimum_levels[episode - 1][difficulty]); + if (min_level > c->player.disp.level) { + throw invalid_argument("level too low for difficulty"); + } + + shared_ptr game(new Lobby()); + char16cpy(game->name, name, 0x10); + char16cpy(game->password, password, 0x10); + game->version = c->version; + game->section_id = c->player.disp.section_id; + game->episode = episode; + game->difficulty = difficulty; + if (battle) { + game->mode = 1; + } + if (challenge) { + game->mode = 2; + } + if (solo) { + game->mode = 3; + } + game->event = Lobby::game_event_for_lobby_event(current_lobby->event); + game->block = 0xFF; + game->max_clients = 4; + game->flags = ((game->episode != 0xFF) ? 0 : LobbyFlag::Episode3) | LobbyFlag::IsGame; + game->min_level = min_level; + game->max_level = 0xFFFFFFFF; + + // TODO: cache these somewhere so we don't read the file every time, lolz + game->rare_item_set.reset(new RareItemSet("system/blueburst/ItemRT.rel", + game->episode - 1, game->difficulty, game->section_id)); + + if (game->version == GameVersion::BB) { + for (size_t x = 0; x < 4; x++) { + game->next_item_id[x] = (0x00200000 * x) + 0x00010000; + } + game->next_game_item_id = 0x00810000; + game->enemies.resize(0x0B50); + + const auto* bp_subtable = s->battle_params->get_subtable(game->mode == 3, + game->episode - 1, game->difficulty); + + if (game->mode == 3) { + for (size_t x = 0; x < 0x20; x++) { + game->variations[x] = rand() % variation_maxes_solo[(episode - 1)][x]; + } + for (size_t x = 0; x < 0x10; x++) { + for (const char* type_char = "sm"; *type_char; type_char++) { + try { + auto filename = string_printf( + "system/blueburst/map/%c%hhu%zu%" PRIu32 "%" PRIu32 ".dat", + *type_char, game->episode, x, game->variations[x * 2], + game->variations[(x * 2) + 1]); + game->enemies = load_map(filename.c_str(), game->episode, + game->difficulty, bp_subtable, false); + break; + } catch (const exception& e) { } + } + if (game->enemies.empty()) { + throw runtime_error("failed to load any map data"); + } + } + } else { + for (size_t x = 0; x < 0x20; x++) { + game->variations[x] = rand() % variation_maxes_online[(episode - 1)][x]; + } + for (size_t x = 0; x < 0x10; x++) { + auto filename = string_printf( + "system/blueburst/map/m%hhu%zu%" PRIu32 "%" PRIu32 ".dat", + game->episode, x, game->variations[x * 2], + game->variations[(x * 2) + 1]); + game->enemies = load_map(filename.c_str(), game->episode, + game->difficulty, bp_subtable, false); + } + } + } + + return game; +} + +void process_create_game_pc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // C1 + struct Cmd { + uint32_t unused[2]; + char16_t name[0x10]; + char16_t password[0x10]; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t challenge_mode; + uint8_t unused2; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + auto game = create_game_generic(s, c, cmd->name, cmd->password, 1, + cmd->difficulty, cmd->battle_mode, cmd->challenge_mode, 0); + + s->add_lobby(game); + s->change_client_lobby(c, game); + c->flags |= ClientFlag::Loading; +} + +void process_create_game_dc_gc(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // C1 + struct Cmd { + uint32_t unused[2]; + char name[0x10]; + char password[0x10]; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t challenge_mode; + uint8_t episode; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + uint8_t episode = cmd->episode; + if (c->version == GameVersion::DC) { + episode = 1; + } + if (c->flags & ClientFlag::Episode3Games) { + episode = 0xFF; + } + + u16string name = decode_sjis(cmd->name); + u16string password = decode_sjis(cmd->password); + + auto game = create_game_generic(s, c, name.c_str(), password.c_str(), + episode, cmd->difficulty, cmd->battle_mode, cmd->challenge_mode, 0); + + s->add_lobby(game); + s->change_client_lobby(c, game); + c->flags |= ClientFlag::Loading; +} + +void process_create_game_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // C1 + struct Cmd { + uint32_t unused[2]; + char16_t name[0x10]; + char16_t password[0x10]; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t challenge_mode; + uint8_t episode; + uint8_t solo_mode; + uint8_t unused2[3]; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + uint8_t episode = cmd->episode; + if (c->version == GameVersion::DC) { + episode = 1; + } + if (c->flags & ClientFlag::Episode3Games) { + episode = 0xFF; + } + + auto game = create_game_generic(s, c, cmd->name, cmd->password, + episode, cmd->difficulty, cmd->battle_mode, cmd->challenge_mode, + cmd->solo_mode); + + s->add_lobby(game); + s->change_client_lobby(c, game); + c->flags |= ClientFlag::Loading; + + game->assign_item_ids_for_player(c->lobby_client_id, c->player.inventory); +} + +void process_lobby_name_request(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 8A + check_size(size, 0); + auto l = s->find_lobby(c->lobby_id); + if (!l) { + throw invalid_argument("client not in any lobby"); + } + send_lobby_name(c, l->name); +} + +void process_client_ready(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 6F + check_size(size, 0); + + auto l = s->find_lobby(c->lobby_id); + if (!l || !l->is_game()) { + // go home client; you're drunk + throw invalid_argument("ready command cannot be sent outside game"); + } + c->flags &= (~ClientFlag::Loading); + + // tell the other players to stop waiting for the new player to load + send_resume_game(l); + // tell the new player the time + send_server_time(c); + // get character info + send_get_player_info(c); +} + +//////////////////////////////////////////////////////////////////////////////// +// Team commands + +void process_team_command_bb(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { // EA + + if (command == 0x01EA) { + send_lobby_message_box(c, u"$C6Teams are not supported."); + } else { + throw invalid_argument("unimplemented team command"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Patch server commands + +void process_encryption_ok_patch(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + check_size(size, 0); + send_command(c, 0x04); // this requests the user's login information +} + +void process_login_patch(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + struct Cmd { + uint32_t unused[3]; + char username[0x10]; + char password[0x10]; + }; + check_size(size, sizeof(Cmd)); + const auto* cmd = reinterpret_cast(data); + + u16string message = u"\ +$C7NewServ Patch Server v1.0\n\n\ +Please note that this server is for private use only.\n\ +This server is not affiliated with, sponsored by, or in any\n\ +other way connected to SEGA or Sonic Team, and is owned\n\ +and operated completely independently.\n\n\ +License check: "; + try { + c->license = s->license_manager->verify_bb(cmd->username, cmd->password); + message += u"OK"; + } catch (const exception& e) { + message += decode_sjis(e.what()); + } + + send_message_box(c, message.c_str()); + send_check_directory_patch(c, "."); + send_check_directory_patch(c, "data"); + send_check_directory_patch(c, "scene"); + send_command(c, 0x0A); + send_command(c, 0x0A); + send_command(c, 0x0A); + + // this command terminates the patch connection successfully. PSO complains if + // we don't check the above directories though + send_command(c, 0x0012, 0x00000000); +} + +//////////////////////////////////////////////////////////////////////////////// +// Command pointer arrays + +void process_ignored_command(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { } + +void process_unimplemented_command(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + log(WARNING, "unknown command: size=%04X command=%04X flag=%08X\n", + size, command, flag); + throw invalid_argument("unimplemented command"); +} + + + +typedef void (*process_command_t)(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data); + +// The entries in these arrays correspond to the ID of the command received. For +// instance, if a command 6C is received, the function at position 0x6C in the +// array corresponding to the client's version is called. +static process_command_t dc_handlers[0x100] = { + // 00 + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, process_chat_dc_gc, NULL, + process_game_list_request, process_menu_selection, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 10 + process_menu_selection, NULL, NULL, process_ignored_command, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + + // 20 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 30 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 40 + process_card_search, NULL, NULL, NULL, + process_ignored_command, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 50 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 60 + process_game_command, NULL, process_game_command, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_game_command, process_game_command, NULL, process_client_ready, + + // 70 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 80 + NULL, NULL, NULL, NULL, + process_change_lobby, NULL, NULL, NULL, + NULL, process_change_arrow_color, process_lobby_name_request, NULL, + NULL, NULL, NULL, NULL, + + // 90 + NULL, NULL, NULL, NULL, + NULL, NULL, process_client_checksum, NULL, + NULL, process_ignored_command, NULL, NULL, + NULL, NULL, NULL, NULL, + + // A0 + process_change_ship, process_change_block, process_quest_list_request, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + process_quest_ready, NULL, NULL, NULL, + + // B0 + NULL, process_server_time_request, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // C0 + NULL, process_create_game_dc_gc, NULL, NULL, + NULL, NULL, process_set_blocked_list, process_set_auto_reply_dc_gc, + process_disable_auto_reply, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // D0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_info_board_request, process_write_info_board_dc_gc, NULL, NULL, + NULL, NULL, NULL, NULL, + + // E0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // F0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, +}; + +static process_command_t pc_handlers[0x100] = { + // 00 + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, process_chat_pc_bb, NULL, + process_game_list_request, process_menu_selection, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 10 + process_menu_selection, NULL, NULL, process_ignored_command, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + + // 20 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 30 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 40 + process_card_search, NULL, NULL, NULL, + process_ignored_command, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 50 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 60 + process_game_command, process_player_data, process_game_command, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_game_command, process_game_command, NULL, process_client_ready, + + // 70 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 80 + NULL, NULL, NULL, NULL, + process_change_lobby, NULL, NULL, NULL, + NULL, process_change_arrow_color, process_lobby_name_request, NULL, + NULL, NULL, NULL, NULL, + + // 90 + NULL, NULL, NULL, NULL, + NULL, NULL, process_client_checksum, NULL, + process_player_data, process_ignored_command, process_login_a_dc_pc_gc, NULL, + process_login_c_dc_pc_gc, process_login_d_e_pc_gc, process_login_d_e_pc_gc, NULL, + + // A0 + process_change_ship, process_change_block, process_quest_list_request, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + process_quest_ready, NULL, NULL, NULL, + + // B0 + NULL, process_server_time_request, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // C0 + NULL, process_create_game_pc, NULL, NULL, + NULL, NULL, process_set_blocked_list, process_set_auto_reply_pc_bb, + process_disable_auto_reply, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // D0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_info_board_request, process_write_info_board_pc_bb, NULL, NULL, + NULL, NULL, NULL, NULL, + + // E0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // F0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, +}; + +static process_command_t gc_handlers[0x100] = { + // 00 + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, process_chat_dc_gc, NULL, + process_game_list_request, process_menu_selection, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 10 + process_menu_selection, NULL, NULL, process_ignored_command, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + + // 20 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 30 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 40 + process_card_search, NULL, NULL, NULL, + process_ignored_command, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 50 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 60 + process_game_command, process_player_data, process_game_command, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_game_command, process_game_command, NULL, process_client_ready, + + // 70 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 80 + NULL, NULL, NULL, NULL, + process_change_lobby, NULL, NULL, NULL, + NULL, process_change_arrow_color, process_lobby_name_request, NULL, + NULL, NULL, NULL, NULL, + + // 90 + NULL, NULL, NULL, NULL, + NULL, NULL, process_client_checksum, NULL, + process_player_data, process_ignored_command, NULL, NULL, + process_login_c_dc_pc_gc, process_login_d_e_pc_gc, process_login_d_e_pc_gc, NULL, + + // A0 + process_change_ship, process_change_block, process_quest_list_request, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + process_quest_ready, NULL, NULL, NULL, + + // B0 + NULL, process_server_time_request, NULL, NULL, + NULL, NULL, NULL, process_ignored_command, + process_ignored_command, NULL, process_ep3_jukebox, NULL, + NULL, NULL, NULL, NULL, + + // C0 + NULL, process_create_game_dc_gc, NULL, NULL, + NULL, NULL, process_set_blocked_list, process_set_auto_reply_dc_gc, + process_disable_auto_reply, process_game_command, process_ep3_server_data_request, process_game_command, + NULL, NULL, NULL, NULL, + + // D0 + NULL, NULL, NULL, NULL, + NULL, NULL, process_ignored_command, NULL, + process_info_board_request, process_write_info_board_dc_gc, NULL, process_verify_license_gc, + process_ep3_menu_challenge, NULL, NULL, NULL, + + // E0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_create_game_dc_gc, NULL, NULL, NULL, + + // F0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, +}; + +static process_command_t bb_handlers[0x100] = { + // 00 + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, process_chat_pc_bb, NULL, + process_game_list_request, process_menu_selection, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 10 + process_menu_selection, NULL, NULL, process_ignored_command, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + + // 20 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 30 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 40 + process_card_search, NULL, NULL, NULL, + process_ignored_command, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 50 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 60 + process_game_command, process_player_data, process_game_command, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_game_command, process_game_command, NULL, process_client_ready, + + // 70 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // 80 + NULL, NULL, NULL, NULL, + process_change_lobby, NULL, NULL, NULL, + NULL, process_change_arrow_color, process_lobby_name_request, NULL, + NULL, NULL, NULL, NULL, + + // 90 + NULL, NULL, NULL, process_login_bb, + NULL, NULL, NULL, NULL, + process_player_data, process_ignored_command, NULL, NULL, + NULL, NULL, NULL, NULL, + + // A0 + process_change_ship, process_change_block, process_quest_list_request, NULL, + NULL, NULL, NULL, NULL, + NULL, process_ignored_command, NULL, NULL, + process_quest_ready, NULL, NULL, NULL, + + // B0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // C0 + NULL, process_create_game_bb, NULL, NULL, + NULL, NULL, process_set_blocked_list, process_set_auto_reply_pc_bb, + process_disable_auto_reply, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + // D0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + process_info_board_request, process_write_info_board_pc_bb, NULL, NULL, + process_guild_card_data_request_bb, NULL, NULL, NULL, + + // E0 + process_key_config_request_bb, NULL, NULL, process_player_preview_request_bb, + NULL, process_create_character_bb, NULL, process_return_player_data_bb, + process_client_checksum_bb, NULL, process_team_command_bb, process_stream_file_request_bb, + process_ignored_command, process_change_account_data_bb, NULL, NULL, + + // F0 + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, +}; + +static process_command_t patch_handlers[0x100] = { + // 00 + NULL, NULL, process_encryption_ok_patch, NULL, + process_login_patch, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 10 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 20 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 30 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 40 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 50 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 60 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 70 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 80 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // 90 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // A0 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // B0 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // C0 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // D0 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // E0 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // F0 + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, +}; + +static process_command_t* handlers[6] = { + dc_handlers, dc_handlers, pc_handlers, patch_handlers, gc_handlers, bb_handlers}; + +void process_command(shared_ptr s, shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data) { + auto fn = handlers[static_cast(c->version)][command & 0xFF]; + if (!fn) { + fn(s, c, command, flag, size, data); + } else { + process_unimplemented_command(s, c, command, flag, size, data); + } +} diff --git a/ReceiveCommands.hh b/ReceiveCommands.hh new file mode 100644 index 00000000..c2443ecd --- /dev/null +++ b/ReceiveCommands.hh @@ -0,0 +1,12 @@ +#include +#include + +#include "Client.hh" +#include "ServerState.hh" + + +void process_connect(std::shared_ptr s, std::shared_ptr c); +void process_disconnect(std::shared_ptr s, + std::shared_ptr c); +void process_command(std::shared_ptr s, std::shared_ptr c, + uint16_t command, uint32_t flag, uint16_t size, const void* data); diff --git a/ReceiveSubcommands.cc b/ReceiveSubcommands.cc new file mode 100644 index 00000000..81000d85 --- /dev/null +++ b/ReceiveSubcommands.cc @@ -0,0 +1,1145 @@ +#include "ReceiveSubcommands.hh" + +#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 BB client sends a game command +// (60, 62, 6C, or 6D) that must be handled by the server. + + + +struct ItemSubcommand { + uint8_t command; + uint8_t size; + uint8_t client_id; + uint8_t unused; + uint32_t item_id; + uint32_t amount; +}; + + + +void check_size(uint16_t size, uint16_t min_size, uint16_t max_size) { + if (size < min_size) { + throw runtime_error("command too small"); + } + if (max_size == 0) { + max_size = min_size; + } + if (size > max_size) { + throw runtime_error("command too large"); + } +} + + + +bool command_is_private(uint8_t command) { + // TODO: are either of the ep3 commands private? looks like not + return (command == 0x62) || (command == 0x6D); +} + + + +void forward_subcommand(shared_ptr l, shared_ptr c, + uint8_t command, uint8_t flag, const PSOSubcommand* p, + size_t count) { + + if (command_is_private(command)) { + if (flag >= l->max_clients) { + return; + } + auto target = l->clients[flag]; + if (!target) { + return; + } + send_command(target, command, flag, p, count * 4); + } else { + send_command(l, command, flag, p, count * 4); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +// Chat commands and the like + +// client requests to send a guild card +static void process_subcommand_send_guild_card(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 9, 0xFFFF); + + if (!command_is_private(command) || !l || flag >= l->max_clients || + (!l->clients[flag]) || (p->byte[1] != count)) { + return; + } + + if (c->version == GameVersion::GC) { + if (count < 0x25) { + return; + } + decode_sjis(c->player.guild_card_desc, + reinterpret_cast(&p[9].byte[0]), 0x58); + } + + send_guild_card(l->clients[flag], c); +} + +// client sends a symbol chat +static void process_subcommand_symbol_chat(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 2, 0xFFFF); + + if (!c->can_chat || (p->byte[1] != count) || (p->byte[1] < 2) || + (p[1].byte[0] != c->lobby_client_id)) { + return; + } + forward_subcommand(l, c, command, flag, p, count); +} + +// client sends a word select chat +static void process_subcommand_word_select(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 8, 0xFFFF); + + if (!c->can_chat || (p->byte[1] != count) || (p->byte[1] < 8) || + (p->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, p, count); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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 s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 2, 0xFFFF); + if (!l->is_game() || (p->byte[1] != count)) { + return; + } + c->area = p[1].dword; + forward_subcommand(l, c, command, flag, p, count); +} + +// when a player is hit by a monster, heal them if infinite HP is enabled +static void process_subcommand_hit_by_monster(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (!l->is_game() || (p->byte[2] != c->lobby_client_id)) { + return; + } + forward_subcommand(l, c, command, flag, p, count); + if ((l->flags & LobbyFlag::CheatsEnabled) && c->infinite_hp) { + send_player_stats_change(l, c, PlayerStatsChange::AddHP, 1020); + } +} + +// when a player casts a tech, restore TP if infinite TP is enabled +static void process_subcommand_use_technique(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (!l->is_game() || (p->byte[1] != count) || (p->byte[2] != c->lobby_client_id)) { + return; + } + forward_subcommand(l, c, command, flag, p, count); + if ((l->flags & LobbyFlag::CheatsEnabled) && c->infinite_hp) { + send_player_stats_change(l, c, PlayerStatsChange::AddTP, 255); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// BB Item commands + +// player drops an item +static void process_subcommand_drop_item(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 6); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint8_t client_id; + uint8_t unused; + uint16_t unused2; // should be 1 + uint16_t area; + uint32_t item_id; + float x; + float y; + float z; + }; + auto* cmd = reinterpret_cast(p); + + if ((cmd->size != 6) || (cmd->client_id != c->lobby_client_id)) { + return; + } + + PlayerInventoryItem item; + { + rw_guard g(c->lock, true); + c->player.remove_item(cmd->item_id, 0, &item); + } + + // note: this locks the lobby itself; we don't need to manually do it + l->add_item(item); + } + forward_subcommand(l, c, command, flag, p, count); +} + +// player splits a stack and drops part of it +static void process_subcommand_drop_stacked_item(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 6); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint8_t client_id; + uint8_t unused; + uint16_t area; + uint16_t unused2; + float x; + float y; + uint32_t item_id; + uint32_t amount; + }; + auto* cmd = reinterpret_cast(p); + + if (!l->is_game() || (cmd->size != 6) || (cmd->client_id != c->lobby_client_id)) { + return; + } + + PlayerInventoryItem item; + { + rw_guard g(c->lock, true); + 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); + } + + // note: this locks the lobby itself; we don't need to manually do it + l->add_item(item); + + send_drop_stacked_item(l, c, item.data, cmd->area, cmd->x, cmd->y); + + } else { + forward_subcommand(l, c, command, flag, p, count); + } +} + +// player requests to pick up an item +static void process_subcommand_pick_up_item(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 3); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint8_t client_id; + uint8_t unused; + uint32_t item_id; + uint8_t area; + uint8_t unused2[3]; + }; + auto* cmd = reinterpret_cast(p); + + if (!l->is_game() || (cmd->size != 3) || (cmd->client_id != c->lobby_client_id)) { + return; + } + + PlayerInventoryItem item; + l->remove_item(cmd->item_id, &item); + + { + rw_guard g(c->lock, true); + c->player.add_item(item); + } + + send_pick_up_item(l, c, item.data.item_id, cmd->area); + + } else { + forward_subcommand(l, c, command, flag, p, count); + } +} + +// player equips an item +static void process_subcommand_equip_unequip_item(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 3); + + if (l->version == GameVersion::BB) { + auto* cmd = reinterpret_cast(p); + if ((cmd->size != 3) || (cmd->client_id != c->lobby_client_id)) { + return; + } + + rw_guard g(c->lock, true); + 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, p, count); + } +} + +// player uses an item (see ClientUseItem for specific item handlers) +static void process_subcommand_use_item(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 2); + + if (l->version == GameVersion::BB) { + auto* cmd = reinterpret_cast(p); + if ((cmd->size != 2) || (cmd->client_id != c->lobby_client_id)) { + return; + } + + rw_guard g(c->lock, true); + 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_locked(l, c, index); + } + + forward_subcommand(l, c, command, flag, p, count); +} + +// player opens the bank window +static void process_subcommand_open_bank(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if ((l->version == GameVersion::BB) && l->is_game()) { + send_bank(c); + } +} + +// player performs some bank action +static void process_subcommand_bank_action(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 4); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t subcommand; + uint8_t size; + uint16_t unused; + uint32_t item_id; + uint32_t meseta_amount; + uint8_t action; + uint8_t item_amount; + uint16_t unused2; + }; + auto* cmd = reinterpret_cast(p); + + if (!l->is_game() || (cmd->size != 4)) { + return; + } + + rw_guard g(c->lock, true); + 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(0xFFFFFFFF); + 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 s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 31); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint16_t unused; + uint32_t item_ids[30]; + }; + auto* cmd = reinterpret_cast(p); + + if (cmd->size != 31) { + return; + } + + PlayerInventory sorted; + memset(&sorted, 0, sizeof(PlayerInventory)); + + rw_guard g(c->lock, true); + 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 PSOSubcommand* p, size_t count) { + check_size(count, 6); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint16_t unused; + uint8_t area; + uint8_t monster_id; + uint16_t request_id; + float x; + float y; + uint32_t unknown[2]; + }; + auto* cmd = reinterpret_cast(p); + + if ((cmd->size != 6) || !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->monster_id <= 0x65) { + is_rare = sample_rare_item(l->rare_item_set->rares[cmd->monster_id].probability); + } + } + + if (is_rare) { + memcpy(&item.data.item_data1d, l->rare_item_set->rares[cmd->monster_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_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(0xFFFFFFFF); + + l->add_item(item); + send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->y, + cmd->request_id); + } +} + +// 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 PSOSubcommand* p, size_t count) { + check_size(count, 10); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint16_t unused; + uint8_t area; + uint8_t unused2; + uint16_t request_id; + float x; + float y; + uint32_t unknown[6]; + }; + auto* cmd = reinterpret_cast(p); + + if ((cmd->size != 10) || !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_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(0xFFFFFFFF); + + l->add_item(item); + send_drop_item(l, item.data, false, cmd->area, cmd->x, cmd->y, + cmd->request_id); + } +} + +// monster hit by player +static void process_subcommand_monster_hit(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 10); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint16_t enemy_id2; + uint16_t enemy_id; + uint16_t damage; + uint32_t flags; + }; + auto* cmd = reinterpret_cast(p); + + if (cmd->size != 10) { + return; + } + + if (cmd->enemy_id >= l->enemies.size()) { + return; + } + + rw_guard g(l->lock, true); + 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, p, count); +} + +// monster killed by player +static void process_subcommand_monster_killed(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 3); + + forward_subcommand(l, c, command, flag, p, count); + + if (l->version == GameVersion::BB) { + struct Cmd { + uint8_t command; + uint8_t size; + uint16_t enemy_id2; + uint16_t enemy_id; + uint16_t killer_client_id; + uint32_t unused; + }; + auto* cmd = reinterpret_cast(p); + + if (!l->is_game() || (cmd->size != 3) || (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; + } + + rw_guard g(l->lock, true); + 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); + } + + rw_guard g(other_c->lock, true); + 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 s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 3); + + if (l->version == GameVersion::BB) { + auto* cmd = reinterpret_cast(p); + if ((cmd->size != 3) || !l->is_game()) { + return; + } + l->remove_item(cmd->item_id, NULL); + } + + forward_subcommand(l, c, command, flag, p, count); +} + +// player requests to tekk an item +static void process_subcommand_identify_item(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + check_size(count, 3); + + if (l->version == GameVersion::BB) { + auto* cmd = reinterpret_cast(p); + if (!l->is_game() || (cmd->size != 3) || (cmd->client_id != c->lobby_client_id)) { + return; + } + + rw_guard g(c->lock, true); + 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 + PSOSubcommand sub[6]; + sub[0].byte[0] = 0xB9; + sub[0].byte[1] = 0x06; + sub[0].word[1] = c->lobby_client_id; + memcpy(&sub[1], &c->player.identify_result.data, sizeof(ItemData)); + send_command(l, 0x60, 0x00, sub, 0x18); + + } else { + forward_subcommand(l, c, command, flag, p, count); + } +} + +// 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 PSOSubcommand* p, size_t count) { +// check_size(count, 3); +// +// if (l->version == GameVersion::BB) { +// auto* cmd = reinterpret_cast(p); +// if ((cmd->size != 3) || (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, p, count); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// + +static void process_subcommand_forward_check_size(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (p->byte[1] != count) { + return; + } + forward_subcommand(l, c, command, flag, p, count); +} + +static void process_subcommand_forward_check_game(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (!l->is_game()) { + return; + } + forward_subcommand(l, c, command, flag, p, count); +} + +static void process_subcommand_forward_check_game_loading(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (!l->is_game() || !l->any_client_loading()) { + return; + } + forward_subcommand(l, c, command, flag, p, count); +} + +static void process_subcommand_forward_check_size_client(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if ((p->byte[1] != count) || (p->byte[2] != c->lobby_client_id)) { + return; + } + forward_subcommand(l, c, command, flag, p, count); +} + +static void process_subcommand_forward_check_size_game(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (!l->is_game() || (p->byte[1] != count)) { + return; + } + forward_subcommand(l, c, command, flag, p, count); +} + +// used for invalid commands. normally, clients should be disconnected - to restore this behavior, change the return value back to SUBCOMMAND_ERROR_INVALID_COMMAND. +static void process_subcommand_invalid(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (command_is_private(command)) { + log(WARNING, "invalid subcommand: %02X (%d of them) (private to player %d)", + p->byte[0], count, flag); + } else { + log(WARNING, "invalid subcommand: %02X (%d of them) (public)", + p->byte[0], count); + } +} + +// used when an error occurs (unknown commands, etc). normally, clients should be disconnected - to restore this behavior, change the return value back to SUBCOMMAND_ERROR_INVALID_COMMAND. +static void process_subcommand_unimplemented(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* p, size_t count) { + if (command_is_private(command)) { + log(WARNING, "unknown subcommand: %02X (%d of them) (private to player %d)", + p->byte[0], count, flag); + } else { + log(WARNING, "unknown subcommand: %02X (%d of them) (public)", + p->byte[0], count); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +// 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 PSOSubcommand* p, size_t count); + +subcommand_handler_t subcommand_handlers[0x100] = { + // 00 + process_subcommand_invalid, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_send_guild_card, + process_subcommand_symbol_chat, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_monster_hit, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // 10 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size, + // 20 + process_subcommand_forward_check_size, + process_subcommand_change_area, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_game, + process_subcommand_equip_unequip_item, + process_subcommand_equip_unequip_item, + process_subcommand_use_item, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_drop_item, + process_subcommand_unimplemented, + process_subcommand_forward_check_size, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_hit_by_monster, + // 30 + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_game, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size, + process_subcommand_forward_check_size, + // 40 + process_subcommand_forward_check_size, + process_subcommand_unimplemented, + process_subcommand_forward_check_size, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_use_technique, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_hit_by_monster, + process_subcommand_hit_by_monster, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + // 50 + process_subcommand_forward_check_size_client, + process_subcommand_unimplemented, + process_subcommand_forward_check_size, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_pick_up_item, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // 60 + process_subcommand_enemy_drop_item, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_destroy_item, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_game_loading, + process_subcommand_forward_check_game_loading, + process_subcommand_forward_check_game_loading, + process_subcommand_forward_check_game_loading, + process_subcommand_unimplemented, + // 70 + process_subcommand_unimplemented, + process_subcommand_forward_check_game_loading, + process_subcommand_unimplemented, + process_subcommand_invalid, + process_subcommand_word_select, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size, + process_subcommand_unimplemented, + process_subcommand_forward_check_size, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // 80 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + process_subcommand_forward_check_size_game, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_client, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // 90 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // A0 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_box_drop_item, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_client, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_client, + process_subcommand_forward_check_size_client, + // B0 + process_subcommand_forward_check_size_client, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_identify_item, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_open_bank, + process_subcommand_unimplemented, + process_subcommand_bank_action, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // C0 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_drop_stacked_item, + process_subcommand_sort_inventory, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_monster_killed, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_forward_check_size_game, + // D0 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // E0 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + // F0 + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, + process_subcommand_unimplemented, +}; + +void process_subcommand(shared_ptr s, shared_ptr l, + shared_ptr c, uint8_t command, uint8_t flag, + const PSOSubcommand* sub, size_t count) { + subcommand_handlers[sub->byte[0]](s, l, c, command, flag, sub, count); +} diff --git a/ReceiveSubcommands.hh b/ReceiveSubcommands.hh new file mode 100644 index 00000000..d42ca758 --- /dev/null +++ b/ReceiveSubcommands.hh @@ -0,0 +1,14 @@ +#include + +#include "PSOProtocol.hh" +#include "Client.hh" +#include "Lobby.hh" +#include "Client.hh" +#include "ServerState.hh" + + +void check_size(uint16_t size, uint16_t min_size, uint16_t max_size = 0); + +void process_subcommand(std::shared_ptr s, + std::shared_ptr l, std::shared_ptr c, uint8_t command, + uint8_t flag, const PSOSubcommand* sub, size_t count); diff --git a/SendCommands.cc b/SendCommands.cc new file mode 100644 index 00000000..7eb097db --- /dev/null +++ b/SendCommands.cc @@ -0,0 +1,2109 @@ +#include "SendCommands.hh" + +#include +#include +#include + +#include "PSOProtocol.hh" +#include "FileContentsCache.hh" +#include "Text.hh" + +using namespace std; + + + +static FileContentsCache file_cache; + + + +void send_command(shared_ptr c, uint16_t command, uint32_t flag, + const void* data, size_t size) { + string send_data; + + switch (c->version) { + case GameVersion::GC: + case GameVersion::DC: { + PSOCommandHeaderDCGC header; + header.command = command; + header.flag = flag; + header.size = sizeof(header) + size; + send_data.append(reinterpret_cast(&header), sizeof(header)); + if (size) { + send_data.append(reinterpret_cast(data), size); + send_data.resize((send_data.size() + 3) & ~3); + } + break; + } + + case GameVersion::PC: + case GameVersion::Patch: { + PSOCommandHeaderPC header; + header.size = sizeof(header) + size; + header.command = command; + header.flag = flag; + send_data.append(reinterpret_cast(&header), sizeof(header)); + if (size) { + send_data.append(reinterpret_cast(data), size); + send_data.resize((send_data.size() + 3) & ~3); + } + break; + } + + case GameVersion::BB: { + PSOCommandHeaderBB header; + header.size = sizeof(header) + size; + header.command = command; + header.flag = flag; + send_data.append(reinterpret_cast(&header), sizeof(header)); + if (size) { + send_data.append(reinterpret_cast(data), size); + send_data.resize((send_data.size() + 7) & ~7); + } + } + + default: + throw logic_error("unimplemented game version in send_command"); + } + + c->send(move(send_data)); +} + +void send_command(shared_ptr l, uint16_t command, uint32_t flag, + const void* data, size_t size) { + rw_guard g(l->lock, false); + for (auto& client : l->clients) { + if (!client) { + continue; + } + send_command(client, command, flag, data, size); + } +} + + + +// specific command sending functions follow. in general, they're written in +// such a way that you don't need to think about anything, even the client's +// version, before calling them. for this reason, some of them are quite +// complex. many are split into several functions, one for each version of PSO, +// named with suffixes _GC, _BB, and the like. in these cases, the function +// without the suffix simply calls the appropriate function for the client's +// version. thus, if you change something in one of the version-specific +// functions, you may have to change it in all of them. + +//////////////////////////////////////////////////////////////////////////////// +// CommandServerInit: this function sends the command that initializes encryption + +// strings needed for various functions +static const char* anti_copyright = "This server is in no way affiliated, sponsored, or supported by SEGA Enterprises or SONICTEAM. The preceding message exists only in order to remain compatible with programs that expect it."; +static const char* dc_port_map_copyright = "DreamCast Port Map. Copyright SEGA Enterprises. 1999"; +static const char* dc_lobby_server_copyright = "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999"; +static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM."; +static const char* patch_server_copyright = "Patch Server. Copyright SonicTeam, LTD. 2001"; + +static void send_server_init_dc_pc_gc(shared_ptr c, const char* copyright_text, + uint8_t command) { + struct { + char copyright[0x40]; + uint32_t server_key; + uint32_t client_key; + char after_message[200]; + } cmd; + + uint32_t server_key = (rand() << 30) | (rand() << 15) | rand(); + uint32_t client_key = (rand() << 30) | (rand() << 15) | rand(); + + memset(&cmd, 0, sizeof(cmd)); + strcpy(cmd.copyright, copyright_text); + cmd.server_key = server_key; + cmd.client_key = client_key; + strcpy(cmd.after_message, anti_copyright); + send_command(c, command, 0x00, cmd); + + rw_guard g(c->lock, true); + c->crypt_out.reset(new PSOPCEncryption(server_key)); + c->crypt_in.reset(new PSOPCEncryption(client_key)); +} + +static void send_server_init_pc(shared_ptr c, bool initial_connection) { + send_server_init_dc_pc_gc(c, + initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright, + 0x17); +} + +static void send_server_init_gc(shared_ptr c, bool initial_connection) { + send_server_init_dc_pc_gc(c, + initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright, + initial_connection ? 0x17 : 0x02); +} + +static void send_server_init_bb(shared_ptr c, bool initial_connection) { + struct { + char copyright[0x60]; + uint8_t server_key[0x30]; + uint8_t client_key[0x30]; + char after_message[200]; + } cmd; + + memset(&cmd, 0, sizeof(cmd)); + strcpy(cmd.copyright, bb_game_server_copyright); + uint8_t server_key[0x30]; + uint8_t client_key[0x30]; + for (size_t x = 0; x < 0x30; x++) { + server_key[x] = rand(); + client_key[x] = rand(); + } + memcpy(cmd.server_key, server_key, 0x30); + memcpy(cmd.client_key, client_key, 0x30); + strcpy(cmd.after_message, anti_copyright); + send_command(c, 0x03, 0x00, cmd); + + rw_guard(c->lock, true); + c->crypt_out.reset(new PSOBBEncryption(server_key)); + c->crypt_in.reset(new PSOBBEncryption(client_key)); +} + +static void send_server_init_patch(shared_ptr c, bool initial_connection) { + struct { + char copyright[0x40]; + uint32_t server_key; + uint32_t client_key; + // BB rejects the command if it's not exactly this size, so we can't add the + // anti-copyright message... lawyers plz be kind kthx + } cmd; + + uint32_t server_key = (rand() << 30) | (rand() << 15) | rand(); + uint32_t client_key = (rand() << 30) | (rand() << 15) | rand(); + + memset(&cmd, 0, sizeof(cmd)); + strcpy(cmd.copyright, patch_server_copyright); + cmd.server_key = server_key; + cmd.client_key = client_key; + send_command(c, 0x02, 0x00, cmd); + + rw_guard g(c->lock, true); + c->crypt_out.reset(new PSOPCEncryption(server_key)); + c->crypt_in.reset(new PSOPCEncryption(client_key)); +} + +void send_server_init(shared_ptr c, bool initial_connection) { + if (c->version == GameVersion::PC) { + send_server_init_pc(c, initial_connection); + } else if (c->version == GameVersion::Patch) { + send_server_init_patch(c, initial_connection); + } else if (c->version == GameVersion::GC) { + send_server_init_gc(c, initial_connection); + } else if (c->version == GameVersion::BB) { + send_server_init_bb(c, initial_connection); + } else { + throw logic_error("unimplemented versioned command"); + } +} + +// for non-BB clients, updates the client's guild card and security data +void send_update_client_config(shared_ptr c) { + struct { + uint32_t player_tag; + uint32_t serial_number; + ClientConfig config; + } cmd = { + 0x00000100, + c->license->serial_number, + c->config.cfg, + }; + send_command(c, 0x04, 0x00, cmd); +} + + + +void send_reconnect(shared_ptr c, uint32_t address, uint16_t port) { + if (!address) { + const sockaddr_in* local_addr = reinterpret_cast(&c->local_addr); + address = local_addr->sin_addr.s_addr; + } + + struct { + uint32_t address; + uint16_t port; + uint16_t unused; + } cmd = {address, port, 0}; + send_command(c, 0x19, 0x00, cmd); +} + +// sends the command (first used by Schthack) that separates PC and GC users +// that connect on the same port +void send_pc_gc_split_reconnect(shared_ptr c, uint32_t address, + uint16_t pc_port) { + if (!address) { + const sockaddr_in* local_addr = reinterpret_cast(&c->local_addr); + address = local_addr->sin_addr.s_addr; + } + + struct { + uint32_t pc_address; + uint16_t pc_port; + uint8_t unused1[0x0F]; + uint8_t gc_command; + uint8_t gc_flag; + uint16_t gc_size; + uint8_t unused2[0xB0 - 0x1D]; + } __attribute__((packed)) cmd; + memset(&cmd, 0, sizeof(cmd)); + cmd.pc_address = address; + cmd.pc_port = pc_port; + cmd.gc_command = 0x19; + cmd.gc_size = 0x97; + send_command(c, 0x19, 0x00, cmd); +} + + + +// sends the command that signals an error or updates the client's guild card +// number and security data +void send_client_init_bb(shared_ptr c, uint32_t error) { + struct { + uint32_t error; // see below + uint32_t player_tag; + uint32_t serial_number; + uint32_t team_id; // just randomize it; teams aren't supported + ClientConfigBB cfg; + uint32_t caps; // should be 0x00000102 + } cmd = { + error, + 0x00000100, + c->license->serial_number, + static_cast((rand() << 30) | (rand() << 15) | rand()), + c->config, + 0x00000102, + }; + send_command(c, 0x00E6, 0x00000000, cmd); +} + +void send_team_and_key_config_bb(shared_ptr c) { + send_command(c, 0x00E2, 0x00000000, c->player.key_config); +} + +// sends a player preview. these are used by the caracter select and character +// creation mechanism +void send_player_preview_bb(shared_ptr c, uint8_t player_index, + const PlayerDispDataBBPreview* preview) { + + if (!preview) { + // no player exists + struct { + uint32_t player_index; + uint32_t error; + } cmd = {player_index, 0x00000002}; + send_command(c, 0x00E4, 0x00000000, cmd); + + } else { + struct { + uint32_t player_index; + PlayerDispDataBBPreview preview; + } cmd = {player_index, *preview}; + send_command(c, 0x00E3, 0x00000000, cmd); + } +} + +// sent in response to the client's 01E8 command +void send_accept_client_checksum_bb(shared_ptr c) { + struct { + uint32_t verify; + uint32_t unused; + } cmd = {1, 0}; + send_command(c, 0x02E8, 0x00000000, cmd); +} + +// sends the "I'm about to send your guild card file" command +void send_guild_card_header_bb(shared_ptr c) { + uint32_t checksum = compute_guild_card_checksum(&c->player.guild_cards, + sizeof(GuildCardFileBB)); + struct { + uint32_t unknown; // should be 1 + uint32_t filesize; // 0x0000490 + uint32_t checksum; + } cmd = {1, 0x490, checksum}; + send_command(c, 0x01DC, 0x00000000, cmd); +} + +// sends a chunk of guild card data +void send_guild_card_chunk_bb(shared_ptr c, size_t chunk_index) { + size_t chunk_offset = chunk_index * 0x6800; + if (chunk_offset >= sizeof(GuildCardFileBB)) { + throw logic_error("attempted to send chunk beyond end of guild card file"); + } + size_t data_size = sizeof(GuildCardFileBB) - chunk_offset; + if (data_size > 0x6800) { + data_size = 0x6800; + } + + string contents(8, '\0'); + *reinterpret_cast(const_cast(contents.data())) = 0; + *reinterpret_cast(const_cast(contents.data() + 4)) = chunk_index; + contents.append(reinterpret_cast(&c->player.guild_cards + chunk_offset), + data_size); + + send_command(c, 0x02DC, 0x00000000, contents); +} + +// sends the game data (battleparamentry files, etc.) +void send_stream_file_bb(shared_ptr c) { + + struct StreamFileEntry { + uint32_t size; + uint32_t checksum; + uint32_t offset; + char filename[0x40]; + }; + + auto index_data = file_cache.get("system/blueburst/streamfile.ind"); + if (index_data->size() % sizeof(StreamFileEntry)) { + throw invalid_argument("stream file index not a multiple of entry size"); + } + + size_t entry_count = index_data->size() / sizeof(StreamFileEntry); + send_command(c, 0x01EB, entry_count, index_data); + + auto* entries = reinterpret_cast(index_data->data()); + + struct { + uint32_t chunk_index; + uint8_t data[0x6800]; + } chunk_cmd; + chunk_cmd.chunk_index = 0; + + uint32_t buffer_offset = 0; + for (size_t x = 0; x < entry_count; x++) { + auto filename = string_printf("system/blueburst/%s", entries[x].filename); + auto file_data = file_cache.get(filename); + + size_t file_data_remaining = file_data->size(); + if (file_data_remaining != entries[x].size) { + throw invalid_argument(filename + " does not match size in stream file index"); + } + while (file_data_remaining) { + size_t read_size = 0x6800 - buffer_offset; + if (read_size > file_data_remaining) { + read_size = file_data_remaining; + } + memcpy(&chunk_cmd.data[buffer_offset], + file_data->data() + file_data->size() - file_data_remaining, read_size); + buffer_offset += read_size; + file_data_remaining -= read_size; + + if (buffer_offset == 0x6800) { + // note: the client sends 0x03EB in response to these, but we'll just + // ignore them because we don't need any of the contents + send_command(c, 0x02EB, 0x00000000, chunk_cmd); + buffer_offset = 0; + chunk_cmd.chunk_index++; + } + } + + if (buffer_offset > 0) { + send_command(c, 0x02EB, 0x00000000, &chunk_cmd, (buffer_offset + 15) & ~3); + } + } +} + +// accepts the player's choice at char select +void send_approve_player_choice_bb(shared_ptr c) { + struct { + uint32_t player_index; + uint32_t unused; + } cmd = {c->config.cfg.bb_player_index, 1}; + send_command(c, 0x00E4, 0x00000000, cmd); +} + +// sends player data to the client (usually sent right before entering lobby) +void send_complete_player_bb(shared_ptr c) { + send_command(c, 0x00E7, 0x00000000, c->player.export_bb_player_data()); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// patch functions + +void send_check_directory_patch(shared_ptr c, const char* dir) { + char data[0x40]; + memset(data, 0, 0x40); + strcpy(data, dir); + send_command(c, 0x09, 0x00, data, 0x40); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// message functions + +struct LargeMessageOptionalHeader { + uint32_t unused; + uint32_t serial_number; +}; + +static void send_large_message_pc_patch_bb(shared_ptr c, uint8_t command, + const char16_t* text, uint32_t from_serial_number, bool include_header) { + u16string data; + if (include_header) { + data.resize(sizeof(LargeMessageOptionalHeader) / sizeof(char16_t)); + *reinterpret_cast(const_cast(data.data())) = + {0, from_serial_number}; + } + data += text; + add_color_inplace(const_cast(data.data()) + + (include_header ? sizeof(LargeMessageOptionalHeader) : 0)); + data.resize((data.size() + 4) & ~3); + send_command(c, command, 0x00, data); +} + +static void send_large_message_dc_gc(shared_ptr c, uint8_t command, + const char16_t* text, uint32_t from_serial_number, bool include_header) { + string data; + if (include_header) { + data.resize(sizeof(LargeMessageOptionalHeader) / sizeof(char)); + *reinterpret_cast(const_cast(data.data())) = + {0, from_serial_number}; + } + data += encode_sjis(text); + add_color_inplace(const_cast(data.data()) + + (include_header ? sizeof(LargeMessageOptionalHeader) : 0)); + data.resize((data.size() + 4) & ~3); + send_command(c, command, 0x00, data); +} + +static void send_large_message(shared_ptr c, uint8_t command, + const char16_t* text, uint32_t from_serial_number, bool include_header) { + if (c->version == GameVersion::PC || c->version == GameVersion::Patch || + c->version == GameVersion::BB) { + send_large_message_pc_patch_bb(c, command, text, from_serial_number, include_header); + } else { + send_large_message_dc_gc(c, command, text, from_serial_number, include_header); + } +} + +void send_message_box(shared_ptr c, const char16_t* text) { + return send_large_message(c, (c->version == GameVersion::Patch) ? 0x13 : 0x1A, + text, 0, false); +} + +void send_lobby_name(shared_ptr c, const char16_t* text) { + return send_large_message(c, 0x8A, text, 0, false); +} + +void send_quest_info(shared_ptr c, const char16_t* text) { + return send_large_message(c, 0xA3, text, 0, false); +} + +void send_lobby_message_box(shared_ptr c, const char16_t* text) { + return send_large_message(c, 0x01, text, 0, true); +} + +void send_ship_info(shared_ptr c, const char16_t* text) { + return send_large_message(c, 0x11, text, 0, true); +} + +void send_text_message(shared_ptr c, const char16_t* text) { + return send_large_message(c, 0xB0, text, 0, true); +} + +void send_text_message(shared_ptr l, const char16_t* text) { + rw_guard g(l->lock, false); + for (size_t x = 0; x < l->max_clients; x++) { + if (l->clients[x]) { + send_text_message(l->clients[x], text); + } + } +} + +void send_chat_message(shared_ptr c, uint32_t from_serial_number, + const char16_t* from_name, const char16_t* text) { + u16string data; + if (c->version == GameVersion::BB) { + data.append(u"\x09J"); + } + data.append(from_name); + data.append(u"\x09J"); + data.append(text); + send_large_message(c, 0x06, data.c_str(), from_serial_number, true); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// info board + +static void send_info_board_pc_bb(shared_ptr c, shared_ptr l) { + struct Entry { + char16_t name[0x10]; + char16_t message[0xAC]; + }; + vector entries; + + { + rw_guard g(l->lock, false); + for (const auto& c : l->clients) { + if (!c.get()) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + memset(&e, 0, sizeof(Entry)); + char16cpy(e.name, c->player.disp.name, 0x10); + char16cpy(e.message, c->player.info_board, 0xAC); + add_color_inplace(e.message); + } + } + + send_command(c, 0xD8, 0x00, entries); +} + +static void send_info_board_dc_gc(shared_ptr c, shared_ptr l) { + struct Entry { + char name[0x10]; + char message[0xAC]; + }; + vector entries; + + { + rw_guard g(l->lock, false); + for (const auto& c : l->clients) { + if (!c.get()) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + memset(&e, 0, sizeof(Entry)); + encode_sjis(e.name, c->player.disp.name, 0x10); + encode_sjis(e.message, c->player.info_board, 0xAC); + add_color_inplace(e.message); + } + } + + send_command(c, 0xD8, 0x00, entries); +} + +void send_info_board(shared_ptr c, shared_ptr l) { + if (c->version == GameVersion::PC || c->version == GameVersion::Patch || + c->version == GameVersion::BB) { + send_info_board_pc_bb(c, l); + } else { + send_info_board_dc_gc(c, l); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +// CommandCardSearchResult: sends a guild card search result to a player. + +static void send_card_search_result_dc_pc_gc(shared_ptr s, + shared_ptr c, shared_ptr result, + shared_ptr result_lobby) { + struct { + uint32_t player_tag; + uint32_t searcher_serial_number; + uint32_t result_serial_number; + struct { + union { + struct { + uint8_t dcgc_command; + uint8_t dcgc_flag; + uint16_t dcgc_size; + }; + struct { + uint16_t pc_size; + uint8_t pc_command; + uint8_t pc_flag; + }; + }; + uint32_t address; + uint16_t port; + uint16_t unused; + } destination_command; + char location_string[0x44]; + uint32_t menu_id; + uint32_t lobby_id; + char unused[0x3C]; + char16_t name[0x20]; + } cmd; + + memset(&cmd, 0, sizeof(cmd)); + cmd.player_tag = 0x00000100; + cmd.searcher_serial_number = c->license->serial_number; + cmd.result_serial_number = result->license->serial_number; + if (c->version == GameVersion::PC) { + cmd.destination_command.pc_size = 0x000C; + cmd.destination_command.pc_command = 0x19; + cmd.destination_command.pc_flag = 0x00; + } else { + cmd.destination_command.dcgc_command = 0x19; + cmd.destination_command.dcgc_flag = 0x00; + cmd.destination_command.dcgc_size = 0x000C; + } + const sockaddr_in* local_addr = reinterpret_cast(&result->local_addr); + cmd.destination_command.address = local_addr->sin_addr.s_addr; + cmd.destination_command.port = ntohs(local_addr->sin_port); + cmd.destination_command.unused = 0; + + if (result_lobby->is_game()) { + string encoded_lobby_name = encode_sjis(result_lobby->name); + snprintf(cmd.location_string, sizeof(cmd.location_string), + "%s, Block 00, ,%s", encoded_lobby_name.c_str(), s->name.c_str()); + } else { + snprintf(cmd.location_string, sizeof(cmd.location_string), "Block 00, ,%s", + s->name.c_str()); + } + cmd.menu_id = LOBBY_MENU_ID; + cmd.lobby_id = result->lobby_id; + memset(cmd.unused, 0, sizeof(cmd.unused)); + char16cpy(cmd.name, result->player.disp.name, 0x20); + + send_command(c, 0x40, 0x00, cmd); +} + +static void send_card_search_result_bb(shared_ptr s, + shared_ptr c, shared_ptr result, + shared_ptr result_lobby) { + // this is identical to the dc/pc/gc function above, except the reconnect + // command format is different. why did you do this, sega? are you so lazy + // that really the best thing you could do is call handle_command() on a + // substring of another command? lrn2code plz + struct { + uint32_t player_tag; + uint32_t searcher_serial_number; + uint32_t result_serial_number; + struct { + uint16_t size; + uint16_t command; + uint32_t flag; + uint32_t address; + uint16_t port; + uint16_t unused; + } destination_command; + char location_string[0x44]; + uint32_t menu_id; + uint32_t lobby_id; + char unused[0x3C]; + char16_t name[0x20]; + } cmd; + + memset(&cmd, 0, sizeof(cmd)); + cmd.player_tag = 0x00000100; + cmd.searcher_serial_number = c->license->serial_number; + cmd.result_serial_number = result->license->serial_number; + cmd.destination_command.size = 0x0010; + cmd.destination_command.command = 0x19; + cmd.destination_command.flag = 0x00000000; + const sockaddr_in* local_addr = reinterpret_cast(&result->local_addr); + cmd.destination_command.address = local_addr->sin_addr.s_addr; + cmd.destination_command.port = ntohs(local_addr->sin_port); + cmd.destination_command.unused = 0; + + if (result_lobby->is_game()) { + string encoded_lobby_name = encode_sjis(result_lobby->name); + snprintf(cmd.location_string, sizeof(cmd.location_string), + "%s, Block 00, ,%s", encoded_lobby_name.c_str(), s->name.c_str()); + } else { + snprintf(cmd.location_string, sizeof(cmd.location_string), "Block 00, ,%s", + s->name.c_str()); + } + cmd.menu_id = LOBBY_MENU_ID; + cmd.lobby_id = result->lobby_id; + memset(cmd.unused, 0, sizeof(cmd.unused)); + char16cpy(cmd.name, result->player.disp.name, 0x20); + + send_command(c, 0x40, 0x00, cmd); +} + +void send_card_search_result(shared_ptr s, shared_ptr c, + shared_ptr result, shared_ptr result_lobby) { + if (c->version == GameVersion::BB) { + send_card_search_result_bb(s, c, result, result_lobby); + } else { + send_card_search_result_dc_pc_gc(s, c, result, result_lobby); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CommandSendGuildCard: generates a guild card for the source player and sends it to the destination player + +static void send_guild_card_gc(shared_ptr c, shared_ptr source) { + struct { + uint8_t subcommand; + uint8_t subsize; + uint16_t unused; + uint32_t player_tag; + uint32_t serial_number; + char name[0x18]; + char desc[0x6C]; + uint8_t reserved1; + uint8_t reserved2; + uint8_t section_id; + uint8_t char_class; + } cmd; + + cmd.subcommand = 0x06; + cmd.subsize = 0x25; + cmd.unused = 0x0000; + cmd.player_tag = 0x00000100; + cmd.reserved1 = 1; + cmd.reserved2 = 1; + + { + rw_guard g(source->lock, false); + cmd.serial_number = source->license->serial_number; + encode_sjis(cmd.name, source->player.disp.name, 0x18); + remove_language_marker_inplace(cmd.name); + encode_sjis(cmd.desc, source->player.guild_card_desc, 0x6C); + cmd.section_id = source->player.disp.section_id; + cmd.char_class = source->player.disp.char_class; + } + + send_command(c, 0x62, c->lobby_client_id, cmd); +} + +static void send_guild_card_bb(shared_ptr c, shared_ptr source) { + struct { + uint8_t subcommand; + uint8_t subsize; + uint16_t unused; + uint32_t serial_number; + char16_t name[0x18]; + char16_t team_name[0x10]; + char16_t desc[0x58]; + uint8_t reserved1; + uint8_t reserved2; + uint8_t section_id; + uint8_t char_class; + } cmd; + + cmd.subcommand = 0x06; + cmd.subsize = 0x43; + cmd.unused = 0x0000; + cmd.reserved1 = 1; + cmd.reserved2 = 1; + + { + rw_guard g(source->lock, false); + cmd.serial_number = source->license->serial_number; + char16cpy(cmd.name, source->player.disp.name, 0x18); + remove_language_marker_inplace(cmd.name); + char16cpy(cmd.team_name, source->player.team_name, 0x10); + remove_language_marker_inplace(cmd.team_name); + char16cpy(cmd.desc, source->player.guild_card_desc, 0x58); + cmd.section_id = source->player.disp.section_id; + cmd.char_class = source->player.disp.char_class; + } + + send_command(c, 0x62, c->lobby_client_id, cmd); +} + +void send_guild_card(shared_ptr c, shared_ptr source) { + if (c->version == GameVersion::GC) { + send_guild_card_gc(c, source); + } else if (c->version == GameVersion::BB) { + send_guild_card_bb(c, source); + } else { + throw logic_error("unimplemented versioned command"); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +// menus + +static void send_menu_pc_bb(shared_ptr c, const char16_t* menu_name, + uint32_t menu_id, const vector& items, bool is_info_menu) { + struct Entry { + uint32_t menu_id; + uint32_t item_id; + uint16_t flags; // should be 0x0F04 + char16_t text[17]; + }; + + vector entries; + entries.emplace_back(); + { + auto& entry = entries.back(); + entry.menu_id = menu_id; + entry.item_id = 0xFFFFFFFF; + entry.flags = 0x0004; + char16cpy(entry.text, menu_name, 17); + } + + for (const auto& item : items) { + if ((c->version == GameVersion::BB) && (item.flags & MenuItemFlag::InvisibleOnBB)) { + continue; + } + if ((c->version == GameVersion::PC) && (item.flags & MenuItemFlag::InvisibleOnPC)) { + continue; + } + if ((item.flags & MenuItemFlag::RequiresMessageBoxes) && + (c->flags & ClientFlag::NoMessageBoxCloseConfirmation)) { + continue; + } + + entries.emplace_back(); + auto& entry = entries.back(); + entry.menu_id = menu_id; + entry.item_id = item.item_id; + entry.flags = (c->version == GameVersion::BB) ? 0x0004 : 0x0F04; + char16cpy(entry.text, item.name.c_str(), 17); + } + + send_command(c, is_info_menu ? 0x1F : 0x07, entries.size() - 1, entries); +} + +static void send_menu_dc_gc(shared_ptr c, const char16_t* menu_name, + uint32_t menu_id, const vector& items, bool is_info_menu) { + struct Entry { + uint32_t menu_id; + uint32_t item_id; + uint16_t flags; // should be 0x0F04 + char text[18]; + }; + + vector entries; + entries.emplace_back(); + { + auto& entry = entries.back(); + entry.menu_id = menu_id; + entry.item_id = 0xFFFFFFFF; + entry.flags = 0x0004; + encode_sjis(entry.text, menu_name, 18); + } + + for (const auto& item : items) { + if ((c->version == GameVersion::DC) && (item.flags & MenuItemFlag::InvisibleOnDC)) { + continue; + } + if ((c->version == GameVersion::GC) && (item.flags & MenuItemFlag::InvisibleOnGC)) { + continue; + } + if ((c->flags & ClientFlag::Episode3Games) && (item.flags & MenuItemFlag::InvisibleOnGCEpisode3)) { + continue; + } + if ((item.flags & MenuItemFlag::RequiresMessageBoxes) && + (c->flags & ClientFlag::NoMessageBoxCloseConfirmation)) { + continue; + } + + entries.emplace_back(); + auto& entry = entries.back(); + entry.menu_id = menu_id; + entry.item_id = item.item_id; + entry.flags = 0x0F04; + encode_sjis(entry.text, item.name.c_str(), 18); + } + + send_command(c, is_info_menu ? 0x1F : 0x07, entries.size() - 1, entries); +} + +void send_menu(shared_ptr c, const char16_t* menu_name, + uint32_t menu_id, const vector& items, bool is_info_menu) { + if (c->version == GameVersion::PC || c->version == GameVersion::Patch || + c->version == GameVersion::BB) { + send_menu_pc_bb(c, menu_name, menu_id, items, is_info_menu); + } else { + send_menu_dc_gc(c, menu_name, menu_id, items, is_info_menu); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CommandGameSelect: presents the player with a Game Select menu. returns the selection in the same way as CommandShipSelect. + +static void send_game_menu_pc(shared_ptr c, shared_ptr s) { + struct Entry { + uint32_t menu_id; + uint32_t game_id; + uint8_t difficulty_tag; // (s->teams[x]->episode == 0xFF ? 0x0A : s->teams[x]->difficulty + 0x22); + uint8_t num_players; + char16_t name[0x10]; + uint8_t episode; + uint8_t flags; + }; + + vector entries; + { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = GAME_MENU_ID; + e.game_id = 0; + decode_sjis(e.name, s->name.c_str(), 0x10); + } + for (shared_ptr l : s->all_lobbies()) { + if (!l->is_game()) { + continue; + } + if (l->version != c->version) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = GAME_MENU_ID; + + rw_guard g(l->lock, false); + e.game_id = l->lobby_id; + e.difficulty_tag = l->difficulty + 0x22; + e.num_players = l->count_clients(); + e.episode = 0; + e.flags = (l->mode << 4) | (l->password[0] ? 2 : 0); + char16cpy(e.name, l->name, 0x10); + } + + send_command(c, 0x08, entries.size() - 1, entries); +} + +static void send_game_menu_gc(shared_ptr c, shared_ptr s) { + struct Entry { + uint32_t menu_id; + uint32_t game_id; + uint8_t difficulty_tag; // (s->teams[x]->episode == 0xFF ? 0x0A : s->teams[x]->difficulty + 0x22); + uint8_t num_players; + char name[0x10]; + uint8_t episode; + uint8_t flags; + }; + + vector entries; + { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = GAME_MENU_ID; + e.game_id = 0; + strncpy(e.name, s->name.c_str(), 0x10); + e.flags = 0x0004; + } + for (shared_ptr l : s->all_lobbies()) { + if (!l->is_game()) { + continue; + } + if (l->version != c->version) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = GAME_MENU_ID; + + rw_guard g(l->lock, false); + e.game_id = l->lobby_id; + e.difficulty_tag = ((l->flags & LobbyFlag::Episode3) ? 0x0A : (l->difficulty + 0x22)); + e.num_players = l->count_clients(); + e.episode = 0; + if (l->flags & LobbyFlag::Episode3) { + e.flags = (l->password[0] ? 2 : 0); + } else { + e.flags = ((l->episode << 6) | (l->mode << 4) | (l->password[0] ? 2 : 0)); + } + encode_sjis(e.name, l->name, 0x10); + } + + send_command(c, 0x08, entries.size() - 1, entries); +} + +static void send_game_menu_bb(shared_ptr c, shared_ptr s) { + struct Entry { + uint32_t menu_id; + uint32_t game_id; + uint8_t difficulty_tag; // (s->teams[x]->episode == 0xFF ? 0x0A : s->teams[x]->difficulty + 0x22); + uint8_t num_players; + char16_t name[0x10]; + uint8_t episode; + uint8_t flags; + }; + + vector entries; + { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = GAME_MENU_ID; + e.game_id = 0; + e.flags = 0x0004; + decode_sjis(e.name, s->name.c_str(), 0x10); + } + for (shared_ptr l : s->all_lobbies()) { + if (!l->is_game()) { + continue; + } + if (l->version != c->version) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = GAME_MENU_ID; + + rw_guard g(l->lock, false); + e.game_id = l->lobby_id; + e.difficulty_tag = l->difficulty + 0x22; + e.num_players = l->count_clients(); + e.episode = (l->max_clients << 4) | l->episode; + e.flags = ((l->mode % 3) << 4) | (l->password[0] ? 2 : 0) | ((l->mode == 3) ? 4 : 0); + char16cpy(e.name, l->name, 0x10); + } + + send_command(c, 0x08, entries.size() - 1, entries); +} + +void send_game_menu(shared_ptr c, shared_ptr s) { + if (c->version == GameVersion::PC) { + send_game_menu_pc(c, s); + } else if (c->version == GameVersion::GC) { + send_game_menu_gc(c, s); + } else if (c->version == GameVersion::BB) { + send_game_menu_bb(c, s); + } else { + throw logic_error("unimplemented versioned command"); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +// CommandQuestSelect: presents the user with a quest select menu based on a quest list. + +static void send_quest_menu_pc(shared_ptr c, uint32_t menu_id, + const vector>& quests, bool is_download_menu) { + struct Entry { + uint32_t menu_id; + uint32_t quest_id; + char16_t name[0x20]; + char16_t short_desc[0x70]; + }; + + vector entries; + for (const auto& quest : quests) { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = menu_id; + e.quest_id = quest->quest_id; + char16cpy(e.name, quest->name.c_str(), 0x20); + char16cpy(e.short_desc, quest->short_description.c_str(), 0x70); + } + + send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); +} + +static void send_quest_menu_pc(std::shared_ptr c, uint32_t menu_id, + const std::vector& items, bool is_download_menu) { + struct Entry { + uint32_t menu_id; + uint32_t item_id; + char16_t name[0x20]; + char16_t short_desc[0x70]; + }; + + vector entries; + for (const auto& item : items) { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = menu_id; + e.item_id = item.item_id; + char16cpy(e.name, item.name.c_str(), 0x20); + char16cpy(e.short_desc, item.description.c_str(), 0x70); + } + + send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); +} + +static void send_quest_menu_gc(shared_ptr c, uint32_t menu_id, + const vector>& quests, bool is_download_menu) { + struct Entry { + uint32_t menu_id; + uint32_t quest_id; + char name[0x20]; + char short_desc[0x70]; + }; + + vector entries; + for (const auto& quest : quests) { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = menu_id; + e.quest_id = quest->quest_id; + encode_sjis(e.name, quest->name.c_str(), 0x20); + encode_sjis(e.short_desc, quest->short_description.c_str(), 0x70); + } + + send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); +} + +static void send_quest_menu_gc(shared_ptr c, uint32_t menu_id, + const std::vector& items, bool is_download_menu) { + struct Entry { + uint32_t menu_id; + uint32_t item_id; + char name[0x20]; + char short_desc[0x70]; + }; + + vector entries; + for (const auto& item : items) { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = menu_id; + e.item_id = item.item_id; + encode_sjis(e.name, item.name.c_str(), 0x20); + encode_sjis(e.short_desc, item.description.c_str(), 0x70); + } + + send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); +} + +static void send_quest_menu_bb(shared_ptr c, uint32_t menu_id, + const vector>& quests, bool is_download_menu) { + // yet again sega does something inexplicable: the description is 10 chars + // longer than on pc, necessitating a separate function here for BB + struct Entry { + uint32_t menu_id; + uint32_t quest_id; + char16_t name[0x20]; + char16_t short_desc[0x7A]; + }; + + vector entries; + for (const auto& quest : quests) { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = menu_id; + e.quest_id = quest->quest_id; + char16cpy(e.name, quest->name.c_str(), 0x20); + char16cpy(e.short_desc, quest->short_description.c_str(), 0x7A); + } + + send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); +} + +static void send_quest_menu_bb(shared_ptr c, uint32_t menu_id, + const std::vector& items, bool is_download_menu) { + // yet again sega does something inexplicable: the description is 10 chars + // longer than on pc, necessitating a separate function here for BB + struct Entry { + uint32_t menu_id; + uint32_t item_id; + char16_t name[0x20]; + char16_t short_desc[0x7A]; + }; + + vector entries; + for (const auto& item : items) { + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = menu_id; + e.item_id = item.item_id; + char16cpy(e.name, item.name.c_str(), 0x20); + char16cpy(e.short_desc, item.description.c_str(), 0x7A); + } + + send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); +} + +void send_quest_menu(shared_ptr c, uint32_t menu_id, + const vector>& quests, bool is_download_menu) { + if (c->version == GameVersion::PC) { + send_quest_menu_pc(c, menu_id, quests, is_download_menu); + } else if (c->version == GameVersion::GC) { + send_quest_menu_gc(c, menu_id, quests, is_download_menu); + } else if (c->version == GameVersion::BB) { + send_quest_menu_bb(c, menu_id, quests, is_download_menu); + } else { + throw logic_error("unimplemented versioned command"); + } +} + +void send_quest_menu(shared_ptr c, uint32_t menu_id, + const std::vector& items, bool is_download_menu) { + if (c->version == GameVersion::PC) { + send_quest_menu_pc(c, menu_id, items, is_download_menu); + } else if (c->version == GameVersion::GC) { + send_quest_menu_gc(c, menu_id, items, is_download_menu); + } else if (c->version == GameVersion::BB) { + send_quest_menu_bb(c, menu_id, items, is_download_menu); + } else { + throw logic_error("unimplemented versioned command"); + } +} + +void send_lobby_list(shared_ptr c, shared_ptr s) { + // this command appeast to be deprecated, as PSO expects it to be exactly how + // this server sends it, and does not react if it's different, except by + // changing the lobby IDs + + struct Entry { + uint32_t menu_id; + uint32_t item_id; + uint32_t unused; // should be 0x00000000 + }; + + vector entries; + for (shared_ptr l : s->all_lobbies()) { + if (!(l->flags & LobbyFlag::Default)) { + continue; + } + if (!(c->flags & ClientFlag::Episode3Games) != !(l->flags & LobbyFlag::Episode3)) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + e.menu_id = LOBBY_MENU_ID; + e.item_id = l->lobby_id; + e.unused = 0; + } + + send_command(c, 0x83, entries.size(), entries); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// lobby joining + +static void send_join_game_pc(shared_ptr c, shared_ptr l) { + struct { + uint32_t variations[0x20]; + PlayerLobbyDataPC lobby_data[4]; + uint8_t client_id; + uint8_t leader_id; + uint8_t unused; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t event; + uint8_t section_id; + uint8_t challenge_mode; + uint32_t game_id; // actually random number for rare monster selection; whatever + uint8_t episode; + uint8_t unused2; + uint8_t solo_mode; + uint8_t unused3; + } cmd; + + size_t player_count = 0; + { + rw_guard g(l->lock, false); + memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); + for (size_t x = 0; x < 4; x++) { + if (!l->clients[x]) { + memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataPC)); + } else { + rw_guard g(l->clients[x]->lock, false); + cmd.lobby_data[x].player_tag = 0x00000100; + cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd.lobby_data[x].ip_address = 0xFFFFFFFF; + cmd.lobby_data[x].client_id = c->lobby_client_id; + char16cpy(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); + player_count++; + } + } + + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.unused = 0x00; + cmd.difficulty = l->difficulty; + cmd.battle_mode = (l->mode == 1) ? 1 : 0; + cmd.event = l->event; + cmd.section_id = l->section_id; + cmd.challenge_mode = (l->mode == 2) ? 1 : 0; + cmd.game_id = l->lobby_id; + cmd.episode = 0x00; + cmd.unused2 = 0x01; + cmd.solo_mode = 0x00; + cmd.unused3 = 0x00; + } + + send_command(c, 0x64, player_count, cmd); +} + +static void send_join_game_gc(shared_ptr c, shared_ptr l) { + struct { + uint32_t variations[0x20]; + PlayerLobbyDataGC lobby_data[4]; + uint8_t client_id; + uint8_t leader_id; + uint8_t unused; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t event; + uint8_t section_id; + uint8_t challenge_mode; + uint32_t game_id; // actually random number for rare monster selection; whatever + uint32_t episode; // for PSOPC, this must be 0x00000100 + struct { + PlayerInventory inventory; + PlayerDispDataPCGC disp; + } player[4]; // only used on ep3 + } cmd; + + size_t player_count = 0; + { + rw_guard g(l->lock, false); + memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); + for (size_t x = 0; x < 4; x++) { + if (!l->clients[x]) { + memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataGC)); + } else { + rw_guard g(l->clients[x]->lock, false); + cmd.lobby_data[x].player_tag = 0x00000100; + cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd.lobby_data[x].ip_address = 0xFFFFFFFF; + cmd.lobby_data[x].client_id = c->lobby_client_id; + encode_sjis(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); + if (l->flags & LobbyFlag::Episode3) { + cmd.player[x].inventory = l->clients[x]->player.inventory; + cmd.player[x].disp = l->clients[x]->player.disp.to_pcgc(); + } + player_count++; + } + } + + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.unused = 0x00; + cmd.difficulty = l->difficulty; + cmd.battle_mode = (l->mode == 1) ? 1 : 0; + cmd.event = l->event; + cmd.section_id = l->section_id; + cmd.challenge_mode = (l->mode == 2) ? 1 : 0; + cmd.game_id = l->lobby_id; + cmd.episode = l->episode; + } + + // player is only sent in ep3 games + size_t data_size = (l->flags & LobbyFlag::Episode3) ? 0x1184 : 0x0114; + send_command(c, 0x64, player_count, &cmd, data_size); +} + +static void send_join_game_bb(shared_ptr c, shared_ptr l) { + struct { + uint32_t variations[0x20]; + PlayerLobbyDataBB lobby_data[4]; + uint8_t client_id; + uint8_t leader_id; + uint8_t unused; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t event; + uint8_t section_id; + uint8_t challenge_mode; + uint32_t game_id; // actually random number for rare monster selection; whatever + uint8_t episode; + uint8_t unused2; + uint8_t solo_mode; + uint8_t unused3; + } cmd; + + size_t player_count = 0; + { + rw_guard g(l->lock, false); + memcpy(cmd.variations, l->variations, sizeof(cmd.variations)); + for (size_t x = 0; x < 4; x++) { + memset(&cmd.lobby_data[x], 0, sizeof(PlayerLobbyDataBB)); + if (l->clients[x]) { + rw_guard g(l->clients[x]->lock, false); + cmd.lobby_data[x].player_tag = 0x00000100; + cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd.lobby_data[x].client_id = c->lobby_client_id; + char16cpy(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); + player_count++; + } + } + + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.unused = 0x00; + cmd.difficulty = l->difficulty; + cmd.battle_mode = (l->mode == 1) ? 1 : 0; + cmd.event = l->event; + cmd.section_id = l->section_id; + cmd.challenge_mode = (l->mode == 2) ? 1 : 0; + cmd.game_id = l->lobby_id; + cmd.episode = 0x00; + cmd.unused2 = 0x01; + cmd.solo_mode = 0x00; + cmd.unused3 = 0x00; + } + + send_command(c, 0x64, player_count, cmd); +} + +static void send_join_lobby_pc(shared_ptr c, shared_ptr l) { + rw_guard g(l->lock, false); + + uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; + struct { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + uint16_t block_number; + uint16_t event; + uint32_t unused; + } cmd = { + c->lobby_client_id, + l->leader_id, + 0x01, + lobby_type, + l->block, + l->event, + 0x00000000, + }; + + struct Entry { + PlayerLobbyDataPC lobby_data; + PlayerLobbyJoinDataPCGC data; + }; + vector entries; + + for (size_t x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + + rw_guard g(l->clients[x]->lock, false); + e.lobby_data.player_tag = 0x00000100; + e.lobby_data.guild_card = l->clients[x]->license->serial_number; + e.lobby_data.ip_address = 0xFFFFFFFF; + e.lobby_data.client_id = l->clients[x]->lobby_client_id; + char16cpy(e.lobby_data.name, l->clients[x]->player.disp.name, 0x10); + e.data = l->clients[x]->player.export_lobby_data_pc(); + } + + send_command(c, 0x67, entries.size(), cmd, entries); +} + +static void send_join_lobby_gc(shared_ptr c, shared_ptr l) { + rw_guard g(l->lock, false); + + uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; + struct { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + uint16_t block_number; + uint16_t event; + uint32_t unused; + } cmd = { + c->lobby_client_id, + l->leader_id, + 0x01, + lobby_type, + l->block, + l->event, + 0x00000000, + }; + + struct Entry { + PlayerLobbyDataGC lobby_data; + PlayerLobbyJoinDataPCGC data; + }; + vector entries; + + for (size_t x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + + rw_guard g(l->clients[x]->lock, false); + e.lobby_data.player_tag = 0x00000100; + e.lobby_data.guild_card = l->clients[x]->license->serial_number; + e.lobby_data.ip_address = 0xFFFFFFFF; + e.lobby_data.client_id = l->clients[x]->lobby_client_id; + encode_sjis(e.lobby_data.name, l->clients[x]->player.disp.name, 0x10); + e.data = l->clients[x]->player.export_lobby_data_gc(); + } + + send_command(c, 0x67, entries.size(), cmd, entries); +} + +static void send_join_lobby_bb(shared_ptr c, shared_ptr l) { + rw_guard g(l->lock, false); + + uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; + struct { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + uint16_t block_number; + uint16_t event; + uint32_t unused; + } cmd = { + c->lobby_client_id, + l->leader_id, + 0x01, + lobby_type, + l->block, + l->event, + 0x00000000, + }; + + struct Entry { + PlayerLobbyDataBB lobby_data; + PlayerLobbyJoinDataBB data; + }; + vector entries; + + for (size_t x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + memset(&e.lobby_data, 0, sizeof(e.lobby_data)); + + rw_guard g(l->clients[x]->lock, false); + e.lobby_data.player_tag = 0x00000100; + e.lobby_data.guild_card = l->clients[x]->license->serial_number; + e.lobby_data.client_id = l->clients[x]->lobby_client_id; + char16cpy(e.lobby_data.name, l->clients[x]->player.disp.name, 0x10); + e.data = l->clients[x]->player.export_lobby_data_bb(); + } + + send_command(c, 0x67, entries.size(), cmd, entries); +} + +void send_join_lobby(shared_ptr c, shared_ptr l) { + if (l->is_game()) { + if (c->version == GameVersion::PC) { + send_join_game_pc(c, l); + } else if (c->version == GameVersion::GC) { + send_join_game_gc(c, l); + } else if (c->version == GameVersion::BB) { + send_join_game_bb(c, l); + } else { + throw logic_error("unimplemented versioned command"); + } + + } else { + if (c->version == GameVersion::PC) { + send_join_lobby_pc(c, l); + } else if (c->version == GameVersion::GC) { + send_join_lobby_gc(c, l); + } else if (c->version == GameVersion::BB) { + send_join_lobby_bb(c, l); + } else { + throw logic_error("unimplemented versioned command"); + } + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +// CommandLobbyAddPlayer: notifies all players in a lobby that a new player is joining. +// this command, unlike the previous, is virtually the same between games and lobbies. + +static void send_player_join_notification_pc(shared_ptr c, + shared_ptr l, shared_ptr joining_client) { + uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; + struct { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + uint16_t block_number; + uint16_t event; + uint32_t unused; + PlayerLobbyDataPC lobby_data; + PlayerLobbyJoinDataPCGC data; + } cmd = { + 0xFF, + l->leader_id, + 0x01, + lobby_type, + l->block, + l->event, + 0x00000000, + {0x00000100, joining_client->license->serial_number, 0xFFFFFFFF, joining_client->lobby_client_id, {0}}, + joining_client->player.export_lobby_data_pc(), + }; + char16cpy(cmd.lobby_data.name, joining_client->player.disp.name, 0x10); + + send_command(c, l->is_game() ? 0x65 : 0x68, 0x01, cmd); +} + +static void send_player_join_notification_gc(shared_ptr c, + shared_ptr l, shared_ptr joining_client) { + uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; + struct { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + uint16_t block_number; + uint16_t event; + uint32_t unused; + PlayerLobbyDataGC lobby_data; + PlayerLobbyJoinDataPCGC data; + } cmd = { + 0xFF, + l->leader_id, + 0x01, + lobby_type, + l->block, + l->event, + 0x00000000, + {0x00000100, joining_client->license->serial_number, 0xFFFFFFFF, joining_client->lobby_client_id, {0}}, + joining_client->player.export_lobby_data_gc(), + }; + encode_sjis(cmd.lobby_data.name, joining_client->player.disp.name, 0x10); + + send_command(c, l->is_game() ? 0x65 : 0x68, 0x01, cmd); +} + +static void send_player_join_notification_bb(shared_ptr c, + shared_ptr l, shared_ptr joining_client) { + uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; + struct { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + uint16_t block_number; + uint16_t event; + uint32_t unused; + PlayerLobbyDataBB lobby_data; + PlayerLobbyJoinDataBB data; + } cmd = { + 0xFF, + l->leader_id, + 0x01, + lobby_type, + l->block, + l->event, + 0x00000000, + {}, + joining_client->player.export_lobby_data_bb(), + }; + memset(&cmd.lobby_data, 0, sizeof(cmd.lobby_data)); + cmd.lobby_data.player_tag = 0x00000100; + cmd.lobby_data.guild_card = joining_client->license->serial_number; + cmd.lobby_data.client_id = joining_client->lobby_client_id; + char16cpy(cmd.lobby_data.name, joining_client->player.disp.name, 0x10); + + send_command(c, l->is_game() ? 0x65 : 0x68, 0x01, cmd); +} + +void send_player_join_notification(shared_ptr c, shared_ptr l, + shared_ptr joining_client) { + if (c->version == GameVersion::PC) { + send_player_join_notification_pc(c, l, joining_client); + } else if (c->version == GameVersion::GC) { + send_player_join_notification_gc(c, l, joining_client); + } else if (c->version == GameVersion::BB) { + send_player_join_notification_bb(c, l, joining_client); + } else { + throw logic_error("unimplemented versioned command"); + } +} + +void send_player_leave_notification(shared_ptr l, uint8_t leaving_client_id) { + struct { + uint8_t client_id; + uint8_t leader_id; + uint16_t unused; + } cmd = {leaving_client_id, l->leader_id, 0}; + send_command(l, l->is_game() ? 0x66 : 0x69, leaving_client_id, cmd); +} + +void send_get_player_info(shared_ptr c) { + send_command(c, 0x95); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// arrows + +void send_arrow_update(shared_ptr l) { + struct Entry { + uint32_t player_tag; + uint32_t serial_number; + uint32_t arrow_color; + }; + vector entries; + + rw_guard g(l->lock, false); + for (size_t x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + + entries.emplace_back(); + auto& e = entries.back(); + e.player_tag = 0x00000100; + e.serial_number = l->clients[x]->license->serial_number; + e.arrow_color = l->clients[x]->lobby_arrow_color; + } + + send_command(l, 0x88, entries.size(), entries); +} + +// tells the player that the joining player is done joining, and the game can resume +void send_resume_game(shared_ptr l) { + uint32_t data = 0x081C0372; + send_command(l, 0x60, 0x00, &data, 4); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// Game/cheat commands + +// sends an HP/TP/Meseta modifying command (see flag definitions in command-functions.h) +void send_player_stats_change(shared_ptr l, shared_ptr c, + PlayerStatsChange stat, uint32_t amount) { + + if (amount > 2550) { + throw invalid_argument("amount cannot be larger than 2550"); + } + + vector subs; + while (amount > 0) { + { + subs.emplace_back(); + auto& sub = subs.back(); + sub.byte[0] = 0x9A; + sub.byte[1] = 0x02; + sub.byte[2] = c->lobby_client_id; + sub.byte[3] = 0x00; + } + { + subs.emplace_back(); + auto& sub = subs.back(); + sub.byte[0] = 0x00; + sub.byte[1] = 0x00; + sub.byte[2] = stat; + sub.byte[3] = (amount > 0xFF) ? 0xFF : amount; + amount -= sub.byte[3]; + } + } + + send_command(l, 0x60, 0x00, subs); +} + +// sends a player to the given area. +void send_warp(shared_ptr c, uint32_t area) { + PSOSubcommand cmds[2]; + cmds[0].byte[0] = 0x94; + cmds[0].byte[1] = 0x02; + cmds[0].byte[2] = c->lobby_client_id; + cmds[0].byte[3] = 0x00; + cmds[1].dword = area; + send_command(c, 0x62, c->lobby_client_id, cmds, 8); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// BB game commands + +// notifies other players of a dropped item from a box or enemy +void send_drop_item(shared_ptr l, const ItemData& item, + bool from_enemy, uint8_t area, float x, float y, uint16_t request_id) { + struct { + uint8_t subcommand; + uint8_t subsize; + uint16_t unused; + uint8_t area; + uint8_t dude; + uint16_t request_id; + float x; + float y; + uint32_t unused2; + ItemData data; + } cmd = {0x5F, 0x0A, 0x0000, area, from_enemy, request_id, x, y, 0, item}; + send_command(l, 0x60, 0x00, cmd); +} + +// notifies other players that a stack was split and part of it dropped (a new item was created) +void send_drop_stacked_item(shared_ptr l, shared_ptr c, + const ItemData& item, uint8_t area, float x, float y) { + struct { + uint8_t subcommand; + uint8_t subsize; + uint16_t unused; + uint16_t area; + uint16_t unused2; + float x; + float y; + uint32_t unused3; + ItemData data; + } cmd = {0x5D, 0x09, 0x0000, area, 0, x, y, 0, item}; + send_command(l, 0x60, 0x00, cmd); +} + +// notifies other players that an item was picked up +void send_pick_up_item(shared_ptr l, shared_ptr c, + uint32_t item_id, uint8_t area) { + struct { + uint8_t subcommand; + uint8_t subsize; + uint16_t client_id; + uint16_t client_id2; + uint16_t area; + uint32_t item_id; + } cmd = {0x59, 0x03, c->lobby_client_id, c->lobby_client_id, area, item_id}; + send_command(l, 0x60, 0x00, cmd); +} + +// creates an item in a player's inventory (used for withdrawing items from the bank) +void send_create_inventory_item(shared_ptr l, shared_ptr c, + const ItemData& item) { + struct { + uint8_t subcommand; + uint8_t subsize; + uint16_t client_id; + ItemData item; + uint32_t unused; + } cmd = {0xBE, 0x07, c->lobby_client_id, item, 0}; + send_command(l, 0x60, 0x00, cmd); +} + +// destroys an item +void send_destroy_item(shared_ptr l, shared_ptr c, + uint32_t item_id, uint32_t amount) { + struct { + uint8_t subcommand; + uint8_t subsize; + uint16_t client_id; + uint32_t item_id; + uint32_t amount; + } cmd = {0x29, 0x03, c->lobby_client_id, item_id, amount}; + send_command(l, 0x60, 0x00, cmd); +} + +// sends the player his/her bank data +void send_bank(shared_ptr c) { + vector items(c->player.bank.items, + &c->player.bank.items[c->player.bank.num_items]); + + uint32_t checksum = (rand() << 30) | (rand() << 15) | rand(); + struct { + uint8_t subcommand; + uint8_t unused1; + uint16_t unused2; + uint32_t size; // same as size in header (computed later) + uint32_t checksum; // can be random; client won't notice + uint32_t numItems; + uint32_t meseta; + } cmd = {0xBC, 0, 0, 0, checksum, c->player.bank.num_items, c->player.bank.meseta}; + + size_t size = 8 + sizeof(cmd) + items.size() * sizeof(PlayerBankItem); + cmd.size = size; + + send_command(c, 0x6C, 0x00, cmd, items); +} + +// notifies players about a level up +void send_level_up(shared_ptr l, shared_ptr c) { + PlayerStats stats = c->player.disp.stats; + + for (size_t x = 0; x < c->player.inventory.num_items; x++) { + if ((c->player.inventory.items[x].equip_flags & 0x08) && + (c->player.inventory.items[x].data.item_data1[0] == 0x02)) { + stats.dfp += (c->player.inventory.items[x].data.item_data1w[2] / 100); + stats.atp += (c->player.inventory.items[x].data.item_data1w[3] / 50); + stats.ata += (c->player.inventory.items[x].data.item_data1w[4] / 200); + stats.mst += (c->player.inventory.items[x].data.item_data1w[5] / 50); + } + } + + PSOSubcommand sub[5]; + sub[0].byte[0] = 0x30; + sub[0].byte[1] = 0x05; + sub[0].word[1] = c->lobby_client_id; + sub[1].word[0] = stats.atp; + sub[1].word[1] = stats.mst; + sub[2].word[0] = stats.evp; + sub[2].word[1] = stats.hp; + sub[3].word[0] = stats.dfp; + sub[3].word[1] = stats.ata; + sub[4].dword = c->player.disp.level; + send_command(l, 0x60, 0x00, sub, 0x14); +} + +// gives a player EXP +void send_give_experience(shared_ptr l, shared_ptr c, + uint32_t amount) { + PSOSubcommand sub[2]; + sub[0].word[0] = 0x02BF; + sub[0].word[1] = c->lobby_client_id; + sub[1].dword = amount; + send_command(l, 0x60, 0x00, sub, 8); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// ep3 only commands + +// sends the (PRS-compressed) card list to the client +void send_ep3_card_list_update(shared_ptr c) { + auto file_data = file_cache.get("system/ep3/cardupdate.mnr"); + + string data("\0\0\0\0", 4); + *reinterpret_cast(const_cast(data.data())) = file_data->size(); + data += *file_data; + data.resize((data.size() + 3) & ~3); + + send_command(c, 0xB8, 0x00, data); +} + +// sends the client a generic rank +void send_ep3_rank_update(shared_ptr c) { + struct { + uint32_t rank; + char rankText[0x0C]; + uint32_t meseta; + uint32_t max_meseta; + uint32_t jukebox_songs_unlocked; + } cmd = {0, "\0\0\0\0\0\0\0\0\0\0\0", 0x00FFFFFF, 0x00FFFFFF, 0xFFFFFFFF}; + send_command(c, 0xB7, 0x00, cmd); +} + +// sends the map list (used for battle setup) to all players in a game +void send_ep3_map_list(shared_ptr l) { + auto file_data = file_cache.get("system/ep3/maplist.mnr"); + + string data(16, '\0'); + PSOSubcommand* subs = reinterpret_cast(const_cast(data.data())); + subs[0].dword = 0x000000B6; + subs[1].dword = (23 + file_data->size()) & 0xFFFFFFFC; + subs[2].dword = 0x00000040; + subs[3].dword = file_data->size(); + data += *file_data; + + send_command(l, 0x6C, 0x00, data); +} + +// sends the map data for the chosen map to all players in the game +void send_ep3_map_data(shared_ptr l, uint32_t map_id) { + string filename = string_printf("system/ep3/map%08lX.mnm", map_id); + auto file_data = file_cache.get(filename); + + string data(12, '\0'); + PSOSubcommand* subs = reinterpret_cast(const_cast(data.data())); + subs[0].dword = 0x000000B6; + subs[1].dword = (19 + file_data->size()) & 0xFFFFFFFC; + subs[2].dword = 0x00000041; + data += *file_data; + + send_command(l, 0x6C, 0x00, data); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// CommandLoadQuestFile: sends a quest file to the client. +// the _OpenFile functions send the begin command (44/A6), and the _SendChunk functions send a chunk of data (13/A7). + +static void send_quest_open_file_pc_gc(shared_ptr c, + const string& filename, uint32_t file_size, bool is_download_quest, + bool is_ep3_quest) { + struct { + char name[0x20]; + uint16_t unused; + uint16_t flags; + char filename[0x10]; + uint32_t file_size; + } cmd; + strncpy(cmd.name, filename.c_str(), 0x20); + cmd.unused = 0; + cmd.flags = 2 + is_ep3_quest; + strncpy(cmd.filename, filename.c_str(), 0x10); + cmd.file_size = file_size; + send_command(c, is_download_quest ? 0xA6 : 0x44, 0x00, cmd); +} + +static void send_quest_open_file_bb(shared_ptr c, + const string& filename, uint32_t file_size, bool is_download_quest, + bool is_ep3_quest) { + struct { + uint8_t unused[0x22]; + uint16_t flags; + char filename[0x10]; + uint32_t file_size; + char name[0x18]; + } cmd; + memset(cmd.unused, 0, 0x22); + cmd.flags = 2 + is_ep3_quest; + strncpy(cmd.filename, filename.c_str(), 0x10); + cmd.file_size = file_size; + send_command(c, is_download_quest ? 0xA6 : 0x44, 0x00, cmd); +} + +static void send_quest_file_chunk(shared_ptr c, const char* filename, + size_t chunk_index, const void* data, size_t size, bool is_download_quest) { + if (size > 0x400) { + throw invalid_argument("quest file chunks must be 1KB or smaller"); + } + + struct { + char filename[0x10]; + uint8_t data[0x400]; + uint32_t data_size; + } cmd; + strncpy(cmd.filename, filename, 0x10); + memcpy(cmd.data, data, size); + if (size < 0x400) { + memset(&cmd.data[size], 0, 0x400 - size); + } + cmd.data_size = size; + + send_command(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd); +} + +void send_quest_file(shared_ptr c, const string& basename, + const string& contents, bool is_download_quest, bool is_ep3_quest) { + + if (c->version == GameVersion::PC || c->version == GameVersion::GC) { + send_quest_open_file_pc_gc(c, basename, contents.size(), is_download_quest, + is_ep3_quest); + } else if (c->version == GameVersion::BB) { + send_quest_open_file_bb(c, basename, contents.size(), is_download_quest, + is_ep3_quest); + } else { + throw invalid_argument("cannot send quest files to this version of client"); + } + + for (size_t offset = 0; offset < contents.size(); offset += 0x400) { + size_t chunk_bytes = contents.size() - offset; + if (chunk_bytes > 0x400) { + chunk_bytes = 0x400; + } + send_quest_file_chunk(c, basename.c_str(), offset / 0x400, + contents.data() + offset, chunk_bytes, is_download_quest); + } +} + +void send_server_time(shared_ptr c) { + uint64_t t = now(); + + time_t t_secs = t / 1000000; + struct tm t_parsed; + gmtime_r(&t_secs, &t_parsed); + + string time_str(128, 0); + size_t len = strftime(const_cast(time_str.data()), time_str.size(), + "%Y:%m:%d: %H:%M:%S.000", &t_parsed); + if (len == 0) { + throw runtime_error("format_time buffer too short"); + } + time_str.resize(len); + + send_command(c, 0xB1, 0x00, time_str); +} diff --git a/SendCommands.hh b/SendCommands.hh new file mode 100644 index 00000000..1ef107f4 --- /dev/null +++ b/SendCommands.hh @@ -0,0 +1,162 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "Client.hh" +#include "Lobby.hh" +#include "Server.hh" +#include "Menu.hh" +#include "Quest.hh" +#include "Text.hh" + + + +#define MAIN_MENU_ID 0x512CBD43 +#define INFORMATION_MENU_ID 0x93320CAA +#define LOBBY_MENU_ID 0x01F8471B +#define GAME_MENU_ID 0x205CD430 +#define QUEST_MENU_ID 0x7F02CA94 +#define QUEST_FILTER_MENU_ID 0xC38CA039 + +#define MAIN_MENU_GO_TO_LOBBY 0x00000001 +#define MAIN_MENU_INFORMATION 0x00000002 +#define MAIN_MENU_DISCONNECT 0x00000003 + + + +void send_command(std::shared_ptr c, uint16_t command, uint32_t flag = 0, + const void* data = NULL, size_t size = 0); + +void send_command(std::shared_ptr l, uint16_t command, uint32_t flag = 0, + const void* data = NULL, size_t size = 0); + +template +void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, + const STRUCT& data) { + send_command(c, command, flag, &data, sizeof(data)); +} + +template +void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, + const std::vector& data) { + send_command(c, command, flag, data.data(), data.size() * sizeof(STRUCT)); +} + +template +void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, + const STRUCT& data, const std::vector& array_data) { + std::string all_data(reinterpret_cast(&data), sizeof(STRUCT)); + all_data.append(reinterpret_cast(array_data.data()), + array_data.size() * sizeof(ENTRY)); + send_command(c, command, flag, all_data.data(), all_data.size()); +} + + + + + +void send_server_init(std::shared_ptr c, bool initial_connection); +void send_update_client_config(std::shared_ptr c); + +void send_reconnect(std::shared_ptr c, uint32_t address, uint16_t port); +void send_pc_gc_split_reconnect(std::shared_ptr c, uint32_t address, + uint16_t pc_port); + +void send_client_init_bb(std::shared_ptr c, uint32_t error); +void send_team_and_key_config_bb(std::shared_ptr c); +void send_player_preview_bb(std::shared_ptr c, uint8_t player_index, + const PlayerDispDataBBPreview* preview); +void send_accept_client_checksum_bb(std::shared_ptr c); +void send_guild_card_header_bb(std::shared_ptr c); +void send_guild_card_chunk_bb(std::shared_ptr c, size_t chunk_index); +void send_stream_file_bb(std::shared_ptr c); +void send_approve_player_choice_bb(std::shared_ptr c); +void send_complete_player_bb(std::shared_ptr c); + +void send_check_directory_patch(std::shared_ptr c, const char* dir); + +void send_message_box(std::shared_ptr c, const char16_t* text); +void send_lobby_name(std::shared_ptr c, const char16_t* text); +void send_quest_info(std::shared_ptr c, const char16_t* text); +void send_lobby_message_box(std::shared_ptr c, const char16_t* text); +void send_ship_info(std::shared_ptr c, const char16_t* text); +void send_text_message(std::shared_ptr c, const char16_t* text); +void send_text_message(std::shared_ptr l, const char16_t* text); +void send_chat_message(std::shared_ptr c, uint32_t from_serial_number, + const char16_t* from_name, const char16_t* text); + +template +void send_text_message_printf(std::shared_ptr t, const char* format, ...) { + va_list va; + va_start(va, format); + std::string buf = string_vprintf(format, va); + va_end(va); + std::u16string decoded = decode_sjis(buf); + return send_text_message(t, decoded.c_str()); +} + +void send_info_board(std::shared_ptr c, std::shared_ptr l); + +void send_card_search_result(std::shared_ptr s, std::shared_ptr c, + std::shared_ptr result, std::shared_ptr result_lobby); + +void send_guild_card(std::shared_ptr c, std::shared_ptr source); +void send_menu(std::shared_ptr c, const char16_t* menu_name, + uint32_t menu_id, const std::vector& items, bool is_info_menu); +void send_game_menu(std::shared_ptr c, std::shared_ptr s); +void send_quest_menu(std::shared_ptr c, uint32_t menu_id, + const std::vector>& quests, bool is_download_menu); +void send_quest_menu(std::shared_ptr c, uint32_t menu_id, + const std::vector& items, bool is_download_menu); +void send_lobby_list(std::shared_ptr c, std::shared_ptr s); + +void send_join_lobby(std::shared_ptr c, std::shared_ptr l); +void send_player_join_notification(std::shared_ptr c, + std::shared_ptr l, std::shared_ptr joining_client); +void send_player_leave_notification(std::shared_ptr l, + uint8_t leaving_client_id); +void send_get_player_info(std::shared_ptr c); + +void send_arrow_update(std::shared_ptr l); +void send_resume_game(std::shared_ptr l); + +enum PlayerStatsChange { + SubtractHP = 0, + SubtractTP = 1, + SubtractMeseta = 2, + AddHP = 3, + AddTP = 4, +}; + +void send_player_stats_change(std::shared_ptr l, std::shared_ptr c, + PlayerStatsChange which, uint32_t amount); +void send_warp(std::shared_ptr c, uint32_t area); + +void send_drop_item(std::shared_ptr l, const ItemData& item, + bool from_enemy, uint8_t area, float x, float y, uint16_t request_id); +void send_drop_stacked_item(std::shared_ptr l, std::shared_ptr c, + const ItemData& item, uint8_t area, float x, float y); +void send_pick_up_item(std::shared_ptr l, std::shared_ptr c, uint32_t id, + uint8_t area); +void send_create_inventory_item(std::shared_ptr l, std::shared_ptr c, + const ItemData& item); +void send_destroy_item(std::shared_ptr l, std::shared_ptr c, + uint32_t item_id, uint32_t amount); +void send_bank(std::shared_ptr c); +void send_level_up(std::shared_ptr l, std::shared_ptr c); +void send_give_experience(std::shared_ptr l, std::shared_ptr c, + uint32_t amount); +void send_ep3_card_list_update(std::shared_ptr c); +void send_ep3_rank_update(std::shared_ptr c); +void send_ep3_map_list(std::shared_ptr l); +void send_ep3_map_data(std::shared_ptr l, uint32_t map_id); + +void send_quest_file(std::shared_ptr c, const std::string& basename, + const std::string& contents, bool is_download_quest, bool is_ep3_quest); + +void send_server_time(std::shared_ptr c); diff --git a/Server.cc b/Server.cc new file mode 100644 index 00000000..565dabdc --- /dev/null +++ b/Server.cc @@ -0,0 +1,309 @@ +#include "Server.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "PSOProtocol.hh" +#include "ReceiveCommands.hh" + +using namespace std; + + + +Server::WorkerThread::WorkerThread(Server* server, int worker_num) : + server(server), worker_num(worker_num), + base(event_base_new(), event_base_free), t() { + this->thread_name = string_printf("Server::run_thread (worker_num=%d)", + worker_num); +} + +void Server::WorkerThread::disconnect_client(struct bufferevent* bev) { + auto client = this->bev_to_client.at(bev); + this->bev_to_client.erase(bev); + { + rw_guard g(client->lock, true); + bufferevent_free(client->bev); + client->bev = NULL; + } + this->server->client_count--; +} + +void Server::WorkerThread::dispatch_on_listen_accept( + struct evconnlistener *listener, evutil_socket_t fd, + struct sockaddr *address, int socklen, void *ctx) { + WorkerThread* wt = (WorkerThread*)ctx; + wt->server->on_listen_accept(*wt, listener, fd, address, socklen); +} + +void Server::WorkerThread::dispatch_on_listen_error( + struct evconnlistener *listener, void *ctx) { + WorkerThread* wt = (WorkerThread*)ctx; + wt->server->on_listen_error(*wt, listener); +} + +void Server::WorkerThread::dispatch_on_client_input( + struct bufferevent *bev, void *ctx) { + WorkerThread* wt = (WorkerThread*)ctx; + wt->server->on_client_input(*wt, bev); +} + +void Server::WorkerThread::dispatch_on_client_error( + struct bufferevent *bev, short events, void *ctx) { + WorkerThread* wt = (WorkerThread*)ctx; + wt->server->on_client_error(*wt, bev, events); +} + +void Server::WorkerThread::dispatch_check_for_thread_exit( + evutil_socket_t fd, short what, void* ctx) { + WorkerThread* wt = (WorkerThread*)ctx; + wt->server->check_for_thread_exit(*wt, fd, what); +} + +void Server::on_listen_accept(Server::WorkerThread& wt, + struct evconnlistener *listener, evutil_socket_t fd, + struct sockaddr *address, int socklen) { + + int fd_flags = fcntl(fd, F_GETFD, 0); + if (fd_flags >= 0) { + fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC); + } + + int listen_fd = evconnlistener_get_fd(listener); + GameVersion version; + ServerBehavior initial_state; + try { + auto p = this->listen_fd_to_version_and_state.at(listen_fd); + version = p.first; + initial_state = p.second; + } catch (const out_of_range& e) { + log(WARNING, "[Server] can\'t determine version for socket %d; disconnecting client", + listen_fd); + close(fd); + return; + } + + struct bufferevent *bev = bufferevent_socket_new(wt.base.get(), fd, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_UNLOCK_CALLBACKS); + auto emplace_ret = wt.bev_to_client.emplace(bev, new Client(bev, version, initial_state)); + this->client_count++; + + bufferevent_setcb(bev, &WorkerThread::dispatch_on_client_input, NULL, + &WorkerThread::dispatch_on_client_error, &wt); + bufferevent_enable(bev, EV_READ | EV_WRITE); + + this->process_client_connect(emplace_ret.first->second); +} + +void Server::on_listen_error(Server::WorkerThread& wt, + struct evconnlistener *listener) { + int err = EVUTIL_SOCKET_ERROR(); + log(ERROR, "[Server] failure on listening socket %d: %d (%s)\n", + evconnlistener_get_fd(listener), err, + evutil_socket_error_to_string(err)); + event_base_loopexit(wt.base.get(), NULL); +} + +void Server::on_client_input(Server::WorkerThread& wt, + struct bufferevent *bev) { + shared_ptr c; + try { + c = wt.bev_to_client.at(bev); + } catch (const out_of_range& e) { + log(WARNING, "[Server] received message from client with no configuration"); + + // ignore all the data + struct evbuffer* in_buffer = bufferevent_get_input(bev); + evbuffer_drain(in_buffer, evbuffer_get_length(in_buffer)); + return; + } + + if (c->should_disconnect) { + wt.disconnect_client(bev); + this->process_client_disconnect(c); + return; + } + + c->last_recv_time = now(); + this->receive_and_process_commands(c, bev); +} + +void Server::on_client_error(Server::WorkerThread& wt, + struct bufferevent *bev, short events) { + shared_ptr c; + try { + c = wt.bev_to_client.at(bev); + } catch (const out_of_range& e) { } + + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + log(WARNING, "[Server] client caused %d (%s)\n", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + wt.disconnect_client(bev); + if (c) { + this->process_client_disconnect(c); + } + } +} + +void Server::check_for_thread_exit(Server::WorkerThread& wt, + evutil_socket_t fd, short what) { + if (this->should_exit) { + event_base_loopexit(wt.base.get(), NULL); + } +} + +void Server::receive_and_process_commands(shared_ptr c, struct bufferevent* bev) { + struct evbuffer* buf = bufferevent_get_input(bev); + size_t header_size = (c->version == GameVersion::BB) ? 8 : 4; + + // read as much data into recv_buffer as we can and decrypt it + size_t existing_bytes = c->recv_buffer.size(); + size_t new_bytes = evbuffer_get_length(buf); + new_bytes &= ~(header_size - 1); // only read in multiples of header_size + c->recv_buffer.resize(existing_bytes + new_bytes); + void* recv_ptr = const_cast(c->recv_buffer.data() + existing_bytes); + if (evbuffer_remove(buf, recv_ptr, new_bytes) != new_bytes) { + throw runtime_error("some bytes could not be read from the receive buffer"); + } + + // decrypt the received data if encryption is enabled + if (c->crypt_in.get()) { + c->crypt_in->decrypt(recv_ptr, new_bytes); + } + + // process as many commands as possible + size_t offset = 0; + while (offset < c->recv_buffer.size()) { + const PSOCommandHeader* header = reinterpret_cast( + c->recv_buffer.data() + offset); + size_t size = header->size(c->version); + if (offset + size > c->recv_buffer.size()) { + break; // don't have a complete command; we're done for now + } + + // if we get here, then we have a complete, decrypted command waiting to be + // processed. we copy it out and append zeroes on the end so that it's safe + // to call string functions on the buffer in command handlers + string data = c->recv_buffer.substr(offset + header_size, size - header_size); + data.append(4, '\0'); + process_command(this->state, c, header->command(c->version), + header->flag(c->version), size - header_size, data.data()); + + // BB pads commands to 8-byte boundaries, so if we see a shorter command, + // skip over the padding + offset += (size + header_size - 1) & ~(header_size - 1); + } + + // remove the processed commands from the receive buffer + c->recv_buffer = c->recv_buffer.substr(offset); +} + +void Server::process_client_connect(std::shared_ptr c) { + process_connect(this->state, c); +} + +void Server::process_client_disconnect(std::shared_ptr c) { + process_disconnect(this->state, c); +} + +void Server::run_thread(int worker_num) { + WorkerThread& wt = this->threads[worker_num]; + + struct timeval tv = usecs_to_timeval(2000000); + + struct event* ev = event_new(wt.base.get(), -1, EV_PERSIST, + &WorkerThread::dispatch_check_for_thread_exit, &wt); + event_add(ev, &tv); + + event_base_dispatch(wt.base.get()); + + event_del(ev); +} + +Server::Server(shared_ptr state) : + should_exit(false), client_count(0), state(state) { + for (size_t x = 0; x < this->state->num_threads; x++) { + this->threads.emplace_back(this, x); + } +} + +void Server::listen(const string& socket_path, GameVersion version, ServerBehavior initial_state) { + int fd = ::listen(socket_path, 0, SOMAXCONN); + log(INFO, "[Server] listening on unix socket %s (version %s) on fd %d", + socket_path.c_str(), name_for_version(version), fd); + this->add_socket(fd, version, initial_state); +} + +void Server::listen(const string& addr, int port, GameVersion version, ServerBehavior initial_state) { + int fd = ::listen(addr, port, SOMAXCONN); + string netloc_str = render_netloc(addr, port); + log(INFO, "[Server] listening on tcp interface %s (version %s) on fd %d", + netloc_str.c_str(), name_for_version(version), fd); + this->add_socket(fd, version, initial_state); +} + +void Server::listen(int port, GameVersion version, ServerBehavior initial_state) { + this->listen("", port, version, initial_state); +} + +void Server::add_socket(int fd, GameVersion version, ServerBehavior initial_state) { + this->listen_fd_to_version_and_state.emplace(piecewise_construct, + forward_as_tuple(fd), forward_as_tuple(version, initial_state)); +} + +void Server::start() { + for (auto& wt : this->threads) { + for (const auto& it : this->listen_fd_to_version_and_state) { + struct evconnlistener* listener = evconnlistener_new(wt.base.get(), + WorkerThread::dispatch_on_listen_accept, &wt, LEV_OPT_REUSEABLE, 0, + it.first); + if (!listener) { + throw runtime_error("can\'t create evconnlistener"); + } + evconnlistener_set_error_cb(listener, WorkerThread::dispatch_on_listen_error); + wt.listeners.emplace(listener, evconnlistener_free); + } + wt.t = thread(&Server::run_thread, this, wt.worker_num); + } +} + +void Server::schedule_stop() { + log(INFO, "[Server] scheduling exit for all threads"); + this->should_exit = true; + + for (const auto& it : listen_fd_to_version_and_state) { + log(INFO, "[Server] closing listening fd %d", it.first); + close(it.first); + } +} + +void Server::wait_for_stop() { + for (auto& wt : this->threads) { + if (!wt.t.joinable()) { + continue; + } + log(INFO, "[Server] waiting for worker %d to terminate", wt.worker_num); + wt.t.join(); + } + log(INFO, "[Server] shutdown complete"); +} diff --git a/Server.hh b/Server.hh new file mode 100644 index 00000000..6f6f4136 --- /dev/null +++ b/Server.hh @@ -0,0 +1,77 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "Client.hh" +#include "ServerState.hh" + + + +class Server { +private: + struct WorkerThread { + Server* server; + int worker_num; + std::unique_ptr base; + std::unordered_set> listeners; + std::unordered_map> bev_to_client; + std::thread t; + std::string thread_name; + + WorkerThread(Server* server, int worker_num); + + void disconnect_client(struct bufferevent* bev); + + static void dispatch_on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx); + static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); + static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); + static void dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx); + static void dispatch_check_for_thread_exit(evutil_socket_t fd, short what, void* ctx); + }; + + std::atomic should_exit; + std::vector threads; + + std::atomic client_count; + std::unordered_map> listen_fd_to_version_and_state; + std::shared_ptr state; + + void on_listen_accept(WorkerThread& wt, struct evconnlistener *listener, + evutil_socket_t fd, struct sockaddr *address, int socklen); + void on_listen_error(WorkerThread& wt, struct evconnlistener *listener); + void on_client_input(WorkerThread& wt, struct bufferevent *bev); + void on_client_error(WorkerThread& wt, struct bufferevent *bev, short events); + void check_for_thread_exit(WorkerThread& wt, evutil_socket_t fd, short what); + + void receive_and_process_commands(std::shared_ptr c, struct bufferevent* buf); + + void process_client_connect(std::shared_ptr c); + void process_client_disconnect(std::shared_ptr c); + void process_client_command(std::shared_ptr c, const std::string& command); + + void run_thread(int thread_id); + +public: + Server() = delete; + Server(const Server&) = delete; + Server(Server&&) = delete; + Server(std::shared_ptr state); + virtual ~Server() = default; + + void listen(const std::string& socket_path, GameVersion version, ServerBehavior initial_state); + void listen(const std::string& addr, int port, GameVersion version, ServerBehavior initial_state); + void listen(int port, GameVersion version, ServerBehavior initial_state); + void add_socket(int fd, GameVersion version, ServerBehavior initial_state); + + virtual void start(); + virtual void schedule_stop(); + virtual void wait_for_stop(); +}; diff --git a/ServerState.cc b/ServerState.cc new file mode 100644 index 00000000..c87bfba2 --- /dev/null +++ b/ServerState.cc @@ -0,0 +1,175 @@ +#include "ServerState.hh" + +#include + +#include "SendCommands.hh" +#include "Text.hh" + +using namespace std; + + + +ServerState::ServerState() : next_lobby_id(1), next_game_id(-1) { + this->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby", + u"Join the lobby.", 0); + this->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information", + u"View server information.", MenuItemFlag::RequiresMessageBoxes); + this->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect", + u"Disconnect.", 0); + + for (size_t x = 0; x < 15; x++) { + shared_ptr l(new Lobby()); + l->flags |= LobbyFlag::Public | LobbyFlag::Default; + this->add_lobby(l); + } + for (size_t x = 0; x < 5; x++) { + shared_ptr l(new Lobby()); + l->flags |= LobbyFlag::Public | LobbyFlag::Default | LobbyFlag::Episode3; + this->add_lobby(l); + } +} + +void ServerState::add_client_to_available_lobby(shared_ptr c) { + rw_guard g(this->lobbies_lock, false); + + // nonnegative lobby IDs are public, so start at 0 + auto it = this->id_to_lobby.lower_bound(0); + for (; it != this->id_to_lobby.end(); it++) { + if (!(it->second->flags & LobbyFlag::Public)) { + continue; + } + try { + it->second->add_client(c); + break; + } catch (const out_of_range&) { } + } + + if (it == this->id_to_lobby.end()) { + throw out_of_range("all lobbies full"); + } + + // send a join message to the joining player, and notifications to all others + this->send_lobby_join_notifications(it->second, c); +} + +void ServerState::remove_client_from_lobby(shared_ptr c) { + rw_guard g(this->lobbies_lock, false); + + auto l = this->id_to_lobby.at(c->lobby_id); + l->remove_client(c); + send_player_leave_notification(l, c->lobby_client_id); +} + +void ServerState::change_client_lobby(shared_ptr c, shared_ptr new_lobby) { + uint8_t old_lobby_client_id = c->lobby_client_id; + + shared_ptr current_lobby = this->find_lobby(c->lobby_id); + try { + if (current_lobby) { + current_lobby->move_client_to_lobby(new_lobby, c); + } else { + new_lobby->add_client(c); + } + } catch (const out_of_range&) { + send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby is full."); + return; + } + + if (current_lobby) { + send_player_leave_notification(current_lobby, old_lobby_client_id); + } + this->send_lobby_join_notifications(new_lobby, c); +} + +void ServerState::send_lobby_join_notifications(shared_ptr l, + shared_ptr joining_client) { + rw_guard g2(l->lock, false); + for (auto& other_client : l->clients) { + if (other_client == joining_client) { + send_join_lobby(joining_client, l); + } else { + send_player_join_notification(other_client, l, joining_client); + } + } +} + +shared_ptr ServerState::find_lobby(int64_t lobby_id) { + rw_guard g(this->lobbies_lock, false); + return this->id_to_lobby.at(lobby_id); +} + +shared_ptr ServerState::find_lobby(const u16string& name) { + rw_guard g(this->lobbies_lock, false); + return this->name_to_lobby.at(name); +} + +vector> ServerState::all_lobbies() { + rw_guard g(this->lobbies_lock, false); + vector> ret; + for (auto& it : this->id_to_lobby) { + ret.emplace_back(it.second); + } + return ret; +} + +void ServerState::add_lobby(shared_ptr l) { + if (l->is_game()) { + l->lobby_id = this->next_game_id--; + } else { + l->lobby_id = this->next_lobby_id++; + } + + rw_guard g(this->lobbies_lock, true); + if (this->id_to_lobby.count(l->lobby_id)) { + throw logic_error("lobby already exists with the given id"); + } + if (this->name_to_lobby.count(l->name)) { + throw invalid_argument("lobby already exists with the given name"); + } + this->id_to_lobby.emplace(l->lobby_id, l); + if (l->name[0]) { + this->name_to_lobby.emplace(l->name, l); + } +} + +void ServerState::remove_lobby(int64_t lobby_id) { + rw_guard g(this->lobbies_lock, true); + auto it = this->id_to_lobby.find(lobby_id); + if (it == this->id_to_lobby.end()) { + return; + } + if (it->second->name[0]) { + this->name_to_lobby.erase(it->second->name); + } + this->id_to_lobby.erase(it); +} + +shared_ptr ServerState::find_client(const char16_t* identifier, + uint64_t serial_number, shared_ptr l) { + + if ((serial_number == 0) && identifier) { + try { + string encoded = encode_sjis(identifier); + serial_number = stoull(encoded, NULL, 0); + } catch (const exception&) { } + } + + // look in the current lobby first + if (l) { + try { + return l->find_client(identifier, serial_number); + } catch (const out_of_range&) { } + } + + // look in all lobbies if not found + for (auto& other_l : this->all_lobbies()) { + if (l == other_l) { + continue; // don't bother looking again + } + try { + return other_l->find_client(identifier, serial_number); + } catch (const out_of_range&) { } + } + + throw out_of_range("client not found"); +} \ No newline at end of file diff --git a/ServerState.hh b/ServerState.hh new file mode 100644 index 00000000..7b603765 --- /dev/null +++ b/ServerState.hh @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Client.hh" +#include "Items.hh" +#include "LevelTable.hh" +#include "License.hh" +#include "Lobby.hh" +#include "Menu.hh" +#include "Quest.hh" + + + +struct PortConfiguration { + uint16_t port; + GameVersion version; + ServerBehavior behavior; +}; + +struct ServerState { + std::string name; + std::unordered_map port_configuration; + std::shared_ptr quest_index; + std::shared_ptr level_table; + std::shared_ptr battle_params; + std::shared_ptr common_item_creator; + + std::shared_ptr license_manager; + + std::vector main_menu; + std::shared_ptr> information_menu; + std::shared_ptr> id_to_information_contents; + + size_t num_threads; + + rw_lock lobbies_lock; + std::map> id_to_lobby; + std::unordered_map> name_to_lobby; + + std::atomic next_lobby_id; + std::atomic next_game_id; + + std::set all_addresses; + uint32_t local_address; + uint32_t external_address; + + ServerState(); + + void add_client_to_available_lobby(std::shared_ptr c); + void remove_client_from_lobby(std::shared_ptr c); + void change_client_lobby(std::shared_ptr c, + std::shared_ptr new_lobby); + + void send_lobby_join_notifications(std::shared_ptr l, + std::shared_ptr joining_client); + + std::shared_ptr find_lobby(int64_t lobby_id); + std::shared_ptr find_lobby(const std::u16string& name); + std::vector> all_lobbies(); + + void add_lobby(std::shared_ptr l); + void remove_lobby(int64_t lobby_id); + + std::shared_ptr find_client(const char16_t* identifier = NULL, + uint64_t serial_number = 0, std::shared_ptr l = NULL); +}; diff --git a/Text.cc b/Text.cc new file mode 100644 index 00000000..37a5cac2 --- /dev/null +++ b/Text.cc @@ -0,0 +1,155 @@ +#include "Text.hh" + +#include +#include +#include +#include + +using namespace std; + + + +int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) { + return char_traits::compare(s1, s2, count); +} + +char16_t* char16cpy(char16_t* dest, const char16_t* src, size_t count) { + return char_traits::copy(dest, src, count); +} + +size_t char16len(const char16_t* s) { + return char_traits::length(s); +} + + + +// None of these functions truly convert between SJIS and Unicode. They will +// convert English properly (and some other languages as well), but Japanese +// text will screw up horribly +// TODO: fix this shit. this is definitely the worst part of this entire project + +void encode_sjis(char* dest, const char16_t* source, size_t max) { + while (*source && (--max)) { + *(dest++) = *(source++); + }; + *dest = 0; +} + +void decode_sjis(char16_t* dest, const char* source, size_t max) { + while (*source && (--max)) { + *(dest++) = *(source++); + }; + *dest = 0; +} + +std::string encode_sjis(const char16_t* source) { + string ret; + while (*source) { + ret.push_back(*(source++)); + }; + return ret; +} + +std::u16string decode_sjis(const char* source) { + u16string ret; + while (*source) { + ret.push_back(*(source++)); + }; + return ret; +} + +std::string encode_sjis(const std::u16string& source) { + string ret; + for (char16_t ch : source) { + ret.push_back(ch); + }; + return ret; +} + +std::u16string decode_sjis(const std::string& source) { + u16string ret; + for (char16_t ch : source) { + ret.push_back(ch); + }; + return ret; +} + + + +void add_language_marker_inplace(char* a, char e, size_t dest_count) { + if ((a[0] == '\t') && (a[1] != 'C')) { + return; + } + + size_t existing_count = strlen(a); + if (existing_count > dest_count - 3) { + existing_count = dest_count - 3; + } + memmove(&a[2], a, existing_count + 1); + a[0] = '\t'; + a[1] = e; + a[existing_count + 2] = 0; +} + +void add_language_marker_inplace(char16_t* a, char16_t e, size_t dest_count) { + if ((a[0] == '\t') && (a[1] != 'C')) { + return; + } + + size_t existing_count = char16len(a); + if (existing_count > dest_count - 3) { + existing_count = dest_count - 3; + } + memmove(&a[2], a, existing_count + 1); + a[0] = '\t'; + a[1] = e; + a[existing_count + 2] = 0; +} + +void remove_language_marker_inplace(char* a) { + if ((a[0] == '\t') && (a[1] != 'C')) { + strcpy(a, &a[2]); + } +} + +void remove_language_marker_inplace(char16_t* a) { + if ((a[0] == '\t') && (a[1] != 'C')) { + char16cpy(a, &a[2], char16len(a) - 2); + } +} + +std::string add_language_marker(const std::string& s, char marker) { + if ((s.size() >= 2) && (s[0] == '\t') && (s[1] != 'C')) { + return s; + } + + string ret; + ret.push_back('\t'); + ret.push_back(marker); + return ret + s; +} + +std::u16string add_language_marker(const std::u16string& s, char16_t marker) { + if ((s.size() >= 2) && (s[0] == L'\t') && (s[1] != L'C')) { + return s; + } + + u16string ret; + ret.push_back(L'\t'); + ret.push_back(marker); + return ret + s; +} + +std::string remove_language_marker(const std::string& s) { + if ((s.size() < 2) || (s[0] != '\t') || (s[1] == 'C')) { + return s; + } + return s.substr(2); +} + +std::u16string remove_language_marker(const std::u16string& s) { + if ((s.size() < 2) || (s[0] != L'\t') || (s[1] == L'C')) { + return s; + } + return s.substr(2); +} diff --git a/Text.hh b/Text.hh new file mode 100644 index 00000000..62cde056 --- /dev/null +++ b/Text.hh @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +#include + + +int char16cmp(const char16_t* s1, const char16_t* s2, size_t count); +char16_t* char16cpy(char16_t* dest, const char16_t* src, size_t count); +size_t char16len(const char16_t* s); + + +void encode_sjis(char* dest, const char16_t* source, size_t dest_count); +void decode_sjis(char16_t* dest, const char* source, size_t dest_count); +std::string encode_sjis(const char16_t* source); +std::u16string decode_sjis(const char* source); +std::string encode_sjis(const std::u16string& source); +std::u16string decode_sjis(const std::string& source); + + +void add_language_marker_inplace(char* s, char marker, size_t dest_count); +void add_language_marker_inplace(char16_t* s, char16_t marker, size_t dest_count); +void remove_language_marker_inplace(char* s); +void remove_language_marker_inplace(char16_t* s); +std::string add_language_marker(const std::string& s, char marker); +std::u16string add_language_marker(const std::u16string& s, char16_t marker); +std::string remove_language_marker(const std::string& s); +std::u16string remove_language_marker(const std::u16string& s); + + +template +void replace_char_inplace(T* a, T f, T r) { + while (*a) { + if (*a == f) { + *a = r; + } + a++; + } +} + +template +void add_color_inplace(T* a) { + T* d = a; + + while (*a) { + if (*a == '$') { + *(d++) = '\t'; + } else if (*a == '#') { + *(d++) = '\n'; + } else if (*a == '%') { + a++; + if (*a == 's') { + *(d++) = '$'; + } else if (*a == '%') { + *(d++) = '%'; + } else if (*a == 'n') { + *(d++) = '#'; + } else { + *(d++) = *a; + } + } + a++; + } +} diff --git a/Version.cc b/Version.cc new file mode 100644 index 00000000..da1bcf48 --- /dev/null +++ b/Version.cc @@ -0,0 +1,57 @@ +#include "Version.hh" + + + +uint16_t flags_for_version(GameVersion version, uint8_t sub_version) { + switch (sub_version) { + case 0x00: // initial check (before 9E recognition) + switch (version) { + case GameVersion::DC: + return ClientFlag::DefaultV2DC; + case GameVersion::GC: + return ClientFlag::DefaultV3GC; + case GameVersion::PC: + return ClientFlag::DefaultV2PC; + case GameVersion::Patch: + return ClientFlag::DefaultV2PC; + case GameVersion::BB: + return ClientFlag::DefaultV3BB; + } + break; + case 0x29: // PSO PC + return ClientFlag::DefaultV2PC; + case 0x30: // ??? + case 0x31: // PSO Ep1&2 US10, US11, EU10, JP10 + case 0x33: // PSO Ep1&2 EU50HZ + case 0x34: // PSO Ep1&2 JP11 + return ClientFlag::DefaultV3GC; + case 0x32: // PSO Ep1&2 US12, JP12 + case 0x35: // PSO Ep1&2 US12, JP12 + case 0x36: // PSO Ep1&2 US12, JP12 + case 0x39: // PSO Ep1&2 US12, JP12 + return ClientFlag::DefaultV3GCPlus; + case 0x40: // PSO Ep3 trial + case 0x41: // PSO Ep3 US + case 0x42: // PSO Ep3 JP + case 0x43: // PSO Ep3 UK + return ClientFlag::DefaultV4; + } + return 0; +} + +const char* name_for_version(GameVersion version) { + switch (version) { + case GameVersion::GC: + return "GC"; + case GameVersion::PC: + return "PC"; + case GameVersion::BB: + return "BB"; + case GameVersion::DC: + return "DC"; + case GameVersion::Patch: + return "Patch"; + default: + return "Unknown"; + } +} diff --git a/Version.hh b/Version.hh new file mode 100644 index 00000000..207e1619 --- /dev/null +++ b/Version.hh @@ -0,0 +1,39 @@ +#pragma once + +#include + + + +enum class GameVersion { + DC = 0, + PC, + Patch, + GC, + BB, +}; + +enum ClientFlag { + // after joining a lobby, client will no longer send D6 commands when they close message boxes + NoMessageBoxCloseConfirmationAfterLobbyJoin = 0x0004, + // client has the above flag and has already joined a lobby + NoMessageBoxCloseConfirmation = 0x0008, + // client can see Ep3 lobbies + CanSeeExtraLobbies = 0x0010, + // client is episode 3 and should use its game mechanic + Episode3Games = 0x0020, + // client is DC v1 (disables some features) + IsDCv1 = 0x0040, + // client is loading into a game + Loading = 0x0080, + + DefaultV1 = IsDCv1, + DefaultV2DC = 0x0000, + DefaultV2PC = 0x0000, + DefaultV3GC = 0x0000, + DefaultV3GCPlus = NoMessageBoxCloseConfirmationAfterLobbyJoin, + DefaultV3BB = NoMessageBoxCloseConfirmationAfterLobbyJoin | NoMessageBoxCloseConfirmation, + DefaultV4 = NoMessageBoxCloseConfirmationAfterLobbyJoin | CanSeeExtraLobbies | Episode3Games, +}; + +uint16_t flags_for_version(GameVersion version, uint8_t sub_version); +const char* name_for_version(GameVersion version); diff --git a/system/blueburst/BattleParamEntry.dat b/system/blueburst/BattleParamEntry.dat new file mode 100755 index 00000000..9fa7c344 Binary files /dev/null and b/system/blueburst/BattleParamEntry.dat differ diff --git a/system/blueburst/BattleParamEntry_ep4.dat b/system/blueburst/BattleParamEntry_ep4.dat new file mode 100755 index 00000000..d58fdc93 Binary files /dev/null and b/system/blueburst/BattleParamEntry_ep4.dat differ diff --git a/system/blueburst/BattleParamEntry_ep4_on.dat b/system/blueburst/BattleParamEntry_ep4_on.dat new file mode 100755 index 00000000..3baa952d Binary files /dev/null and b/system/blueburst/BattleParamEntry_ep4_on.dat differ diff --git a/system/blueburst/BattleParamEntry_lab.dat b/system/blueburst/BattleParamEntry_lab.dat new file mode 100755 index 00000000..0baaad78 Binary files /dev/null and b/system/blueburst/BattleParamEntry_lab.dat differ diff --git a/system/blueburst/BattleParamEntry_lab_on.dat b/system/blueburst/BattleParamEntry_lab_on.dat new file mode 100755 index 00000000..7bafca36 Binary files /dev/null and b/system/blueburst/BattleParamEntry_lab_on.dat differ diff --git a/system/blueburst/BattleParamEntry_on.dat b/system/blueburst/BattleParamEntry_on.dat new file mode 100755 index 00000000..7771015c Binary files /dev/null and b/system/blueburst/BattleParamEntry_on.dat differ diff --git a/system/blueburst/ItemMagEdit.prs b/system/blueburst/ItemMagEdit.prs new file mode 100755 index 00000000..45d38bf8 Binary files /dev/null and b/system/blueburst/ItemMagEdit.prs differ diff --git a/system/blueburst/ItemPMT.prs b/system/blueburst/ItemPMT.prs new file mode 100755 index 00000000..588890ae Binary files /dev/null and b/system/blueburst/ItemPMT.prs differ diff --git a/system/blueburst/ItemRT.rel b/system/blueburst/ItemRT.rel new file mode 100755 index 00000000..db986b81 Binary files /dev/null and b/system/blueburst/ItemRT.rel differ diff --git a/system/blueburst/PlyLevelTbl.prs b/system/blueburst/PlyLevelTbl.prs new file mode 100755 index 00000000..23f390c6 Binary files /dev/null and b/system/blueburst/PlyLevelTbl.prs differ diff --git a/system/blueburst/default.nsa b/system/blueburst/default.nsa new file mode 100755 index 00000000..a2156b09 Binary files /dev/null and b/system/blueburst/default.nsa differ diff --git a/system/blueburst/default.nsb b/system/blueburst/default.nsb new file mode 100755 index 00000000..f9426a1e Binary files /dev/null and b/system/blueburst/default.nsb differ diff --git a/system/blueburst/droptype.ini b/system/blueburst/droptype.ini new file mode 100755 index 00000000..aae6cfb8 --- /dev/null +++ b/system/blueburst/droptype.ini @@ -0,0 +1,154 @@ +# Types of nonrare items that enemies drop. + +# NORMAL MODE + +# shield = 01 +# armor = 02 +# unit = 03 +# random = 04 +# unknown = 05 +# weapon = 06 + +Drop_Item_00_00 06 # booma +Drop_Item_00_01 06 # gobooma +Drop_Item_00_02 01 # gigobooma +Drop_Item_00_03 02 # savage wolf +Drop_Item_00_04 06 # barbarous wolf +Drop_Item_00_05 02 # rag rappy +Drop_Item_00_06 01 # al rappy +Drop_Item_00_07 06 # mothmant +Drop_Item_00_7A 05 # monest +Drop_Item_00_08 06 # hildebear +Drop_Item_00_09 06 # hildeblue +Drop_Item_00_0A 04 # dragon +Drop_Item_00_0B 06 # evil shark +Drop_Item_00_0C 06 # pal shark +Drop_Item_00_0D 03 # guil shark +Drop_Item_00_0E 02 # poison lily +Drop_Item_00_0F 03 # nar lily +Drop_Item_00_10 06 # grass assassin +Drop_Item_00_11 01 # nano dragon +Drop_Item_00_12 02 # pofuilly slime +Drop_Item_00_13 03 # pouilly slime +Drop_Item_00_14 01 # pan arms +Drop_Item_00_15 03 # migium +Drop_Item_00_16 02 # hidoom +Drop_Item_00_17 04 # de rol le +Drop_Item_00_18 06 # gillchic +Drop_Item_00_19 06 # dubchic +Drop_Item_00_1A 01 # canadine +Drop_Item_00_1B 02 # canane +Drop_Item_00_1C 06 # sinow beat +Drop_Item_00_1D 03 # sinow gold +Drop_Item_00_1E 01 # garanz +Drop_Item_00_1F 04 # vol opt +Drop_Item_00_20 06 # dimenian +Drop_Item_00_21 06 # la dimenian +Drop_Item_00_22 02 # so dimenian +Drop_Item_00_23 06 # claw +Drop_Item_00_24 03 # bulclaw +Drop_Item_00_25 06 # bulk +Drop_Item_00_26 02 # delsaber +Drop_Item_00_27 02 # dark belra +Drop_Item_00_28 01 # dark gunner +Drop_Item_00_29 01 # death dunner +Drop_Item_00_2A 03 # chaos sorceror +Drop_Item_00_2B 03 # chaos bringer +Drop_Item_00_2C 04 # dark falz + +Drop_Item_00_2E 06 # dimenian +Drop_Item_00_2F 06 # la dimenian +Drop_Item_00_30 01 # so dimenian +Drop_Item_00_31 02 # rag rappy +Drop_Item_00_32 01 # love rappy +Drop_Item_00_33 04 # hallo rappy +Drop_Item_00_34 04 # saint rappy +Drop_Item_00_35 04 # egg rappy +Drop_Item_00_36 06 # grass assassin +Drop_Item_00_37 02 # poison lily +Drop_Item_00_38 03 # nar lily +Drop_Item_00_39 06 # mothmant +Drop_Item_00_3A 06 # hildebear +Drop_Item_00_3B 06 # hildeblue +Drop_Item_00_3C 02 # dark belra +Drop_Item_00_3D 04 # barba ray + +Drop_Item_00_3E 02 # savage wolf +Drop_Item_00_3F 06 # barbarous wolf +Drop_Item_00_40 06 # gillchic +Drop_Item_00_41 06 # dubchic +Drop_Item_00_42 02 # delsaber +Drop_Item_00_43 01 # pan arms +Drop_Item_00_44 03 # migium +Drop_Item_00_45 02 # hidoom +Drop_Item_00_46 01 # garanz +Drop_Item_00_47 03 # chaos sorceror +Drop_Item_00_48 04 # gol dragon + +Drop_Item_00_49 06 # merillia +Drop_Item_00_4A 06 # meriltas +Drop_Item_00_4B 06 # ul gibbon +Drop_Item_00_4C 01 # zol gibbon +Drop_Item_00_4D 06 # gee +Drop_Item_00_4E 06 # sinow berill +Drop_Item_00_4F 02 # sinow spigell +Drop_Item_00_50 05 # mericarol +Drop_Item_00_51 02 # merikle +Drop_Item_00_52 01 # mericus +Drop_Item_00_53 03 # gibbles +Drop_Item_00_54 01 # gi gue +Drop_Item_00_55 06 # del lily +Drop_Item_00_56 02 # ill gill +Drop_Item_00_57 03 # epsilon +Drop_Item_00_58 04 # gal gryphon + +Drop_Item_00_59 06 # dolmolm +Drop_Item_00_5A 06 # dolmdarl +Drop_Item_00_5B 03 # recobox +Drop_Item_00_5C 06 # recon +Drop_Item_00_5D 01 # deldepth +Drop_Item_00_5E 06 # delbiter +Drop_Item_00_5F 06 # morfos +Drop_Item_00_60 06 # sinow zoa +Drop_Item_00_61 02 # sinow zele +Drop_Item_00_62 04 # olga flow + +Drop_Item_00_64 05 # boota +Drop_Item_00_65 05 # ze boota +Drop_Item_00_66 05 # ba boota +Drop_Item_00_67 05 # sand rappy +Drop_Item_00_68 05 # del rappy +Drop_Item_00_69 05 # satellite lizard +Drop_Item_00_6A 05 # yowie +Drop_Item_00_6B 05 # astark +Drop_Item_00_6C 05 # zu +Drop_Item_00_6D 05 # pazuzu +Drop_Item_00_6E 05 # dorphon +Drop_Item_00_6F 05 # dorphon eclair +Drop_Item_00_70 05 # goran +Drop_Item_00_71 05 # pyro goran +Drop_Item_00_72 05 # goran detonator +Drop_Item_00_73 05 # merissa a +Drop_Item_00_74 05 # merissa aa +Drop_Item_00_75 05 # girtablulu +Drop_Item_00_76 05 # saint-million +Drop_Item_00_77 05 # shambertin +Drop_Item_00_78 05 # kondrieu + +# HARD MODE + +Drop_Item_01_30 02 # so dimenian +Drop_Item_01_56 01 # ill gill + +# VERY HARD MODE + +Drop_Item_02_02 03 # gigobooma +Drop_Item_02_04 01 # barbarous wolf 08 +Drop_Item_02_05 01 # rag rappy 05 +Drop_Item_02_31 01 # rag rappy +Drop_Item_02_3F 01 # barbarous wolf +Drop_Item_02_56 02 # ill gill + +# ULTIMATE MODE + +# no changes \ No newline at end of file diff --git a/system/blueburst/enemies.nsi b/system/blueburst/enemies.nsi new file mode 100755 index 00000000..de3da997 Binary files /dev/null and b/system/blueburst/enemies.nsi differ diff --git a/system/blueburst/map/m1000.dat b/system/blueburst/map/m1000.dat new file mode 100755 index 00000000..80331ec9 Binary files /dev/null and b/system/blueburst/map/m1000.dat differ diff --git a/system/blueburst/map/m1100.dat b/system/blueburst/map/m1100.dat new file mode 100755 index 00000000..5f12c09f Binary files /dev/null and b/system/blueburst/map/m1100.dat differ diff --git a/system/blueburst/map/m1101.dat b/system/blueburst/map/m1101.dat new file mode 100755 index 00000000..5ba8c158 Binary files /dev/null and b/system/blueburst/map/m1101.dat differ diff --git a/system/blueburst/map/m1102.dat b/system/blueburst/map/m1102.dat new file mode 100755 index 00000000..e0dbbb47 Binary files /dev/null and b/system/blueburst/map/m1102.dat differ diff --git a/system/blueburst/map/m1103.dat b/system/blueburst/map/m1103.dat new file mode 100755 index 00000000..07e55b5c Binary files /dev/null and b/system/blueburst/map/m1103.dat differ diff --git a/system/blueburst/map/m1104.dat b/system/blueburst/map/m1104.dat new file mode 100755 index 00000000..5f0b9bc5 Binary files /dev/null and b/system/blueburst/map/m1104.dat differ diff --git a/system/blueburst/map/m1200.dat b/system/blueburst/map/m1200.dat new file mode 100755 index 00000000..2ef9c72d Binary files /dev/null and b/system/blueburst/map/m1200.dat differ diff --git a/system/blueburst/map/m1201.dat b/system/blueburst/map/m1201.dat new file mode 100755 index 00000000..0fbd058f Binary files /dev/null and b/system/blueburst/map/m1201.dat differ diff --git a/system/blueburst/map/m1202.dat b/system/blueburst/map/m1202.dat new file mode 100755 index 00000000..88d40520 Binary files /dev/null and b/system/blueburst/map/m1202.dat differ diff --git a/system/blueburst/map/m1203.dat b/system/blueburst/map/m1203.dat new file mode 100755 index 00000000..df4065b0 Binary files /dev/null and b/system/blueburst/map/m1203.dat differ diff --git a/system/blueburst/map/m1204.dat b/system/blueburst/map/m1204.dat new file mode 100755 index 00000000..7668e3d3 Binary files /dev/null and b/system/blueburst/map/m1204.dat differ diff --git a/system/blueburst/map/m1300.dat b/system/blueburst/map/m1300.dat new file mode 100755 index 00000000..9d3f32d2 Binary files /dev/null and b/system/blueburst/map/m1300.dat differ diff --git a/system/blueburst/map/m1301.dat b/system/blueburst/map/m1301.dat new file mode 100755 index 00000000..d1e38edd Binary files /dev/null and b/system/blueburst/map/m1301.dat differ diff --git a/system/blueburst/map/m1310.dat b/system/blueburst/map/m1310.dat new file mode 100755 index 00000000..09454aa0 Binary files /dev/null and b/system/blueburst/map/m1310.dat differ diff --git a/system/blueburst/map/m1311.dat b/system/blueburst/map/m1311.dat new file mode 100755 index 00000000..dfa75bb0 Binary files /dev/null and b/system/blueburst/map/m1311.dat differ diff --git a/system/blueburst/map/m1320.dat b/system/blueburst/map/m1320.dat new file mode 100755 index 00000000..700d05d8 Binary files /dev/null and b/system/blueburst/map/m1320.dat differ diff --git a/system/blueburst/map/m1321.dat b/system/blueburst/map/m1321.dat new file mode 100755 index 00000000..725f9c0b Binary files /dev/null and b/system/blueburst/map/m1321.dat differ diff --git a/system/blueburst/map/m1400.dat b/system/blueburst/map/m1400.dat new file mode 100755 index 00000000..7433eb08 Binary files /dev/null and b/system/blueburst/map/m1400.dat differ diff --git a/system/blueburst/map/m1401.dat b/system/blueburst/map/m1401.dat new file mode 100755 index 00000000..c7fcb77e Binary files /dev/null and b/system/blueburst/map/m1401.dat differ diff --git a/system/blueburst/map/m1410.dat b/system/blueburst/map/m1410.dat new file mode 100755 index 00000000..3f01f778 Binary files /dev/null and b/system/blueburst/map/m1410.dat differ diff --git a/system/blueburst/map/m1411.dat b/system/blueburst/map/m1411.dat new file mode 100755 index 00000000..b909170c Binary files /dev/null and b/system/blueburst/map/m1411.dat differ diff --git a/system/blueburst/map/m1420.dat b/system/blueburst/map/m1420.dat new file mode 100755 index 00000000..2a3c491e Binary files /dev/null and b/system/blueburst/map/m1420.dat differ diff --git a/system/blueburst/map/m1421.dat b/system/blueburst/map/m1421.dat new file mode 100755 index 00000000..47866530 Binary files /dev/null and b/system/blueburst/map/m1421.dat differ diff --git a/system/blueburst/map/m1500.dat b/system/blueburst/map/m1500.dat new file mode 100755 index 00000000..8cc5db07 Binary files /dev/null and b/system/blueburst/map/m1500.dat differ diff --git a/system/blueburst/map/m1501.dat b/system/blueburst/map/m1501.dat new file mode 100755 index 00000000..840b7741 Binary files /dev/null and b/system/blueburst/map/m1501.dat differ diff --git a/system/blueburst/map/m1510.dat b/system/blueburst/map/m1510.dat new file mode 100755 index 00000000..7457d326 Binary files /dev/null and b/system/blueburst/map/m1510.dat differ diff --git a/system/blueburst/map/m1511.dat b/system/blueburst/map/m1511.dat new file mode 100755 index 00000000..6747ec13 Binary files /dev/null and b/system/blueburst/map/m1511.dat differ diff --git a/system/blueburst/map/m1520.dat b/system/blueburst/map/m1520.dat new file mode 100755 index 00000000..5a5e1bac Binary files /dev/null and b/system/blueburst/map/m1520.dat differ diff --git a/system/blueburst/map/m1521.dat b/system/blueburst/map/m1521.dat new file mode 100755 index 00000000..20c6c181 Binary files /dev/null and b/system/blueburst/map/m1521.dat differ diff --git a/system/blueburst/map/m1600.dat b/system/blueburst/map/m1600.dat new file mode 100755 index 00000000..743d5ab4 Binary files /dev/null and b/system/blueburst/map/m1600.dat differ diff --git a/system/blueburst/map/m1601.dat b/system/blueburst/map/m1601.dat new file mode 100755 index 00000000..cbf218ea Binary files /dev/null and b/system/blueburst/map/m1601.dat differ diff --git a/system/blueburst/map/m1610.dat b/system/blueburst/map/m1610.dat new file mode 100755 index 00000000..1cf3233d Binary files /dev/null and b/system/blueburst/map/m1610.dat differ diff --git a/system/blueburst/map/m1611.dat b/system/blueburst/map/m1611.dat new file mode 100755 index 00000000..5c039c29 Binary files /dev/null and b/system/blueburst/map/m1611.dat differ diff --git a/system/blueburst/map/m1620.dat b/system/blueburst/map/m1620.dat new file mode 100755 index 00000000..c740f62a Binary files /dev/null and b/system/blueburst/map/m1620.dat differ diff --git a/system/blueburst/map/m1621.dat b/system/blueburst/map/m1621.dat new file mode 100755 index 00000000..61ef9b42 Binary files /dev/null and b/system/blueburst/map/m1621.dat differ diff --git a/system/blueburst/map/m1700.dat b/system/blueburst/map/m1700.dat new file mode 100755 index 00000000..8635c518 Binary files /dev/null and b/system/blueburst/map/m1700.dat differ diff --git a/system/blueburst/map/m1701.dat b/system/blueburst/map/m1701.dat new file mode 100755 index 00000000..950b6fe6 Binary files /dev/null and b/system/blueburst/map/m1701.dat differ diff --git a/system/blueburst/map/m1710.dat b/system/blueburst/map/m1710.dat new file mode 100755 index 00000000..64615d2f Binary files /dev/null and b/system/blueburst/map/m1710.dat differ diff --git a/system/blueburst/map/m1711.dat b/system/blueburst/map/m1711.dat new file mode 100755 index 00000000..bb2a41b5 Binary files /dev/null and b/system/blueburst/map/m1711.dat differ diff --git a/system/blueburst/map/m1720.dat b/system/blueburst/map/m1720.dat new file mode 100755 index 00000000..122c9665 Binary files /dev/null and b/system/blueburst/map/m1720.dat differ diff --git a/system/blueburst/map/m1721.dat b/system/blueburst/map/m1721.dat new file mode 100755 index 00000000..dca68215 Binary files /dev/null and b/system/blueburst/map/m1721.dat differ diff --git a/system/blueburst/map/m1800.dat b/system/blueburst/map/m1800.dat new file mode 100755 index 00000000..49084245 Binary files /dev/null and b/system/blueburst/map/m1800.dat differ diff --git a/system/blueburst/map/m1801.dat b/system/blueburst/map/m1801.dat new file mode 100755 index 00000000..c707b702 Binary files /dev/null and b/system/blueburst/map/m1801.dat differ diff --git a/system/blueburst/map/m1810.dat b/system/blueburst/map/m1810.dat new file mode 100755 index 00000000..f81efa2e Binary files /dev/null and b/system/blueburst/map/m1810.dat differ diff --git a/system/blueburst/map/m1811.dat b/system/blueburst/map/m1811.dat new file mode 100755 index 00000000..f52ef47f Binary files /dev/null and b/system/blueburst/map/m1811.dat differ diff --git a/system/blueburst/map/m1820.dat b/system/blueburst/map/m1820.dat new file mode 100755 index 00000000..e4f27a7c Binary files /dev/null and b/system/blueburst/map/m1820.dat differ diff --git a/system/blueburst/map/m1821.dat b/system/blueburst/map/m1821.dat new file mode 100755 index 00000000..a271d8c7 Binary files /dev/null and b/system/blueburst/map/m1821.dat differ diff --git a/system/blueburst/map/m1900.dat b/system/blueburst/map/m1900.dat new file mode 100755 index 00000000..12f3875a Binary files /dev/null and b/system/blueburst/map/m1900.dat differ diff --git a/system/blueburst/map/m1901.dat b/system/blueburst/map/m1901.dat new file mode 100755 index 00000000..d7aadc67 Binary files /dev/null and b/system/blueburst/map/m1901.dat differ diff --git a/system/blueburst/map/m1910.dat b/system/blueburst/map/m1910.dat new file mode 100755 index 00000000..2ba00fdc Binary files /dev/null and b/system/blueburst/map/m1910.dat differ diff --git a/system/blueburst/map/m1911.dat b/system/blueburst/map/m1911.dat new file mode 100755 index 00000000..abeac981 Binary files /dev/null and b/system/blueburst/map/m1911.dat differ diff --git a/system/blueburst/map/m1920.dat b/system/blueburst/map/m1920.dat new file mode 100755 index 00000000..049dc2b9 Binary files /dev/null and b/system/blueburst/map/m1920.dat differ diff --git a/system/blueburst/map/m1921.dat b/system/blueburst/map/m1921.dat new file mode 100755 index 00000000..66d5d834 Binary files /dev/null and b/system/blueburst/map/m1921.dat differ diff --git a/system/blueburst/map/m1A00.dat b/system/blueburst/map/m1A00.dat new file mode 100755 index 00000000..7cb1f5b7 Binary files /dev/null and b/system/blueburst/map/m1A00.dat differ diff --git a/system/blueburst/map/m1A01.dat b/system/blueburst/map/m1A01.dat new file mode 100755 index 00000000..f6d710a4 Binary files /dev/null and b/system/blueburst/map/m1A01.dat differ diff --git a/system/blueburst/map/m1A10.dat b/system/blueburst/map/m1A10.dat new file mode 100755 index 00000000..b214cb60 Binary files /dev/null and b/system/blueburst/map/m1A10.dat differ diff --git a/system/blueburst/map/m1A11.dat b/system/blueburst/map/m1A11.dat new file mode 100755 index 00000000..8e1088c2 Binary files /dev/null and b/system/blueburst/map/m1A11.dat differ diff --git a/system/blueburst/map/m1A20.dat b/system/blueburst/map/m1A20.dat new file mode 100755 index 00000000..148aa49a Binary files /dev/null and b/system/blueburst/map/m1A20.dat differ diff --git a/system/blueburst/map/m1A21.dat b/system/blueburst/map/m1A21.dat new file mode 100755 index 00000000..41063594 Binary files /dev/null and b/system/blueburst/map/m1A21.dat differ diff --git a/system/blueburst/map/m1B00.dat b/system/blueburst/map/m1B00.dat new file mode 100755 index 00000000..70026bdd Binary files /dev/null and b/system/blueburst/map/m1B00.dat differ diff --git a/system/blueburst/map/m1C00.dat b/system/blueburst/map/m1C00.dat new file mode 100755 index 00000000..2bf835a2 Binary files /dev/null and b/system/blueburst/map/m1C00.dat differ diff --git a/system/blueburst/map/m1D00.dat b/system/blueburst/map/m1D00.dat new file mode 100755 index 00000000..05aad87e Binary files /dev/null and b/system/blueburst/map/m1D00.dat differ diff --git a/system/blueburst/map/m1E00.dat b/system/blueburst/map/m1E00.dat new file mode 100755 index 00000000..30861455 Binary files /dev/null and b/system/blueburst/map/m1E00.dat differ diff --git a/system/blueburst/map/m2000.dat b/system/blueburst/map/m2000.dat new file mode 100755 index 00000000..c8108fb9 Binary files /dev/null and b/system/blueburst/map/m2000.dat differ diff --git a/system/blueburst/map/m2100.dat b/system/blueburst/map/m2100.dat new file mode 100755 index 00000000..013c008e Binary files /dev/null and b/system/blueburst/map/m2100.dat differ diff --git a/system/blueburst/map/m2110.dat b/system/blueburst/map/m2110.dat new file mode 100755 index 00000000..35409b2d Binary files /dev/null and b/system/blueburst/map/m2110.dat differ diff --git a/system/blueburst/map/m2200.dat b/system/blueburst/map/m2200.dat new file mode 100755 index 00000000..581e8e88 Binary files /dev/null and b/system/blueburst/map/m2200.dat differ diff --git a/system/blueburst/map/m2210.dat b/system/blueburst/map/m2210.dat new file mode 100755 index 00000000..571648f9 Binary files /dev/null and b/system/blueburst/map/m2210.dat differ diff --git a/system/blueburst/map/m2300.dat b/system/blueburst/map/m2300.dat new file mode 100755 index 00000000..d646f797 Binary files /dev/null and b/system/blueburst/map/m2300.dat differ diff --git a/system/blueburst/map/m2310.dat b/system/blueburst/map/m2310.dat new file mode 100755 index 00000000..cda7c879 Binary files /dev/null and b/system/blueburst/map/m2310.dat differ diff --git a/system/blueburst/map/m2400.dat b/system/blueburst/map/m2400.dat new file mode 100755 index 00000000..729e607e Binary files /dev/null and b/system/blueburst/map/m2400.dat differ diff --git a/system/blueburst/map/m2410.dat b/system/blueburst/map/m2410.dat new file mode 100755 index 00000000..4aa97aa3 Binary files /dev/null and b/system/blueburst/map/m2410.dat differ diff --git a/system/blueburst/map/m2500.dat b/system/blueburst/map/m2500.dat new file mode 100755 index 00000000..afa87a30 Binary files /dev/null and b/system/blueburst/map/m2500.dat differ diff --git a/system/blueburst/map/m2501.dat b/system/blueburst/map/m2501.dat new file mode 100755 index 00000000..2a8fdfa5 Binary files /dev/null and b/system/blueburst/map/m2501.dat differ diff --git a/system/blueburst/map/m2502.dat b/system/blueburst/map/m2502.dat new file mode 100755 index 00000000..1a86febb Binary files /dev/null and b/system/blueburst/map/m2502.dat differ diff --git a/system/blueburst/map/m2600.dat b/system/blueburst/map/m2600.dat new file mode 100755 index 00000000..4f28ecb4 Binary files /dev/null and b/system/blueburst/map/m2600.dat differ diff --git a/system/blueburst/map/m2601.dat b/system/blueburst/map/m2601.dat new file mode 100755 index 00000000..9682290c Binary files /dev/null and b/system/blueburst/map/m2601.dat differ diff --git a/system/blueburst/map/m2602.dat b/system/blueburst/map/m2602.dat new file mode 100755 index 00000000..26ba3d0e Binary files /dev/null and b/system/blueburst/map/m2602.dat differ diff --git a/system/blueburst/map/m2700.dat b/system/blueburst/map/m2700.dat new file mode 100755 index 00000000..a66e239a Binary files /dev/null and b/system/blueburst/map/m2700.dat differ diff --git a/system/blueburst/map/m2701.dat b/system/blueburst/map/m2701.dat new file mode 100755 index 00000000..817580b7 Binary files /dev/null and b/system/blueburst/map/m2701.dat differ diff --git a/system/blueburst/map/m2702.dat b/system/blueburst/map/m2702.dat new file mode 100755 index 00000000..0b7134b1 Binary files /dev/null and b/system/blueburst/map/m2702.dat differ diff --git a/system/blueburst/map/m2800.dat b/system/blueburst/map/m2800.dat new file mode 100755 index 00000000..7dee3d21 Binary files /dev/null and b/system/blueburst/map/m2800.dat differ diff --git a/system/blueburst/map/m2801.dat b/system/blueburst/map/m2801.dat new file mode 100755 index 00000000..102c1a26 Binary files /dev/null and b/system/blueburst/map/m2801.dat differ diff --git a/system/blueburst/map/m2810.dat b/system/blueburst/map/m2810.dat new file mode 100755 index 00000000..216ec317 Binary files /dev/null and b/system/blueburst/map/m2810.dat differ diff --git a/system/blueburst/map/m2811.dat b/system/blueburst/map/m2811.dat new file mode 100755 index 00000000..0643de59 Binary files /dev/null and b/system/blueburst/map/m2811.dat differ diff --git a/system/blueburst/map/m2900.dat b/system/blueburst/map/m2900.dat new file mode 100755 index 00000000..f6cabb13 Binary files /dev/null and b/system/blueburst/map/m2900.dat differ diff --git a/system/blueburst/map/m2901.dat b/system/blueburst/map/m2901.dat new file mode 100755 index 00000000..023d9e77 Binary files /dev/null and b/system/blueburst/map/m2901.dat differ diff --git a/system/blueburst/map/m2902.dat b/system/blueburst/map/m2902.dat new file mode 100755 index 00000000..5e298884 Binary files /dev/null and b/system/blueburst/map/m2902.dat differ diff --git a/system/blueburst/map/m2A00.dat b/system/blueburst/map/m2A00.dat new file mode 100755 index 00000000..32cbb944 Binary files /dev/null and b/system/blueburst/map/m2A00.dat differ diff --git a/system/blueburst/map/m2A01.dat b/system/blueburst/map/m2A01.dat new file mode 100755 index 00000000..20dda75b Binary files /dev/null and b/system/blueburst/map/m2A01.dat differ diff --git a/system/blueburst/map/m2A10.dat b/system/blueburst/map/m2A10.dat new file mode 100755 index 00000000..867c5504 Binary files /dev/null and b/system/blueburst/map/m2A10.dat differ diff --git a/system/blueburst/map/m2A11.dat b/system/blueburst/map/m2A11.dat new file mode 100755 index 00000000..9ea712f4 Binary files /dev/null and b/system/blueburst/map/m2A11.dat differ diff --git a/system/blueburst/map/m2B00.dat b/system/blueburst/map/m2B00.dat new file mode 100755 index 00000000..fdf7fc8e Binary files /dev/null and b/system/blueburst/map/m2B00.dat differ diff --git a/system/blueburst/map/m2B01.dat b/system/blueburst/map/m2B01.dat new file mode 100755 index 00000000..a3943880 Binary files /dev/null and b/system/blueburst/map/m2B01.dat differ diff --git a/system/blueburst/map/m2B10.dat b/system/blueburst/map/m2B10.dat new file mode 100755 index 00000000..30c0f0e3 Binary files /dev/null and b/system/blueburst/map/m2B10.dat differ diff --git a/system/blueburst/map/m2B11.dat b/system/blueburst/map/m2B11.dat new file mode 100755 index 00000000..0780c4d8 Binary files /dev/null and b/system/blueburst/map/m2B11.dat differ diff --git a/system/blueburst/map/m2C00.dat b/system/blueburst/map/m2C00.dat new file mode 100755 index 00000000..fb98bfdb Binary files /dev/null and b/system/blueburst/map/m2C00.dat differ diff --git a/system/blueburst/map/m2D00.dat b/system/blueburst/map/m2D00.dat new file mode 100755 index 00000000..bebbdcd2 Binary files /dev/null and b/system/blueburst/map/m2D00.dat differ diff --git a/system/blueburst/map/m2E00.dat b/system/blueburst/map/m2E00.dat new file mode 100755 index 00000000..463eed2a Binary files /dev/null and b/system/blueburst/map/m2E00.dat differ diff --git a/system/blueburst/map/m2F00.dat b/system/blueburst/map/m2F00.dat new file mode 100755 index 00000000..86c0134f Binary files /dev/null and b/system/blueburst/map/m2F00.dat differ diff --git a/system/blueburst/map/m3000.dat b/system/blueburst/map/m3000.dat new file mode 100755 index 00000000..a1ae0879 Binary files /dev/null and b/system/blueburst/map/m3000.dat differ diff --git a/system/blueburst/map/m3100.dat b/system/blueburst/map/m3100.dat new file mode 100755 index 00000000..987462ec Binary files /dev/null and b/system/blueburst/map/m3100.dat differ diff --git a/system/blueburst/map/m3101.dat b/system/blueburst/map/m3101.dat new file mode 100755 index 00000000..bf55b3dd Binary files /dev/null and b/system/blueburst/map/m3101.dat differ diff --git a/system/blueburst/map/m3102.dat b/system/blueburst/map/m3102.dat new file mode 100755 index 00000000..25a5e674 Binary files /dev/null and b/system/blueburst/map/m3102.dat differ diff --git a/system/blueburst/map/m3200.dat b/system/blueburst/map/m3200.dat new file mode 100755 index 00000000..62969907 Binary files /dev/null and b/system/blueburst/map/m3200.dat differ diff --git a/system/blueburst/map/m3201.dat b/system/blueburst/map/m3201.dat new file mode 100755 index 00000000..b648bc58 Binary files /dev/null and b/system/blueburst/map/m3201.dat differ diff --git a/system/blueburst/map/m3202.dat b/system/blueburst/map/m3202.dat new file mode 100755 index 00000000..9a6c0939 Binary files /dev/null and b/system/blueburst/map/m3202.dat differ diff --git a/system/blueburst/map/m3300.dat b/system/blueburst/map/m3300.dat new file mode 100755 index 00000000..488ab969 Binary files /dev/null and b/system/blueburst/map/m3300.dat differ diff --git a/system/blueburst/map/m3301.dat b/system/blueburst/map/m3301.dat new file mode 100755 index 00000000..a43649a4 Binary files /dev/null and b/system/blueburst/map/m3301.dat differ diff --git a/system/blueburst/map/m3302.dat b/system/blueburst/map/m3302.dat new file mode 100755 index 00000000..72b7377f Binary files /dev/null and b/system/blueburst/map/m3302.dat differ diff --git a/system/blueburst/map/m3400.dat b/system/blueburst/map/m3400.dat new file mode 100755 index 00000000..773a2c89 Binary files /dev/null and b/system/blueburst/map/m3400.dat differ diff --git a/system/blueburst/map/m3401.dat b/system/blueburst/map/m3401.dat new file mode 100755 index 00000000..1de5b72f Binary files /dev/null and b/system/blueburst/map/m3401.dat differ diff --git a/system/blueburst/map/m3402.dat b/system/blueburst/map/m3402.dat new file mode 100755 index 00000000..5fc67ee0 Binary files /dev/null and b/system/blueburst/map/m3402.dat differ diff --git a/system/blueburst/map/m3500.dat b/system/blueburst/map/m3500.dat new file mode 100755 index 00000000..ee23d0c1 Binary files /dev/null and b/system/blueburst/map/m3500.dat differ diff --git a/system/blueburst/map/m3501.dat b/system/blueburst/map/m3501.dat new file mode 100755 index 00000000..82c16e33 Binary files /dev/null and b/system/blueburst/map/m3501.dat differ diff --git a/system/blueburst/map/m3502.dat b/system/blueburst/map/m3502.dat new file mode 100755 index 00000000..0e268f94 Binary files /dev/null and b/system/blueburst/map/m3502.dat differ diff --git a/system/blueburst/map/m3600.dat b/system/blueburst/map/m3600.dat new file mode 100755 index 00000000..72f420aa Binary files /dev/null and b/system/blueburst/map/m3600.dat differ diff --git a/system/blueburst/map/m3610.dat b/system/blueburst/map/m3610.dat new file mode 100755 index 00000000..cb831a3f Binary files /dev/null and b/system/blueburst/map/m3610.dat differ diff --git a/system/blueburst/map/m3620.dat b/system/blueburst/map/m3620.dat new file mode 100755 index 00000000..01454735 Binary files /dev/null and b/system/blueburst/map/m3620.dat differ diff --git a/system/blueburst/map/m3700.dat b/system/blueburst/map/m3700.dat new file mode 100755 index 00000000..0bfcd8cc Binary files /dev/null and b/system/blueburst/map/m3700.dat differ diff --git a/system/blueburst/map/m3701.dat b/system/blueburst/map/m3701.dat new file mode 100755 index 00000000..555b6e7b Binary files /dev/null and b/system/blueburst/map/m3701.dat differ diff --git a/system/blueburst/map/m3702.dat b/system/blueburst/map/m3702.dat new file mode 100755 index 00000000..cfd69c76 Binary files /dev/null and b/system/blueburst/map/m3702.dat differ diff --git a/system/blueburst/map/m3800.dat b/system/blueburst/map/m3800.dat new file mode 100755 index 00000000..80c914f0 Binary files /dev/null and b/system/blueburst/map/m3800.dat differ diff --git a/system/blueburst/map/m3810.dat b/system/blueburst/map/m3810.dat new file mode 100755 index 00000000..9b56912a Binary files /dev/null and b/system/blueburst/map/m3810.dat differ diff --git a/system/blueburst/map/m3820.dat b/system/blueburst/map/m3820.dat new file mode 100755 index 00000000..6dc22049 Binary files /dev/null and b/system/blueburst/map/m3820.dat differ diff --git a/system/blueburst/map/m3900.dat b/system/blueburst/map/m3900.dat new file mode 100755 index 00000000..f272c42f Binary files /dev/null and b/system/blueburst/map/m3900.dat differ diff --git a/system/blueburst/map/s1100.dat b/system/blueburst/map/s1100.dat new file mode 100755 index 00000000..8189cb0b Binary files /dev/null and b/system/blueburst/map/s1100.dat differ diff --git a/system/blueburst/map/s1101.dat b/system/blueburst/map/s1101.dat new file mode 100755 index 00000000..38b84563 Binary files /dev/null and b/system/blueburst/map/s1101.dat differ diff --git a/system/blueburst/map/s1102.dat b/system/blueburst/map/s1102.dat new file mode 100755 index 00000000..07672942 Binary files /dev/null and b/system/blueburst/map/s1102.dat differ diff --git a/system/blueburst/map/s1200.dat b/system/blueburst/map/s1200.dat new file mode 100755 index 00000000..5b7735a3 Binary files /dev/null and b/system/blueburst/map/s1200.dat differ diff --git a/system/blueburst/map/s1201.dat b/system/blueburst/map/s1201.dat new file mode 100755 index 00000000..a31010f6 Binary files /dev/null and b/system/blueburst/map/s1201.dat differ diff --git a/system/blueburst/map/s1202.dat b/system/blueburst/map/s1202.dat new file mode 100755 index 00000000..e0c6a082 Binary files /dev/null and b/system/blueburst/map/s1202.dat differ diff --git a/system/blueburst/map/s1300.dat b/system/blueburst/map/s1300.dat new file mode 100755 index 00000000..aeef8b49 Binary files /dev/null and b/system/blueburst/map/s1300.dat differ diff --git a/system/blueburst/map/s1310.dat b/system/blueburst/map/s1310.dat new file mode 100755 index 00000000..1111203a Binary files /dev/null and b/system/blueburst/map/s1310.dat differ diff --git a/system/blueburst/map/s1320.dat b/system/blueburst/map/s1320.dat new file mode 100755 index 00000000..a564a0d2 Binary files /dev/null and b/system/blueburst/map/s1320.dat differ diff --git a/system/blueburst/map/s1400.dat b/system/blueburst/map/s1400.dat new file mode 100755 index 00000000..64312c91 Binary files /dev/null and b/system/blueburst/map/s1400.dat differ diff --git a/system/blueburst/map/s1410.dat b/system/blueburst/map/s1410.dat new file mode 100755 index 00000000..15a33df8 Binary files /dev/null and b/system/blueburst/map/s1410.dat differ diff --git a/system/blueburst/map/s1420.dat b/system/blueburst/map/s1420.dat new file mode 100755 index 00000000..b1bd80dc Binary files /dev/null and b/system/blueburst/map/s1420.dat differ diff --git a/system/blueburst/map/s1500.dat b/system/blueburst/map/s1500.dat new file mode 100755 index 00000000..8cc5db07 Binary files /dev/null and b/system/blueburst/map/s1500.dat differ diff --git a/system/blueburst/map/s1510.dat b/system/blueburst/map/s1510.dat new file mode 100755 index 00000000..7457d326 Binary files /dev/null and b/system/blueburst/map/s1510.dat differ diff --git a/system/blueburst/map/s1520.dat b/system/blueburst/map/s1520.dat new file mode 100755 index 00000000..5a5e1bac Binary files /dev/null and b/system/blueburst/map/s1520.dat differ diff --git a/system/blueburst/map/s2000.dat b/system/blueburst/map/s2000.dat new file mode 100755 index 00000000..97dc98d7 Binary files /dev/null and b/system/blueburst/map/s2000.dat differ diff --git a/system/blueburst/map/s2100.dat b/system/blueburst/map/s2100.dat new file mode 100755 index 00000000..eff9c158 Binary files /dev/null and b/system/blueburst/map/s2100.dat differ diff --git a/system/blueburst/map/s2110.dat b/system/blueburst/map/s2110.dat new file mode 100755 index 00000000..7efa5b1c Binary files /dev/null and b/system/blueburst/map/s2110.dat differ diff --git a/system/blueburst/map/s2200.dat b/system/blueburst/map/s2200.dat new file mode 100755 index 00000000..d9f49e6d Binary files /dev/null and b/system/blueburst/map/s2200.dat differ diff --git a/system/blueburst/map/s2210.dat b/system/blueburst/map/s2210.dat new file mode 100755 index 00000000..c455ba84 Binary files /dev/null and b/system/blueburst/map/s2210.dat differ diff --git a/system/blueburst/map/s2300.dat b/system/blueburst/map/s2300.dat new file mode 100755 index 00000000..51478e9d Binary files /dev/null and b/system/blueburst/map/s2300.dat differ diff --git a/system/blueburst/map/s2310.dat b/system/blueburst/map/s2310.dat new file mode 100755 index 00000000..51381edb Binary files /dev/null and b/system/blueburst/map/s2310.dat differ diff --git a/system/blueburst/map/s2400.dat b/system/blueburst/map/s2400.dat new file mode 100755 index 00000000..72d9d4ac Binary files /dev/null and b/system/blueburst/map/s2400.dat differ diff --git a/system/blueburst/map/s2410.dat b/system/blueburst/map/s2410.dat new file mode 100755 index 00000000..3c6fb1be Binary files /dev/null and b/system/blueburst/map/s2410.dat differ diff --git a/system/blueburst/map/s2500.dat b/system/blueburst/map/s2500.dat new file mode 100755 index 00000000..f8c76cf4 Binary files /dev/null and b/system/blueburst/map/s2500.dat differ diff --git a/system/blueburst/map/s2501.dat b/system/blueburst/map/s2501.dat new file mode 100755 index 00000000..0b5b57d4 Binary files /dev/null and b/system/blueburst/map/s2501.dat differ diff --git a/system/blueburst/map/s2502.dat b/system/blueburst/map/s2502.dat new file mode 100755 index 00000000..bd378ce5 Binary files /dev/null and b/system/blueburst/map/s2502.dat differ diff --git a/system/blueburst/map/s2600.dat b/system/blueburst/map/s2600.dat new file mode 100755 index 00000000..daf7d261 Binary files /dev/null and b/system/blueburst/map/s2600.dat differ diff --git a/system/blueburst/map/s2601.dat b/system/blueburst/map/s2601.dat new file mode 100755 index 00000000..ef2ef183 Binary files /dev/null and b/system/blueburst/map/s2601.dat differ diff --git a/system/blueburst/map/s2602.dat b/system/blueburst/map/s2602.dat new file mode 100755 index 00000000..f2f33ab4 Binary files /dev/null and b/system/blueburst/map/s2602.dat differ diff --git a/system/blueburst/map/s2700.dat b/system/blueburst/map/s2700.dat new file mode 100755 index 00000000..a89681aa Binary files /dev/null and b/system/blueburst/map/s2700.dat differ diff --git a/system/blueburst/map/s2701.dat b/system/blueburst/map/s2701.dat new file mode 100755 index 00000000..7afea958 Binary files /dev/null and b/system/blueburst/map/s2701.dat differ diff --git a/system/blueburst/map/s2702.dat b/system/blueburst/map/s2702.dat new file mode 100755 index 00000000..17821f83 Binary files /dev/null and b/system/blueburst/map/s2702.dat differ diff --git a/system/blueburst/map/s2800.dat b/system/blueburst/map/s2800.dat new file mode 100755 index 00000000..8f3c0e83 Binary files /dev/null and b/system/blueburst/map/s2800.dat differ diff --git a/system/blueburst/map/s2801.dat b/system/blueburst/map/s2801.dat new file mode 100755 index 00000000..4e1e94f3 Binary files /dev/null and b/system/blueburst/map/s2801.dat differ diff --git a/system/blueburst/map/s2810.dat b/system/blueburst/map/s2810.dat new file mode 100755 index 00000000..61bced87 Binary files /dev/null and b/system/blueburst/map/s2810.dat differ diff --git a/system/blueburst/map/s2811.dat b/system/blueburst/map/s2811.dat new file mode 100755 index 00000000..70bfcc03 Binary files /dev/null and b/system/blueburst/map/s2811.dat differ diff --git a/system/blueburst/map/s2900.dat b/system/blueburst/map/s2900.dat new file mode 100755 index 00000000..ee672269 Binary files /dev/null and b/system/blueburst/map/s2900.dat differ diff --git a/system/blueburst/map/s2901.dat b/system/blueburst/map/s2901.dat new file mode 100755 index 00000000..e082c7a5 Binary files /dev/null and b/system/blueburst/map/s2901.dat differ diff --git a/system/blueburst/map/s2902.dat b/system/blueburst/map/s2902.dat new file mode 100755 index 00000000..e028098f Binary files /dev/null and b/system/blueburst/map/s2902.dat differ diff --git a/system/blueburst/map/s2A00.dat b/system/blueburst/map/s2A00.dat new file mode 100755 index 00000000..68f046bb Binary files /dev/null and b/system/blueburst/map/s2A00.dat differ diff --git a/system/blueburst/map/s2A10.dat b/system/blueburst/map/s2A10.dat new file mode 100755 index 00000000..eaedabe2 Binary files /dev/null and b/system/blueburst/map/s2A10.dat differ diff --git a/system/blueburst/map/s2B00.dat b/system/blueburst/map/s2B00.dat new file mode 100755 index 00000000..ec4bf681 Binary files /dev/null and b/system/blueburst/map/s2B00.dat differ diff --git a/system/blueburst/map/s2B10.dat b/system/blueburst/map/s2B10.dat new file mode 100755 index 00000000..ab066a1d Binary files /dev/null and b/system/blueburst/map/s2B10.dat differ diff --git a/system/blueburst/map/s3000.dat b/system/blueburst/map/s3000.dat new file mode 100755 index 00000000..8932a62e Binary files /dev/null and b/system/blueburst/map/s3000.dat differ diff --git a/system/blueburst/player_class_0.nsc b/system/blueburst/player_class_0.nsc new file mode 100755 index 00000000..f8800a64 Binary files /dev/null and b/system/blueburst/player_class_0.nsc differ diff --git a/system/blueburst/player_class_1.nsc b/system/blueburst/player_class_1.nsc new file mode 100755 index 00000000..cf5364a6 Binary files /dev/null and b/system/blueburst/player_class_1.nsc differ diff --git a/system/blueburst/player_class_10.nsc b/system/blueburst/player_class_10.nsc new file mode 100755 index 00000000..e67621e6 Binary files /dev/null and b/system/blueburst/player_class_10.nsc differ diff --git a/system/blueburst/player_class_11.nsc b/system/blueburst/player_class_11.nsc new file mode 100755 index 00000000..6d8c838f Binary files /dev/null and b/system/blueburst/player_class_11.nsc differ diff --git a/system/blueburst/player_class_2.nsc b/system/blueburst/player_class_2.nsc new file mode 100755 index 00000000..991c065e Binary files /dev/null and b/system/blueburst/player_class_2.nsc differ diff --git a/system/blueburst/player_class_3.nsc b/system/blueburst/player_class_3.nsc new file mode 100755 index 00000000..fba974a6 Binary files /dev/null and b/system/blueburst/player_class_3.nsc differ diff --git a/system/blueburst/player_class_4.nsc b/system/blueburst/player_class_4.nsc new file mode 100755 index 00000000..10446d00 Binary files /dev/null and b/system/blueburst/player_class_4.nsc differ diff --git a/system/blueburst/player_class_5.nsc b/system/blueburst/player_class_5.nsc new file mode 100755 index 00000000..409b0310 Binary files /dev/null and b/system/blueburst/player_class_5.nsc differ diff --git a/system/blueburst/player_class_6.nsc b/system/blueburst/player_class_6.nsc new file mode 100755 index 00000000..0437f50d Binary files /dev/null and b/system/blueburst/player_class_6.nsc differ diff --git a/system/blueburst/player_class_7.nsc b/system/blueburst/player_class_7.nsc new file mode 100755 index 00000000..3062f234 Binary files /dev/null and b/system/blueburst/player_class_7.nsc differ diff --git a/system/blueburst/player_class_8.nsc b/system/blueburst/player_class_8.nsc new file mode 100755 index 00000000..aef18d3a Binary files /dev/null and b/system/blueburst/player_class_8.nsc differ diff --git a/system/blueburst/player_class_9.nsc b/system/blueburst/player_class_9.nsc new file mode 100755 index 00000000..99bb8a25 Binary files /dev/null and b/system/blueburst/player_class_9.nsc differ diff --git a/system/blueburst/streamfile.ind b/system/blueburst/streamfile.ind new file mode 100755 index 00000000..eeb144f8 Binary files /dev/null and b/system/blueburst/streamfile.ind differ diff --git a/system/config.json b/system/config.json new file mode 100755 index 00000000..6892f2aa --- /dev/null +++ b/system/config.json @@ -0,0 +1,108 @@ +{ + // ************************************** + // *** FUZZIQER SOFTWARE PSO SERVER *** + // *** CONFIGURATION FILE *** + // ************************************** + + // ************** + // SERVER OPTIONS + // ************** + + // Server's name (max. 16 characters) + "ServerName": "Alexandria", + // Address to connect local clients to + "LocalAddress": "10.0.1.6", + // Address to connect external clients to + "ExternalAddress": "10.0.1.6", + // Number of worker threads to run + "Threads": 1, + + // **************** + // INFORMATION MENU + // **************** + + // Each entry is a 3-list of [title, short-description, full-contents]. + "InformationMenuContents": [ + ["Text", "$C7Some things you\nmay need to know\nabout text on\nthis server", "$C7Everything you type will be filtered.\n\nDollar signs will become tab chars, which can be\nused to color team names and info boards.\nTo color your text, type %sCx, where x is a\nvalue from the Text Colors list.\n\nPound signs (number signs) will become returns\n(newlines).\n\nA percent sign will create a special character.\nTyping a percent sign followed by one of these\nletters will make one of these special characters:\n%%d = %d %%x = %x %%p = %p %%+ = %+\n%%1 = %1 %%2 = %2 %%3 = %3 %%c = %c\n%%l = %l %%y = %y %%X = %X %%Y = %Y\n%%Z = %Z %%? = %? %%C = %C %%R = %R\n%%s = %s %%%% = %% %%n = %n"], + ["Text colors", "$C7Display color values", "These values can be used to color text.\n\n$C0Color 0$C7 - Black\n$C1Color 1$C7 - Blue\n$C2Color 2$C7 - Green\n$C3Color 3$C7 - Cyan\n$C4Color 4$C7 - Red\n$C5Color 5$C7 - Purple\n$C6Color 6$C7 - Yellow\n$C7Color 7$C7 - White\n$C8Color 8$C7 - Pink\n$C9Color 9$C7 - Violet\n$CGColor G$C7 - Orange Pulse"], + ["Lobby commands", "$C7Display commands\nfor use in the\nlobby", "Lobby commands: you must be a moderator to use\nthese commands.\n\n%sallevent - change the server's event\n%sevent - change this lobby's event\n%stype - change this lobby's type\n%sann - announce a message\n%sax - send a message to the server"], + ["Game commands", "$C7Display commands\nfor use in games", "Game commands: you must be the game leader to\nuse these commands.\n\n%spassword - set the game's password\n%smaxlevel <%n> - set the game's maximum level\n%sminlevel <%n> - set the game's minimum level\n%scheat - enable or disable cheat mode"], + ["Player commands", "$C7Display commands\nfor player data", "Player commands: anyone can use these commands.\n\n%sarrow - change your lobby arrow color\n%sbbchar - convert a character to PSOBB format\n%sli - show lobby/game information"], + ["Cheat commands", "$C7Display commands\nfor cheating", "Cheat commands: cheat mode must be enabled in the\ngame for these commands to work.\n\n%scheat - enables or disables cheat mode in a game.\n%sinfhp - enables or disables infinite HP.\n%sinftp - enables or disables infinite TP.\n%swarp - sends you to a certain area.\n%sitem - creates an item. (BB only)\n%sedit - changes your character's stats. (BB only)"], + ["Mod commands", "$C7Display commands\nfor moderators", "Moderation commands: you must be a moderator to\nuse these commands.\n\n%ssilence - silence/unsilence a player\n%skick - kick a player\n%sban - ban a player"], + ["Using $item", "$C7Show how to use\nthe %sitem command", "Using the %sitem command\n\nFor PSOBB, the %sitem command is used to create\nitems. Use it like this:\n\n%sitem \n\nThe item code must be from 2 to 16 bytes (4 to 32\ncharacters) of hexadecimal data. Any bytes you\nomit will become zeroes.\n\nThe item that you specify will appear from the next\nenemy you kill or box you break.\n\nTo create meseta, use the command like this:\n\n%sitem 04000000 %n0 %n0 "], + ["Using $edit", "$C7Show how to use\nthe %sedit command", "Using the %sedit command\n\n%sedit is used on PSOBB to change character stats.\nUse one of the following subcommands:\n\n%sedit ATP \n%sedit MST \n%sedit EVP \n%sedit HP \n%sedit DFP \n%sedit ATA \n%sedit LCK \n%sedit MESETA \n%sedit EXP \n%sedit LEVEL \n%sedit NAMECOLOR \n%sedit NAME \n%sedit NPC \n%sedit TECH \n\nNPC names: none, ninja, rico, sonic, knuckles,\n flowen, elly\n\nTechnique names: foie, gifoie, rafoie, barta,\n gibarta, rabarta, zonde, gizonde, razonde,\n grants, deband, jellen, zalure, shifta, ryuker,\n resta, anti, reverser, megid, all"], + ["Using $bbchar", "$C7Show how to use\nthe %sbbchar command", "Using the %sbbchar command\n\n%sbbchar is used to convert a character from an\nolder version of PSO to Blue Burst format and save\nit on this server. Use the command like this:\n\n%sbbchar \n\nIf the username and password are correct, the\ncharacter that you're currently playing as will be\nconverted to PSOBB format and saved under that\naccount, in the specified character slot."], + ["Using banks", "$C7Show how to use\nthe bank switching\nfeature on PSOBB", "Using PSOBB banks\n\nThe %schangebank command allows you to borrow\nthe bank from another character in your account.\nUse it like this:\n\n%schangebank player<%n>\n\nFor example, to use character 4's bank, type:\n\n%schangebank player4"], + ["Arrow colors", "$C7Display lobby arrow\ncolor list", "$C7These values can be used with the $C6%sarrow$C7 command.\n\n0: no marker\n1: red\n2: blue\n3: green\n4: yellow\n5: purple\n6: cyan\n7: orange\n8: pink\n9: white\n10: white\n11: white\n12: black"], + ["Event values", "$C7Display lobby event\nlist", "These values can be used with the $C6%sevent$C7 command.\n\nnone - no event\nxmas - Christmas event\nval - Valentine's Day\neaster - Easter Sunday event\nhallo - Halloween event\nsonic - Sonic Adventure DX event\nnewyear - New Year's event\nbval - White Day\nwedding - Wedding Day event\nspring - spring event\ns-spring - spring event with striped background\nsummer - summer event\ns-summer - summer event with striped background\nfall - fall event"], + ["GC lobby types", "$C7Display lobby type\nlist for Episodes\nI & II", "These values can be used with the %sln command.\n$C6*$C7 indicates lobbies where players can't move.\n$C2Green$C7 indicates Episode 1 & 2 (GC) only lobbies.\n\nnormal - standard lobby\n$C2inormal$C7 - under standard lobby $C6*$C7\n$C2ipc$C7 - under PC lobby $C6*$C7\n$C2iball$C7 - under soccer lobby $C6*$C7\n$C2cave1$C7 - Cave 1 $C6*$C7\n$C2cave2u$C7 - Cave 2 Ultimate $C6*$C7\n$C2dragon$C7 - Dragon stage (floor is black)\n$C2derolle$C7 - De Rol Le stage (water/walls are gone)\n$C2volopt$C7 - Vol Opt stage\n$C2darkfalz$C7 - Dark Falz stage"], + ["Ep3 lobby types", "$C7Display lobby type\nlist for Episode\nIII", "These values can be used with the %sln command.\n$C6*$C7 indicates lobbies where players can't move.\n$C8Pink$C7 indicates Episode 3 only lobbies.\n\nnormal - standard lobby\n$C8planet$C7 - Blank Ragol Lobby\n$C8clouds$C7 - Blank Sky Lobby\n$C8cave$C7 - Unguis Lapis\n$C8jungle$C7 - Episode 2 Jungle\n$C8forest2-1$C7 - Episode 1 Forest 2 (ground)\n$C8forest2-2$C7 - Episode 1 Forest 2 (near Dome)\n$C8windpower$C7\n$C8overview$C7\n$C8seaside$C7 - Episode 2 Seaside\n$C8some?$C7\n$C8dmorgue$C7 - Destroyed Morgue\n$C8caelum$C7 - Caelum\n$C8digital$C7\n$C8boss1$C7\n$C8boss2$C7\n$C8boss3$C7\n$C8knight$C7 - Leukon Knight stage\n$C8sky$C7 - Via Tubus\n$C8morgue$C7 - Morgue"], + ["Area list", "$C7Display stage code\nlist", "These values can be used with the $C6%swarp$C7 and\n$C6%smove$C7 commands.\n\n$C2Green$C7 areas will be empty unless you are in a quest.\n$C6Yellow$C7 areas will not allow you to move.\n\n $C8Episode 1 / Episode 2 / Episode 4$C7\n0: Pioneer 2 / Pioneer 2 / Pioneer 2\n1: Forest 1 / Temple Alpha / Crater East\n2: Forest 2 / Temple Beta / Crater West\n3: Caves 1 / Spaceship Alpha / Crater South\n4: Caves 2 / Spaceship Beta / Crater North\n5: Caves 3 / CCA / Crater Interior\n6: Mines 1 / Jungle North / Desert 1\n7: Mines 2 / Jungle South / Desert 2\n8: Ruins 1 / Mountain / Desert 3\n9: Ruins 2 / Seaside / Saint Million\n10: Ruins 3 / Seabed Upper / $C6Purgatory$C7\n11: Dragon / Seabed Lower\n12: De Rol Le / Gal Gryphon\n13: Vol Opt / Olga Flow\n14: Dark Falz / Barba Ray\n15: $C2Lobby$C7 / Gol Dragon\n16: $C6Battle 1$C7 / $C6Seaside Night$C7\n17: $C6Battle 2$C7 / $C2Tower$C7"], + ], + + // *************** + // GAME PARAMETERS + // *************** + + // Item drop rates for non-rare items. For each type (boxes or enemies), all + // the categories must add up to a number less than 0x100000000. Each number + // is a probability (out of 0x100000000) that the given item type will appear. + // The values in each list must sum to 0xFFFFFFFF or less. + + "CommonItemDropRates-Enemy": [ + 0x03000000, // material + 0x20000000, // equipment + 0x06000000, // technique disk + 0x01800000, // scape doll + 0x06000000, // grinder + 0x10000000, // atomizers, etc. + 0x20000000, // mates/fluids + 0x40000000, // meseta + ], + "CommonItemDropRates-Box": [ + 0x00800000, // material + 0x20000000, // equipment + 0x01000000, // technique disk + 0x02000000, // scape doll + 0x08000000, // grinder + 0x10000000, // atomizers, etc. + 0x20000000, // mates/fluids + 0x80000000, // meseta + ], + + // Unit drop rates for non-rare items. Each entry is an array of unit types, + // one array per difficulty. Each entry in the array has an equal probability + // of dropping. If a unit type is 0xFF, then no item will drop. + + "CommonUnitTypes": [ + // normal + [0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x08, 0x08, 0x08, 0x0C, 0x0C, 0x0C, + 0x10, 0x10, 0x10, 0x14, 0x14, 0x14, 0x18, 0x18, 0x18, 0x21, 0x21, 0x21, + 0x24, 0x24, 0x24, 0x27, 0x27, 0x27, 0x2A, 0x2A, 0x2A, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF], + // hard + [0x01, 0x01, 0x05, 0x05, 0x09, 0x09, 0x0D, 0x0D, 0x11, 0x11, 0x15, 0x15, + 0x19, 0x19, 0x21, 0x21, 0x24, 0x24, 0x27, 0x27, 0x2A, 0x2A, 0x30, 0x30, + 0x33, 0x33, 0x36, 0x36, 0x39, 0x39, 0x3C, 0x3C, 0x3F, 0x3F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF], + // very hard + [0x02, 0x02, 0x06, 0x06, 0x0A, 0x0A, 0x0E, 0x0E, 0x12, 0x12, 0x16, 0x16, + 0x1A, 0x1A, 0x22, 0x22, 0x25, 0x25, 0x28, 0x28, 0x2B, 0x2B, 0x31, 0x31, + 0x34, 0x34, 0x37, 0x37, 0x3A, 0x3A, 0x3D, 0x3D, 0x3F, 0x3F, 0x41, 0x41, + 0x42, 0x42, 0x43, 0x43, 0x44, 0x44, 0x45, 0x45, 0x46, 0x46, 0x47, 0x47, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF], + // ultimate + [0x02, 0x02, 0x06, 0x06, 0x0A, 0x0A, 0x0E, 0x0E, 0x12, 0x12, 0x16, 0x16, + 0x1A, 0x1A, 0x23, 0x23, 0x26, 0x26, 0x29, 0x29, 0x2C, 0x2C, 0x31, 0x31, + 0x34, 0x34, 0x37, 0x37, 0x3A, 0x3A, 0x3D, 0x3D, 0x3F, 0x3F, 0x41, 0x41, + 0x42, 0x42, 0x43, 0x43, 0x44, 0x44, 0x45, 0x45, 0x46, 0x46, 0x47, 0x47, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF], + ], +} \ No newline at end of file diff --git a/system/ep3/cardupdate.mnr b/system/ep3/cardupdate.mnr new file mode 100755 index 00000000..f2b195ee Binary files /dev/null and b/system/ep3/cardupdate.mnr differ diff --git a/system/ep3/map00000230.mnm b/system/ep3/map00000230.mnm new file mode 100755 index 00000000..17364c2d Binary files /dev/null and b/system/ep3/map00000230.mnm differ diff --git a/system/ep3/map00000244.mnm b/system/ep3/map00000244.mnm new file mode 100755 index 00000000..fe3c4cfb Binary files /dev/null and b/system/ep3/map00000244.mnm differ diff --git a/system/ep3/map00000258.mnm b/system/ep3/map00000258.mnm new file mode 100755 index 00000000..05c58fd0 Binary files /dev/null and b/system/ep3/map00000258.mnm differ diff --git a/system/ep3/map0000026C.mnm b/system/ep3/map0000026C.mnm new file mode 100755 index 00000000..3eb267af Binary files /dev/null and b/system/ep3/map0000026C.mnm differ diff --git a/system/ep3/map00000280.mnm b/system/ep3/map00000280.mnm new file mode 100755 index 00000000..318de8bc Binary files /dev/null and b/system/ep3/map00000280.mnm differ diff --git a/system/ep3/map00000294.mnm b/system/ep3/map00000294.mnm new file mode 100755 index 00000000..44556451 Binary files /dev/null and b/system/ep3/map00000294.mnm differ diff --git a/system/ep3/map000002A8.mnm b/system/ep3/map000002A8.mnm new file mode 100755 index 00000000..52e241d8 Binary files /dev/null and b/system/ep3/map000002A8.mnm differ diff --git a/system/ep3/map000002BC.mnm b/system/ep3/map000002BC.mnm new file mode 100755 index 00000000..5e81eeec Binary files /dev/null and b/system/ep3/map000002BC.mnm differ diff --git a/system/ep3/map000002BD.mnm b/system/ep3/map000002BD.mnm new file mode 100755 index 00000000..3efc895c Binary files /dev/null and b/system/ep3/map000002BD.mnm differ diff --git a/system/ep3/map000002E4.mnm b/system/ep3/map000002E4.mnm new file mode 100755 index 00000000..85d367d0 Binary files /dev/null and b/system/ep3/map000002E4.mnm differ diff --git a/system/ep3/map000002F8.mnm b/system/ep3/map000002F8.mnm new file mode 100755 index 00000000..c7e73b4d Binary files /dev/null and b/system/ep3/map000002F8.mnm differ diff --git a/system/ep3/map000002F9.mnm b/system/ep3/map000002F9.mnm new file mode 100755 index 00000000..8ddd6110 Binary files /dev/null and b/system/ep3/map000002F9.mnm differ diff --git a/system/ep3/map00000320.mnm b/system/ep3/map00000320.mnm new file mode 100755 index 00000000..5868c1a2 Binary files /dev/null and b/system/ep3/map00000320.mnm differ diff --git a/system/ep3/map00000334.mnm b/system/ep3/map00000334.mnm new file mode 100755 index 00000000..206c0c51 Binary files /dev/null and b/system/ep3/map00000334.mnm differ diff --git a/system/ep3/maplist.mnr b/system/ep3/maplist.mnr new file mode 100755 index 00000000..30cf3f96 Binary files /dev/null and b/system/ep3/maplist.mnr differ diff --git a/system/quests/b001-bb.bin b/system/quests/b001-bb.bin new file mode 100755 index 00000000..4fd7b26a Binary files /dev/null and b/system/quests/b001-bb.bin differ diff --git a/system/quests/b001-bb.dat b/system/quests/b001-bb.dat new file mode 100755 index 00000000..673a2621 Binary files /dev/null and b/system/quests/b001-bb.dat differ diff --git a/system/quests/b001-dc.bin b/system/quests/b001-dc.bin new file mode 100755 index 00000000..436c9d65 Binary files /dev/null and b/system/quests/b001-dc.bin differ diff --git a/system/quests/b001-dc.dat b/system/quests/b001-dc.dat new file mode 100755 index 00000000..673a2621 Binary files /dev/null and b/system/quests/b001-dc.dat differ diff --git a/system/quests/b001-gc.bin b/system/quests/b001-gc.bin new file mode 100755 index 00000000..c5b2f8c8 Binary files /dev/null and b/system/quests/b001-gc.bin differ diff --git a/system/quests/b001-gc.dat b/system/quests/b001-gc.dat new file mode 100755 index 00000000..673a2621 Binary files /dev/null and b/system/quests/b001-gc.dat differ diff --git a/system/quests/b001-pc.bin b/system/quests/b001-pc.bin new file mode 100755 index 00000000..4913c94e Binary files /dev/null and b/system/quests/b001-pc.bin differ diff --git a/system/quests/b001-pc.dat b/system/quests/b001-pc.dat new file mode 100755 index 00000000..253405dd Binary files /dev/null and b/system/quests/b001-pc.dat differ diff --git a/system/quests/b002-bb.bin b/system/quests/b002-bb.bin new file mode 100755 index 00000000..eddead3a Binary files /dev/null and b/system/quests/b002-bb.bin differ diff --git a/system/quests/b002-bb.dat b/system/quests/b002-bb.dat new file mode 100755 index 00000000..8e9a7dc5 Binary files /dev/null and b/system/quests/b002-bb.dat differ diff --git a/system/quests/b002-dc.bin b/system/quests/b002-dc.bin new file mode 100755 index 00000000..875f6344 Binary files /dev/null and b/system/quests/b002-dc.bin differ diff --git a/system/quests/b002-dc.dat b/system/quests/b002-dc.dat new file mode 100755 index 00000000..12925437 Binary files /dev/null and b/system/quests/b002-dc.dat differ diff --git a/system/quests/b002-gc.bin b/system/quests/b002-gc.bin new file mode 100755 index 00000000..8c1e0401 Binary files /dev/null and b/system/quests/b002-gc.bin differ diff --git a/system/quests/b002-gc.dat b/system/quests/b002-gc.dat new file mode 100755 index 00000000..8e9a7dc5 Binary files /dev/null and b/system/quests/b002-gc.dat differ diff --git a/system/quests/b002-pc.bin b/system/quests/b002-pc.bin new file mode 100755 index 00000000..9c71fe8d Binary files /dev/null and b/system/quests/b002-pc.bin differ diff --git a/system/quests/b002-pc.dat b/system/quests/b002-pc.dat new file mode 100755 index 00000000..cdd43154 Binary files /dev/null and b/system/quests/b002-pc.dat differ diff --git a/system/quests/b003-bb.bin b/system/quests/b003-bb.bin new file mode 100755 index 00000000..aff67c72 Binary files /dev/null and b/system/quests/b003-bb.bin differ diff --git a/system/quests/b003-bb.dat b/system/quests/b003-bb.dat new file mode 100755 index 00000000..a6bd3c6e Binary files /dev/null and b/system/quests/b003-bb.dat differ diff --git a/system/quests/b003-dc.bin b/system/quests/b003-dc.bin new file mode 100755 index 00000000..117eff78 Binary files /dev/null and b/system/quests/b003-dc.bin differ diff --git a/system/quests/b003-dc.dat b/system/quests/b003-dc.dat new file mode 100755 index 00000000..a6bd3c6e Binary files /dev/null and b/system/quests/b003-dc.dat differ diff --git a/system/quests/b003-gc.bin b/system/quests/b003-gc.bin new file mode 100755 index 00000000..22c64195 Binary files /dev/null and b/system/quests/b003-gc.bin differ diff --git a/system/quests/b003-gc.dat b/system/quests/b003-gc.dat new file mode 100755 index 00000000..a6bd3c6e Binary files /dev/null and b/system/quests/b003-gc.dat differ diff --git a/system/quests/b003-pc.bin b/system/quests/b003-pc.bin new file mode 100755 index 00000000..b1b7a3ec Binary files /dev/null and b/system/quests/b003-pc.bin differ diff --git a/system/quests/b003-pc.dat b/system/quests/b003-pc.dat new file mode 100755 index 00000000..faed944e Binary files /dev/null and b/system/quests/b003-pc.dat differ diff --git a/system/quests/b004-bb.bin b/system/quests/b004-bb.bin new file mode 100755 index 00000000..794361d6 Binary files /dev/null and b/system/quests/b004-bb.bin differ diff --git a/system/quests/b004-bb.dat b/system/quests/b004-bb.dat new file mode 100755 index 00000000..e838ab71 Binary files /dev/null and b/system/quests/b004-bb.dat differ diff --git a/system/quests/b004-dc.bin b/system/quests/b004-dc.bin new file mode 100755 index 00000000..ebed6eaa Binary files /dev/null and b/system/quests/b004-dc.bin differ diff --git a/system/quests/b004-dc.dat b/system/quests/b004-dc.dat new file mode 100755 index 00000000..e838ab71 Binary files /dev/null and b/system/quests/b004-dc.dat differ diff --git a/system/quests/b004-gc.bin b/system/quests/b004-gc.bin new file mode 100755 index 00000000..b04ce3e4 Binary files /dev/null and b/system/quests/b004-gc.bin differ diff --git a/system/quests/b004-gc.dat b/system/quests/b004-gc.dat new file mode 100755 index 00000000..e838ab71 Binary files /dev/null and b/system/quests/b004-gc.dat differ diff --git a/system/quests/b004-pc.bin b/system/quests/b004-pc.bin new file mode 100755 index 00000000..ce27f11c Binary files /dev/null and b/system/quests/b004-pc.bin differ diff --git a/system/quests/b004-pc.dat b/system/quests/b004-pc.dat new file mode 100755 index 00000000..e838ab71 Binary files /dev/null and b/system/quests/b004-pc.dat differ diff --git a/system/quests/b005-bb.bin b/system/quests/b005-bb.bin new file mode 100755 index 00000000..04c8635e Binary files /dev/null and b/system/quests/b005-bb.bin differ diff --git a/system/quests/b005-bb.dat b/system/quests/b005-bb.dat new file mode 100755 index 00000000..9be76bcd Binary files /dev/null and b/system/quests/b005-bb.dat differ diff --git a/system/quests/b005-dc.bin b/system/quests/b005-dc.bin new file mode 100755 index 00000000..a00b9818 Binary files /dev/null and b/system/quests/b005-dc.bin differ diff --git a/system/quests/b005-dc.dat b/system/quests/b005-dc.dat new file mode 100755 index 00000000..9be76bcd Binary files /dev/null and b/system/quests/b005-dc.dat differ diff --git a/system/quests/b005-gc.bin b/system/quests/b005-gc.bin new file mode 100755 index 00000000..247e8eb8 Binary files /dev/null and b/system/quests/b005-gc.bin differ diff --git a/system/quests/b005-gc.dat b/system/quests/b005-gc.dat new file mode 100755 index 00000000..9be76bcd Binary files /dev/null and b/system/quests/b005-gc.dat differ diff --git a/system/quests/b005-pc.bin b/system/quests/b005-pc.bin new file mode 100755 index 00000000..f191275e Binary files /dev/null and b/system/quests/b005-pc.bin differ diff --git a/system/quests/b005-pc.dat b/system/quests/b005-pc.dat new file mode 100755 index 00000000..9be76bcd Binary files /dev/null and b/system/quests/b005-pc.dat differ diff --git a/system/quests/b006-bb.bin b/system/quests/b006-bb.bin new file mode 100755 index 00000000..3f05c271 Binary files /dev/null and b/system/quests/b006-bb.bin differ diff --git a/system/quests/b006-bb.dat b/system/quests/b006-bb.dat new file mode 100755 index 00000000..a9c6bb0e Binary files /dev/null and b/system/quests/b006-bb.dat differ diff --git a/system/quests/b006-dc.bin b/system/quests/b006-dc.bin new file mode 100755 index 00000000..0e730c7a Binary files /dev/null and b/system/quests/b006-dc.bin differ diff --git a/system/quests/b006-dc.dat b/system/quests/b006-dc.dat new file mode 100755 index 00000000..a9c6bb0e Binary files /dev/null and b/system/quests/b006-dc.dat differ diff --git a/system/quests/b006-gc.bin b/system/quests/b006-gc.bin new file mode 100755 index 00000000..76ab7f5e Binary files /dev/null and b/system/quests/b006-gc.bin differ diff --git a/system/quests/b006-gc.dat b/system/quests/b006-gc.dat new file mode 100755 index 00000000..a9c6bb0e Binary files /dev/null and b/system/quests/b006-gc.dat differ diff --git a/system/quests/b006-pc.bin b/system/quests/b006-pc.bin new file mode 100755 index 00000000..84f435a1 Binary files /dev/null and b/system/quests/b006-pc.bin differ diff --git a/system/quests/b006-pc.dat b/system/quests/b006-pc.dat new file mode 100755 index 00000000..a9c6bb0e Binary files /dev/null and b/system/quests/b006-pc.dat differ diff --git a/system/quests/b007-bb.bin b/system/quests/b007-bb.bin new file mode 100755 index 00000000..608ab8e2 Binary files /dev/null and b/system/quests/b007-bb.bin differ diff --git a/system/quests/b007-bb.dat b/system/quests/b007-bb.dat new file mode 100755 index 00000000..0e78972b Binary files /dev/null and b/system/quests/b007-bb.dat differ diff --git a/system/quests/b007-dc.bin b/system/quests/b007-dc.bin new file mode 100755 index 00000000..45167425 Binary files /dev/null and b/system/quests/b007-dc.bin differ diff --git a/system/quests/b007-dc.dat b/system/quests/b007-dc.dat new file mode 100755 index 00000000..0e78972b Binary files /dev/null and b/system/quests/b007-dc.dat differ diff --git a/system/quests/b007-gc.bin b/system/quests/b007-gc.bin new file mode 100755 index 00000000..969db18f Binary files /dev/null and b/system/quests/b007-gc.bin differ diff --git a/system/quests/b007-gc.dat b/system/quests/b007-gc.dat new file mode 100755 index 00000000..0e78972b Binary files /dev/null and b/system/quests/b007-gc.dat differ diff --git a/system/quests/b007-pc.bin b/system/quests/b007-pc.bin new file mode 100755 index 00000000..2d208a57 Binary files /dev/null and b/system/quests/b007-pc.bin differ diff --git a/system/quests/b007-pc.dat b/system/quests/b007-pc.dat new file mode 100755 index 00000000..0e78972b Binary files /dev/null and b/system/quests/b007-pc.dat differ diff --git a/system/quests/b008-bb.bin b/system/quests/b008-bb.bin new file mode 100755 index 00000000..88b0f1c6 Binary files /dev/null and b/system/quests/b008-bb.bin differ diff --git a/system/quests/b008-bb.dat b/system/quests/b008-bb.dat new file mode 100755 index 00000000..4a559194 Binary files /dev/null and b/system/quests/b008-bb.dat differ diff --git a/system/quests/b008-dc.bin b/system/quests/b008-dc.bin new file mode 100755 index 00000000..aee2be1b Binary files /dev/null and b/system/quests/b008-dc.bin differ diff --git a/system/quests/b008-dc.dat b/system/quests/b008-dc.dat new file mode 100755 index 00000000..4a559194 Binary files /dev/null and b/system/quests/b008-dc.dat differ diff --git a/system/quests/b008-gc.bin b/system/quests/b008-gc.bin new file mode 100755 index 00000000..c6168479 Binary files /dev/null and b/system/quests/b008-gc.bin differ diff --git a/system/quests/b008-gc.dat b/system/quests/b008-gc.dat new file mode 100755 index 00000000..4a559194 Binary files /dev/null and b/system/quests/b008-gc.dat differ diff --git a/system/quests/b008-pc.bin b/system/quests/b008-pc.bin new file mode 100755 index 00000000..24106e7b Binary files /dev/null and b/system/quests/b008-pc.bin differ diff --git a/system/quests/b008-pc.dat b/system/quests/b008-pc.dat new file mode 100755 index 00000000..4a559194 Binary files /dev/null and b/system/quests/b008-pc.dat differ diff --git a/system/quests/c101-bb.bin b/system/quests/c101-bb.bin new file mode 100755 index 00000000..d37a8d61 Binary files /dev/null and b/system/quests/c101-bb.bin differ diff --git a/system/quests/c101-bb.dat b/system/quests/c101-bb.dat new file mode 100755 index 00000000..f022c752 Binary files /dev/null and b/system/quests/c101-bb.dat differ diff --git a/system/quests/c101-dc.bin b/system/quests/c101-dc.bin new file mode 100755 index 00000000..86d06496 Binary files /dev/null and b/system/quests/c101-dc.bin differ diff --git a/system/quests/c101-dc.dat b/system/quests/c101-dc.dat new file mode 100755 index 00000000..da32cc91 Binary files /dev/null and b/system/quests/c101-dc.dat differ diff --git a/system/quests/c101-gc.bin b/system/quests/c101-gc.bin new file mode 100755 index 00000000..6844de9f Binary files /dev/null and b/system/quests/c101-gc.bin differ diff --git a/system/quests/c101-gc.dat b/system/quests/c101-gc.dat new file mode 100755 index 00000000..da32cc91 Binary files /dev/null and b/system/quests/c101-gc.dat differ diff --git a/system/quests/c101-pc.bin b/system/quests/c101-pc.bin new file mode 100755 index 00000000..57aa3e32 Binary files /dev/null and b/system/quests/c101-pc.bin differ diff --git a/system/quests/c101-pc.dat b/system/quests/c101-pc.dat new file mode 100755 index 00000000..da32cc91 Binary files /dev/null and b/system/quests/c101-pc.dat differ diff --git a/system/quests/c102-bb.bin b/system/quests/c102-bb.bin new file mode 100755 index 00000000..54c5eb16 Binary files /dev/null and b/system/quests/c102-bb.bin differ diff --git a/system/quests/c102-bb.dat b/system/quests/c102-bb.dat new file mode 100755 index 00000000..3d34142f Binary files /dev/null and b/system/quests/c102-bb.dat differ diff --git a/system/quests/c102-dc.bin b/system/quests/c102-dc.bin new file mode 100755 index 00000000..eedd5c97 Binary files /dev/null and b/system/quests/c102-dc.bin differ diff --git a/system/quests/c102-dc.dat b/system/quests/c102-dc.dat new file mode 100755 index 00000000..bfe05e0f Binary files /dev/null and b/system/quests/c102-dc.dat differ diff --git a/system/quests/c102-gc.bin b/system/quests/c102-gc.bin new file mode 100755 index 00000000..387a61a2 Binary files /dev/null and b/system/quests/c102-gc.bin differ diff --git a/system/quests/c102-gc.dat b/system/quests/c102-gc.dat new file mode 100755 index 00000000..bfe05e0f Binary files /dev/null and b/system/quests/c102-gc.dat differ diff --git a/system/quests/c102-pc.bin b/system/quests/c102-pc.bin new file mode 100755 index 00000000..29f5a42b Binary files /dev/null and b/system/quests/c102-pc.bin differ diff --git a/system/quests/c102-pc.dat b/system/quests/c102-pc.dat new file mode 100755 index 00000000..bfe05e0f Binary files /dev/null and b/system/quests/c102-pc.dat differ diff --git a/system/quests/c103-bb.bin b/system/quests/c103-bb.bin new file mode 100755 index 00000000..daffdbbc Binary files /dev/null and b/system/quests/c103-bb.bin differ diff --git a/system/quests/c103-bb.dat b/system/quests/c103-bb.dat new file mode 100755 index 00000000..a576342d Binary files /dev/null and b/system/quests/c103-bb.dat differ diff --git a/system/quests/c103-dc.bin b/system/quests/c103-dc.bin new file mode 100755 index 00000000..cfad890b Binary files /dev/null and b/system/quests/c103-dc.bin differ diff --git a/system/quests/c103-dc.dat b/system/quests/c103-dc.dat new file mode 100755 index 00000000..9e0aa601 Binary files /dev/null and b/system/quests/c103-dc.dat differ diff --git a/system/quests/c103-gc.bin b/system/quests/c103-gc.bin new file mode 100755 index 00000000..86a53465 Binary files /dev/null and b/system/quests/c103-gc.bin differ diff --git a/system/quests/c103-gc.dat b/system/quests/c103-gc.dat new file mode 100755 index 00000000..9e0aa601 Binary files /dev/null and b/system/quests/c103-gc.dat differ diff --git a/system/quests/c103-pc.bin b/system/quests/c103-pc.bin new file mode 100755 index 00000000..c4ccc060 Binary files /dev/null and b/system/quests/c103-pc.bin differ diff --git a/system/quests/c103-pc.dat b/system/quests/c103-pc.dat new file mode 100755 index 00000000..9e0aa601 Binary files /dev/null and b/system/quests/c103-pc.dat differ diff --git a/system/quests/c104-bb.bin b/system/quests/c104-bb.bin new file mode 100755 index 00000000..3dffcb5f Binary files /dev/null and b/system/quests/c104-bb.bin differ diff --git a/system/quests/c104-bb.dat b/system/quests/c104-bb.dat new file mode 100755 index 00000000..7cf52ba0 Binary files /dev/null and b/system/quests/c104-bb.dat differ diff --git a/system/quests/c104-dc.bin b/system/quests/c104-dc.bin new file mode 100755 index 00000000..1bd34d5b Binary files /dev/null and b/system/quests/c104-dc.bin differ diff --git a/system/quests/c104-dc.dat b/system/quests/c104-dc.dat new file mode 100755 index 00000000..cb3b3c82 Binary files /dev/null and b/system/quests/c104-dc.dat differ diff --git a/system/quests/c104-gc.bin b/system/quests/c104-gc.bin new file mode 100755 index 00000000..93a6b6c5 Binary files /dev/null and b/system/quests/c104-gc.bin differ diff --git a/system/quests/c104-gc.dat b/system/quests/c104-gc.dat new file mode 100755 index 00000000..02319552 Binary files /dev/null and b/system/quests/c104-gc.dat differ diff --git a/system/quests/c104-pc.bin b/system/quests/c104-pc.bin new file mode 100755 index 00000000..2eb68a34 Binary files /dev/null and b/system/quests/c104-pc.bin differ diff --git a/system/quests/c104-pc.dat b/system/quests/c104-pc.dat new file mode 100755 index 00000000..cb3b3c82 Binary files /dev/null and b/system/quests/c104-pc.dat differ diff --git a/system/quests/c105-bb.bin b/system/quests/c105-bb.bin new file mode 100755 index 00000000..42ea65e7 Binary files /dev/null and b/system/quests/c105-bb.bin differ diff --git a/system/quests/c105-bb.dat b/system/quests/c105-bb.dat new file mode 100755 index 00000000..e0c68d3b Binary files /dev/null and b/system/quests/c105-bb.dat differ diff --git a/system/quests/c105-dc.bin b/system/quests/c105-dc.bin new file mode 100755 index 00000000..7bd7dfda Binary files /dev/null and b/system/quests/c105-dc.bin differ diff --git a/system/quests/c105-dc.dat b/system/quests/c105-dc.dat new file mode 100755 index 00000000..59d4600a Binary files /dev/null and b/system/quests/c105-dc.dat differ diff --git a/system/quests/c105-gc.bin b/system/quests/c105-gc.bin new file mode 100755 index 00000000..7cc05aa4 Binary files /dev/null and b/system/quests/c105-gc.bin differ diff --git a/system/quests/c105-gc.dat b/system/quests/c105-gc.dat new file mode 100755 index 00000000..59d4600a Binary files /dev/null and b/system/quests/c105-gc.dat differ diff --git a/system/quests/c105-pc.bin b/system/quests/c105-pc.bin new file mode 100755 index 00000000..be4b740e Binary files /dev/null and b/system/quests/c105-pc.bin differ diff --git a/system/quests/c105-pc.dat b/system/quests/c105-pc.dat new file mode 100755 index 00000000..59d4600a Binary files /dev/null and b/system/quests/c105-pc.dat differ diff --git a/system/quests/c106-bb.bin b/system/quests/c106-bb.bin new file mode 100755 index 00000000..dd360bb8 Binary files /dev/null and b/system/quests/c106-bb.bin differ diff --git a/system/quests/c106-bb.dat b/system/quests/c106-bb.dat new file mode 100755 index 00000000..6f02e782 Binary files /dev/null and b/system/quests/c106-bb.dat differ diff --git a/system/quests/c106-dc.bin b/system/quests/c106-dc.bin new file mode 100755 index 00000000..b9d4c463 Binary files /dev/null and b/system/quests/c106-dc.bin differ diff --git a/system/quests/c106-dc.dat b/system/quests/c106-dc.dat new file mode 100755 index 00000000..009ee52e Binary files /dev/null and b/system/quests/c106-dc.dat differ diff --git a/system/quests/c106-gc.bin b/system/quests/c106-gc.bin new file mode 100755 index 00000000..5dcd4bd5 Binary files /dev/null and b/system/quests/c106-gc.bin differ diff --git a/system/quests/c106-gc.dat b/system/quests/c106-gc.dat new file mode 100755 index 00000000..009ee52e Binary files /dev/null and b/system/quests/c106-gc.dat differ diff --git a/system/quests/c106-pc.bin b/system/quests/c106-pc.bin new file mode 100755 index 00000000..c1d4de1f Binary files /dev/null and b/system/quests/c106-pc.bin differ diff --git a/system/quests/c106-pc.dat b/system/quests/c106-pc.dat new file mode 100755 index 00000000..009ee52e Binary files /dev/null and b/system/quests/c106-pc.dat differ diff --git a/system/quests/c107-bb.bin b/system/quests/c107-bb.bin new file mode 100755 index 00000000..01023078 Binary files /dev/null and b/system/quests/c107-bb.bin differ diff --git a/system/quests/c107-bb.dat b/system/quests/c107-bb.dat new file mode 100755 index 00000000..84d5d9bc Binary files /dev/null and b/system/quests/c107-bb.dat differ diff --git a/system/quests/c107-dc.bin b/system/quests/c107-dc.bin new file mode 100755 index 00000000..333f78ab Binary files /dev/null and b/system/quests/c107-dc.bin differ diff --git a/system/quests/c107-dc.dat b/system/quests/c107-dc.dat new file mode 100755 index 00000000..7a782bda Binary files /dev/null and b/system/quests/c107-dc.dat differ diff --git a/system/quests/c107-gc.bin b/system/quests/c107-gc.bin new file mode 100755 index 00000000..35b75cfe Binary files /dev/null and b/system/quests/c107-gc.bin differ diff --git a/system/quests/c107-gc.dat b/system/quests/c107-gc.dat new file mode 100755 index 00000000..7a782bda Binary files /dev/null and b/system/quests/c107-gc.dat differ diff --git a/system/quests/c107-pc.bin b/system/quests/c107-pc.bin new file mode 100755 index 00000000..47cbd18c Binary files /dev/null and b/system/quests/c107-pc.bin differ diff --git a/system/quests/c107-pc.dat b/system/quests/c107-pc.dat new file mode 100755 index 00000000..7a782bda Binary files /dev/null and b/system/quests/c107-pc.dat differ diff --git a/system/quests/c108-bb.bin b/system/quests/c108-bb.bin new file mode 100755 index 00000000..0f13f6f9 Binary files /dev/null and b/system/quests/c108-bb.bin differ diff --git a/system/quests/c108-bb.dat b/system/quests/c108-bb.dat new file mode 100755 index 00000000..df95a046 Binary files /dev/null and b/system/quests/c108-bb.dat differ diff --git a/system/quests/c108-dc.bin b/system/quests/c108-dc.bin new file mode 100755 index 00000000..219b5db5 Binary files /dev/null and b/system/quests/c108-dc.bin differ diff --git a/system/quests/c108-dc.dat b/system/quests/c108-dc.dat new file mode 100755 index 00000000..671aa722 Binary files /dev/null and b/system/quests/c108-dc.dat differ diff --git a/system/quests/c108-gc.bin b/system/quests/c108-gc.bin new file mode 100755 index 00000000..caa28096 Binary files /dev/null and b/system/quests/c108-gc.bin differ diff --git a/system/quests/c108-gc.dat b/system/quests/c108-gc.dat new file mode 100755 index 00000000..671aa722 Binary files /dev/null and b/system/quests/c108-gc.dat differ diff --git a/system/quests/c108-pc.bin b/system/quests/c108-pc.bin new file mode 100755 index 00000000..ab93b6fe Binary files /dev/null and b/system/quests/c108-pc.bin differ diff --git a/system/quests/c108-pc.dat b/system/quests/c108-pc.dat new file mode 100755 index 00000000..671aa722 Binary files /dev/null and b/system/quests/c108-pc.dat differ diff --git a/system/quests/c109-bb.bin b/system/quests/c109-bb.bin new file mode 100755 index 00000000..1d121f84 Binary files /dev/null and b/system/quests/c109-bb.bin differ diff --git a/system/quests/c109-bb.dat b/system/quests/c109-bb.dat new file mode 100755 index 00000000..e2286347 Binary files /dev/null and b/system/quests/c109-bb.dat differ diff --git a/system/quests/c109-dc.bin b/system/quests/c109-dc.bin new file mode 100755 index 00000000..9ab4ca28 Binary files /dev/null and b/system/quests/c109-dc.bin differ diff --git a/system/quests/c109-dc.dat b/system/quests/c109-dc.dat new file mode 100755 index 00000000..cf7c4fb4 Binary files /dev/null and b/system/quests/c109-dc.dat differ diff --git a/system/quests/c109-gc.bin b/system/quests/c109-gc.bin new file mode 100755 index 00000000..9f5b04b3 Binary files /dev/null and b/system/quests/c109-gc.bin differ diff --git a/system/quests/c109-gc.dat b/system/quests/c109-gc.dat new file mode 100755 index 00000000..cf7c4fb4 Binary files /dev/null and b/system/quests/c109-gc.dat differ diff --git a/system/quests/c109-pc.bin b/system/quests/c109-pc.bin new file mode 100755 index 00000000..9420a530 Binary files /dev/null and b/system/quests/c109-pc.bin differ diff --git a/system/quests/c109-pc.dat b/system/quests/c109-pc.dat new file mode 100755 index 00000000..e338ddef Binary files /dev/null and b/system/quests/c109-pc.dat differ diff --git a/system/quests/c201-bb.bin b/system/quests/c201-bb.bin new file mode 100755 index 00000000..aa76f21b Binary files /dev/null and b/system/quests/c201-bb.bin differ diff --git a/system/quests/c201-bb.dat b/system/quests/c201-bb.dat new file mode 100755 index 00000000..93af348a Binary files /dev/null and b/system/quests/c201-bb.dat differ diff --git a/system/quests/c201-gc.bin b/system/quests/c201-gc.bin new file mode 100755 index 00000000..9ab38c1d Binary files /dev/null and b/system/quests/c201-gc.bin differ diff --git a/system/quests/c201-gc.dat b/system/quests/c201-gc.dat new file mode 100755 index 00000000..3a82a16e Binary files /dev/null and b/system/quests/c201-gc.dat differ diff --git a/system/quests/c202-bb.bin b/system/quests/c202-bb.bin new file mode 100755 index 00000000..e2dfd1be Binary files /dev/null and b/system/quests/c202-bb.bin differ diff --git a/system/quests/c202-bb.dat b/system/quests/c202-bb.dat new file mode 100755 index 00000000..a3d9ad80 Binary files /dev/null and b/system/quests/c202-bb.dat differ diff --git a/system/quests/c202-gc.bin b/system/quests/c202-gc.bin new file mode 100755 index 00000000..32cf6a2c Binary files /dev/null and b/system/quests/c202-gc.bin differ diff --git a/system/quests/c202-gc.dat b/system/quests/c202-gc.dat new file mode 100755 index 00000000..52c56166 Binary files /dev/null and b/system/quests/c202-gc.dat differ diff --git a/system/quests/c203-bb.bin b/system/quests/c203-bb.bin new file mode 100755 index 00000000..0c1b1be6 Binary files /dev/null and b/system/quests/c203-bb.bin differ diff --git a/system/quests/c203-bb.dat b/system/quests/c203-bb.dat new file mode 100755 index 00000000..34f129b7 Binary files /dev/null and b/system/quests/c203-bb.dat differ diff --git a/system/quests/c203-gc.bin b/system/quests/c203-gc.bin new file mode 100755 index 00000000..5e34da23 Binary files /dev/null and b/system/quests/c203-gc.bin differ diff --git a/system/quests/c203-gc.dat b/system/quests/c203-gc.dat new file mode 100755 index 00000000..0c50a7d6 Binary files /dev/null and b/system/quests/c203-gc.dat differ diff --git a/system/quests/c204-bb.bin b/system/quests/c204-bb.bin new file mode 100755 index 00000000..7a088c59 Binary files /dev/null and b/system/quests/c204-bb.bin differ diff --git a/system/quests/c204-bb.dat b/system/quests/c204-bb.dat new file mode 100755 index 00000000..c2b14f0f Binary files /dev/null and b/system/quests/c204-bb.dat differ diff --git a/system/quests/c204-gc.bin b/system/quests/c204-gc.bin new file mode 100755 index 00000000..f551d108 Binary files /dev/null and b/system/quests/c204-gc.bin differ diff --git a/system/quests/c204-gc.dat b/system/quests/c204-gc.dat new file mode 100755 index 00000000..44401162 Binary files /dev/null and b/system/quests/c204-gc.dat differ diff --git a/system/quests/c205-bb.bin b/system/quests/c205-bb.bin new file mode 100755 index 00000000..9fd8a09b Binary files /dev/null and b/system/quests/c205-bb.bin differ diff --git a/system/quests/c205-bb.dat b/system/quests/c205-bb.dat new file mode 100755 index 00000000..33104ac2 Binary files /dev/null and b/system/quests/c205-bb.dat differ diff --git a/system/quests/c205-gc.bin b/system/quests/c205-gc.bin new file mode 100755 index 00000000..66d6d884 Binary files /dev/null and b/system/quests/c205-gc.bin differ diff --git a/system/quests/c205-gc.dat b/system/quests/c205-gc.dat new file mode 100755 index 00000000..41080242 Binary files /dev/null and b/system/quests/c205-gc.dat differ diff --git a/system/quests/e301-gc3.bin b/system/quests/e301-gc3.bin new file mode 100755 index 00000000..1e3f6113 Binary files /dev/null and b/system/quests/e301-gc3.bin differ diff --git a/system/quests/e302-gc3.bin b/system/quests/e302-gc3.bin new file mode 100755 index 00000000..7c84a9af Binary files /dev/null and b/system/quests/e302-gc3.bin differ diff --git a/system/quests/e303-gc3.bin b/system/quests/e303-gc3.bin new file mode 100755 index 00000000..cf364598 Binary files /dev/null and b/system/quests/e303-gc3.bin differ diff --git a/system/quests/nights_e.gba b/system/quests/nights_e.gba new file mode 100755 index 00000000..63244c7e Binary files /dev/null and b/system/quests/nights_e.gba differ diff --git a/system/quests/puyo_e.gba b/system/quests/puyo_e.gba new file mode 100755 index 00000000..6112904b Binary files /dev/null and b/system/quests/puyo_e.gba differ diff --git a/system/quests/q000-dl-gc.bin b/system/quests/q000-dl-gc.bin new file mode 100755 index 00000000..4616dd56 Binary files /dev/null and b/system/quests/q000-dl-gc.bin differ diff --git a/system/quests/q000-dl-gc.dat b/system/quests/q000-dl-gc.dat new file mode 100755 index 00000000..31926a6c Binary files /dev/null and b/system/quests/q000-dl-gc.dat differ diff --git a/system/quests/q001-1p-bb.bin b/system/quests/q001-1p-bb.bin new file mode 100755 index 00000000..3f2c5ae4 Binary files /dev/null and b/system/quests/q001-1p-bb.bin differ diff --git a/system/quests/q001-1p-bb.dat b/system/quests/q001-1p-bb.dat new file mode 100755 index 00000000..cfff544f Binary files /dev/null and b/system/quests/q001-1p-bb.dat differ diff --git a/system/quests/q002-1p-bb.bin b/system/quests/q002-1p-bb.bin new file mode 100755 index 00000000..9c3cff0d Binary files /dev/null and b/system/quests/q002-1p-bb.bin differ diff --git a/system/quests/q002-1p-bb.dat b/system/quests/q002-1p-bb.dat new file mode 100755 index 00000000..3f339517 Binary files /dev/null and b/system/quests/q002-1p-bb.dat differ diff --git a/system/quests/q003-1p-bb.bin b/system/quests/q003-1p-bb.bin new file mode 100755 index 00000000..176c501e Binary files /dev/null and b/system/quests/q003-1p-bb.bin differ diff --git a/system/quests/q003-1p-bb.dat b/system/quests/q003-1p-bb.dat new file mode 100755 index 00000000..c54407d2 Binary files /dev/null and b/system/quests/q003-1p-bb.dat differ diff --git a/system/quests/q004-1p-bb.bin b/system/quests/q004-1p-bb.bin new file mode 100755 index 00000000..e2a35768 Binary files /dev/null and b/system/quests/q004-1p-bb.bin differ diff --git a/system/quests/q004-1p-bb.dat b/system/quests/q004-1p-bb.dat new file mode 100755 index 00000000..489a7100 Binary files /dev/null and b/system/quests/q004-1p-bb.dat differ diff --git a/system/quests/q005-1p-bb.bin b/system/quests/q005-1p-bb.bin new file mode 100755 index 00000000..d960b3d0 Binary files /dev/null and b/system/quests/q005-1p-bb.bin differ diff --git a/system/quests/q005-1p-bb.dat b/system/quests/q005-1p-bb.dat new file mode 100755 index 00000000..1a593bcf Binary files /dev/null and b/system/quests/q005-1p-bb.dat differ diff --git a/system/quests/q006-1p-bb.bin b/system/quests/q006-1p-bb.bin new file mode 100755 index 00000000..0cebd1ed Binary files /dev/null and b/system/quests/q006-1p-bb.bin differ diff --git a/system/quests/q006-1p-bb.dat b/system/quests/q006-1p-bb.dat new file mode 100755 index 00000000..8457356d Binary files /dev/null and b/system/quests/q006-1p-bb.dat differ diff --git a/system/quests/q007-1p-bb.bin b/system/quests/q007-1p-bb.bin new file mode 100755 index 00000000..9e7ebe52 Binary files /dev/null and b/system/quests/q007-1p-bb.bin differ diff --git a/system/quests/q007-1p-bb.dat b/system/quests/q007-1p-bb.dat new file mode 100755 index 00000000..34460468 Binary files /dev/null and b/system/quests/q007-1p-bb.dat differ diff --git a/system/quests/q008-1p-bb.bin b/system/quests/q008-1p-bb.bin new file mode 100755 index 00000000..2fa6d038 Binary files /dev/null and b/system/quests/q008-1p-bb.bin differ diff --git a/system/quests/q008-1p-bb.dat b/system/quests/q008-1p-bb.dat new file mode 100755 index 00000000..5c6d6456 Binary files /dev/null and b/system/quests/q008-1p-bb.dat differ diff --git a/system/quests/q009-1p-bb.bin b/system/quests/q009-1p-bb.bin new file mode 100755 index 00000000..021c4f75 Binary files /dev/null and b/system/quests/q009-1p-bb.bin differ diff --git a/system/quests/q009-1p-bb.dat b/system/quests/q009-1p-bb.dat new file mode 100755 index 00000000..50e3ef2a Binary files /dev/null and b/system/quests/q009-1p-bb.dat differ diff --git a/system/quests/q010-1p-bb.bin b/system/quests/q010-1p-bb.bin new file mode 100755 index 00000000..4bff7640 Binary files /dev/null and b/system/quests/q010-1p-bb.bin differ diff --git a/system/quests/q010-1p-bb.dat b/system/quests/q010-1p-bb.dat new file mode 100755 index 00000000..f66d72ba Binary files /dev/null and b/system/quests/q010-1p-bb.dat differ diff --git a/system/quests/q011-1p-bb.bin b/system/quests/q011-1p-bb.bin new file mode 100755 index 00000000..6bcb4063 Binary files /dev/null and b/system/quests/q011-1p-bb.bin differ diff --git a/system/quests/q011-1p-bb.dat b/system/quests/q011-1p-bb.dat new file mode 100755 index 00000000..b6b02ac3 Binary files /dev/null and b/system/quests/q011-1p-bb.dat differ diff --git a/system/quests/q012-1p-bb.bin b/system/quests/q012-1p-bb.bin new file mode 100755 index 00000000..ec9db572 Binary files /dev/null and b/system/quests/q012-1p-bb.bin differ diff --git a/system/quests/q012-1p-bb.dat b/system/quests/q012-1p-bb.dat new file mode 100755 index 00000000..5d10ec87 Binary files /dev/null and b/system/quests/q012-1p-bb.dat differ diff --git a/system/quests/q013-1p-bb.bin b/system/quests/q013-1p-bb.bin new file mode 100755 index 00000000..805bee4f Binary files /dev/null and b/system/quests/q013-1p-bb.bin differ diff --git a/system/quests/q013-1p-bb.dat b/system/quests/q013-1p-bb.dat new file mode 100755 index 00000000..ea254e7e Binary files /dev/null and b/system/quests/q013-1p-bb.dat differ diff --git a/system/quests/q014-1p-bb.bin b/system/quests/q014-1p-bb.bin new file mode 100755 index 00000000..f9fa50a6 Binary files /dev/null and b/system/quests/q014-1p-bb.bin differ diff --git a/system/quests/q014-1p-bb.dat b/system/quests/q014-1p-bb.dat new file mode 100755 index 00000000..8760c13c Binary files /dev/null and b/system/quests/q014-1p-bb.dat differ diff --git a/system/quests/q015-1p-bb.bin b/system/quests/q015-1p-bb.bin new file mode 100755 index 00000000..c35097f8 Binary files /dev/null and b/system/quests/q015-1p-bb.bin differ diff --git a/system/quests/q015-1p-bb.dat b/system/quests/q015-1p-bb.dat new file mode 100755 index 00000000..75243dfa Binary files /dev/null and b/system/quests/q015-1p-bb.dat differ diff --git a/system/quests/q016-1p-bb.bin b/system/quests/q016-1p-bb.bin new file mode 100755 index 00000000..9a3d6ef6 Binary files /dev/null and b/system/quests/q016-1p-bb.bin differ diff --git a/system/quests/q016-1p-bb.dat b/system/quests/q016-1p-bb.dat new file mode 100755 index 00000000..cce2d8ff Binary files /dev/null and b/system/quests/q016-1p-bb.dat differ diff --git a/system/quests/q017-1p-bb.bin b/system/quests/q017-1p-bb.bin new file mode 100755 index 00000000..32f7798c Binary files /dev/null and b/system/quests/q017-1p-bb.bin differ diff --git a/system/quests/q017-1p-bb.dat b/system/quests/q017-1p-bb.dat new file mode 100755 index 00000000..3343a5f5 Binary files /dev/null and b/system/quests/q017-1p-bb.dat differ diff --git a/system/quests/q018-1p-bb.bin b/system/quests/q018-1p-bb.bin new file mode 100755 index 00000000..ae3f9378 Binary files /dev/null and b/system/quests/q018-1p-bb.bin differ diff --git a/system/quests/q018-1p-bb.dat b/system/quests/q018-1p-bb.dat new file mode 100755 index 00000000..34bf7a98 Binary files /dev/null and b/system/quests/q018-1p-bb.dat differ diff --git a/system/quests/q019-1p-bb.bin b/system/quests/q019-1p-bb.bin new file mode 100755 index 00000000..d0007a85 Binary files /dev/null and b/system/quests/q019-1p-bb.bin differ diff --git a/system/quests/q019-1p-bb.dat b/system/quests/q019-1p-bb.dat new file mode 100755 index 00000000..379bb7f6 Binary files /dev/null and b/system/quests/q019-1p-bb.dat differ diff --git a/system/quests/q020-1p-bb.bin b/system/quests/q020-1p-bb.bin new file mode 100755 index 00000000..3d4e84af Binary files /dev/null and b/system/quests/q020-1p-bb.bin differ diff --git a/system/quests/q020-1p-bb.dat b/system/quests/q020-1p-bb.dat new file mode 100755 index 00000000..d92e47b0 Binary files /dev/null and b/system/quests/q020-1p-bb.dat differ diff --git a/system/quests/q021-1p-bb.bin b/system/quests/q021-1p-bb.bin new file mode 100755 index 00000000..d42df820 Binary files /dev/null and b/system/quests/q021-1p-bb.bin differ diff --git a/system/quests/q021-1p-bb.dat b/system/quests/q021-1p-bb.dat new file mode 100755 index 00000000..c7b741b3 Binary files /dev/null and b/system/quests/q021-1p-bb.dat differ diff --git a/system/quests/q022-1p-bb.bin b/system/quests/q022-1p-bb.bin new file mode 100755 index 00000000..7d578f88 Binary files /dev/null and b/system/quests/q022-1p-bb.bin differ diff --git a/system/quests/q022-1p-bb.dat b/system/quests/q022-1p-bb.dat new file mode 100755 index 00000000..c9b65509 Binary files /dev/null and b/system/quests/q022-1p-bb.dat differ diff --git a/system/quests/q023-1p-bb.bin b/system/quests/q023-1p-bb.bin new file mode 100755 index 00000000..3525c8bc Binary files /dev/null and b/system/quests/q023-1p-bb.bin differ diff --git a/system/quests/q023-1p-bb.dat b/system/quests/q023-1p-bb.dat new file mode 100755 index 00000000..ceee4db1 Binary files /dev/null and b/system/quests/q023-1p-bb.dat differ diff --git a/system/quests/q024-1p-bb.bin b/system/quests/q024-1p-bb.bin new file mode 100755 index 00000000..da299fb7 Binary files /dev/null and b/system/quests/q024-1p-bb.bin differ diff --git a/system/quests/q024-1p-bb.dat b/system/quests/q024-1p-bb.dat new file mode 100755 index 00000000..0527d143 Binary files /dev/null and b/system/quests/q024-1p-bb.dat differ diff --git a/system/quests/q025-1p-bb.bin b/system/quests/q025-1p-bb.bin new file mode 100755 index 00000000..a227cf93 Binary files /dev/null and b/system/quests/q025-1p-bb.bin differ diff --git a/system/quests/q025-1p-bb.dat b/system/quests/q025-1p-bb.dat new file mode 100755 index 00000000..261f9e19 Binary files /dev/null and b/system/quests/q025-1p-bb.dat differ diff --git a/system/quests/q026-1p-bb.bin b/system/quests/q026-1p-bb.bin new file mode 100755 index 00000000..2c73ffed Binary files /dev/null and b/system/quests/q026-1p-bb.bin differ diff --git a/system/quests/q026-1p-bb.dat b/system/quests/q026-1p-bb.dat new file mode 100755 index 00000000..c220402e Binary files /dev/null and b/system/quests/q026-1p-bb.dat differ diff --git a/system/quests/q026-1p-gc.bin b/system/quests/q026-1p-gc.bin new file mode 100755 index 00000000..a9095eed Binary files /dev/null and b/system/quests/q026-1p-gc.bin differ diff --git a/system/quests/q026-1p-gc.dat b/system/quests/q026-1p-gc.dat new file mode 100755 index 00000000..8b9a5595 Binary files /dev/null and b/system/quests/q026-1p-gc.dat differ diff --git a/system/quests/q027-1p-bb.bin b/system/quests/q027-1p-bb.bin new file mode 100755 index 00000000..00c6d5bc Binary files /dev/null and b/system/quests/q027-1p-bb.bin differ diff --git a/system/quests/q027-1p-bb.dat b/system/quests/q027-1p-bb.dat new file mode 100755 index 00000000..d2866329 Binary files /dev/null and b/system/quests/q027-1p-bb.dat differ diff --git a/system/quests/q031-1p-bb.bin b/system/quests/q031-1p-bb.bin new file mode 100755 index 00000000..219b2510 Binary files /dev/null and b/system/quests/q031-1p-bb.bin differ diff --git a/system/quests/q031-1p-bb.dat b/system/quests/q031-1p-bb.dat new file mode 100755 index 00000000..ce437c5a Binary files /dev/null and b/system/quests/q031-1p-bb.dat differ diff --git a/system/quests/q033-1p-bb.bin b/system/quests/q033-1p-bb.bin new file mode 100755 index 00000000..3e6361d7 Binary files /dev/null and b/system/quests/q033-1p-bb.bin differ diff --git a/system/quests/q033-1p-bb.dat b/system/quests/q033-1p-bb.dat new file mode 100755 index 00000000..715d2937 Binary files /dev/null and b/system/quests/q033-1p-bb.dat differ diff --git a/system/quests/q058-ret-bb.bin b/system/quests/q058-ret-bb.bin new file mode 100755 index 00000000..2556a7b2 Binary files /dev/null and b/system/quests/q058-ret-bb.bin differ diff --git a/system/quests/q058-ret-bb.dat b/system/quests/q058-ret-bb.dat new file mode 100755 index 00000000..da08758a Binary files /dev/null and b/system/quests/q058-ret-bb.dat differ diff --git a/system/quests/q058-ret-dc.bin b/system/quests/q058-ret-dc.bin new file mode 100755 index 00000000..387ed19b Binary files /dev/null and b/system/quests/q058-ret-dc.bin differ diff --git a/system/quests/q058-ret-dc.dat b/system/quests/q058-ret-dc.dat new file mode 100755 index 00000000..69d113ce Binary files /dev/null and b/system/quests/q058-ret-dc.dat differ diff --git a/system/quests/q058-ret-dc1.bin b/system/quests/q058-ret-dc1.bin new file mode 100755 index 00000000..5c864687 Binary files /dev/null and b/system/quests/q058-ret-dc1.bin differ diff --git a/system/quests/q058-ret-dc1.dat b/system/quests/q058-ret-dc1.dat new file mode 100755 index 00000000..013e1bbc Binary files /dev/null and b/system/quests/q058-ret-dc1.dat differ diff --git a/system/quests/q058-ret-gc.bin b/system/quests/q058-ret-gc.bin new file mode 100755 index 00000000..7b851543 Binary files /dev/null and b/system/quests/q058-ret-gc.bin differ diff --git a/system/quests/q058-ret-gc.dat b/system/quests/q058-ret-gc.dat new file mode 100755 index 00000000..53775402 Binary files /dev/null and b/system/quests/q058-ret-gc.dat differ diff --git a/system/quests/q058-ret-pc.bin b/system/quests/q058-ret-pc.bin new file mode 100755 index 00000000..62f25a79 Binary files /dev/null and b/system/quests/q058-ret-pc.bin differ diff --git a/system/quests/q058-ret-pc.dat b/system/quests/q058-ret-pc.dat new file mode 100755 index 00000000..82d8d074 Binary files /dev/null and b/system/quests/q058-ret-pc.dat differ diff --git a/system/quests/q059-ret-bb.bin b/system/quests/q059-ret-bb.bin new file mode 100755 index 00000000..06a6e474 Binary files /dev/null and b/system/quests/q059-ret-bb.bin differ diff --git a/system/quests/q059-ret-bb.dat b/system/quests/q059-ret-bb.dat new file mode 100755 index 00000000..8ac1f7cf Binary files /dev/null and b/system/quests/q059-ret-bb.dat differ diff --git a/system/quests/q059-ret-dc.bin b/system/quests/q059-ret-dc.bin new file mode 100755 index 00000000..434b913c Binary files /dev/null and b/system/quests/q059-ret-dc.bin differ diff --git a/system/quests/q059-ret-dc.dat b/system/quests/q059-ret-dc.dat new file mode 100755 index 00000000..0edfada5 Binary files /dev/null and b/system/quests/q059-ret-dc.dat differ diff --git a/system/quests/q059-ret-dc1.bin b/system/quests/q059-ret-dc1.bin new file mode 100755 index 00000000..9eba3e49 Binary files /dev/null and b/system/quests/q059-ret-dc1.bin differ diff --git a/system/quests/q059-ret-dc1.dat b/system/quests/q059-ret-dc1.dat new file mode 100755 index 00000000..0edfada5 Binary files /dev/null and b/system/quests/q059-ret-dc1.dat differ diff --git a/system/quests/q059-ret-gc.bin b/system/quests/q059-ret-gc.bin new file mode 100755 index 00000000..1486cf7f Binary files /dev/null and b/system/quests/q059-ret-gc.bin differ diff --git a/system/quests/q059-ret-gc.dat b/system/quests/q059-ret-gc.dat new file mode 100755 index 00000000..ca7345ff Binary files /dev/null and b/system/quests/q059-ret-gc.dat differ diff --git a/system/quests/q059-ret-pc.bin b/system/quests/q059-ret-pc.bin new file mode 100755 index 00000000..5e6c3a1f Binary files /dev/null and b/system/quests/q059-ret-pc.bin differ diff --git a/system/quests/q059-ret-pc.dat b/system/quests/q059-ret-pc.dat new file mode 100755 index 00000000..7e42e938 Binary files /dev/null and b/system/quests/q059-ret-pc.dat differ diff --git a/system/quests/q060-ret-bb.bin b/system/quests/q060-ret-bb.bin new file mode 100755 index 00000000..0638ec32 Binary files /dev/null and b/system/quests/q060-ret-bb.bin differ diff --git a/system/quests/q060-ret-bb.dat b/system/quests/q060-ret-bb.dat new file mode 100755 index 00000000..5dac1498 Binary files /dev/null and b/system/quests/q060-ret-bb.dat differ diff --git a/system/quests/q060-ret-dc.bin b/system/quests/q060-ret-dc.bin new file mode 100755 index 00000000..9fdaf94c Binary files /dev/null and b/system/quests/q060-ret-dc.bin differ diff --git a/system/quests/q060-ret-dc.dat b/system/quests/q060-ret-dc.dat new file mode 100755 index 00000000..13249c8b Binary files /dev/null and b/system/quests/q060-ret-dc.dat differ diff --git a/system/quests/q060-ret-dc1.bin b/system/quests/q060-ret-dc1.bin new file mode 100755 index 00000000..39b9dad5 Binary files /dev/null and b/system/quests/q060-ret-dc1.bin differ diff --git a/system/quests/q060-ret-dc1.dat b/system/quests/q060-ret-dc1.dat new file mode 100755 index 00000000..13249c8b Binary files /dev/null and b/system/quests/q060-ret-dc1.dat differ diff --git a/system/quests/q060-ret-gc.bin b/system/quests/q060-ret-gc.bin new file mode 100755 index 00000000..3785d7f2 Binary files /dev/null and b/system/quests/q060-ret-gc.bin differ diff --git a/system/quests/q060-ret-gc.dat b/system/quests/q060-ret-gc.dat new file mode 100755 index 00000000..fcba9859 Binary files /dev/null and b/system/quests/q060-ret-gc.dat differ diff --git a/system/quests/q060-ret-pc.bin b/system/quests/q060-ret-pc.bin new file mode 100755 index 00000000..81767615 Binary files /dev/null and b/system/quests/q060-ret-pc.bin differ diff --git a/system/quests/q060-ret-pc.dat b/system/quests/q060-ret-pc.dat new file mode 100755 index 00000000..6e5e32f5 Binary files /dev/null and b/system/quests/q060-ret-pc.dat differ diff --git a/system/quests/q068-ret-dc.bin b/system/quests/q068-ret-dc.bin new file mode 100755 index 00000000..2aec8005 Binary files /dev/null and b/system/quests/q068-ret-dc.bin differ diff --git a/system/quests/q068-ret-dc.dat b/system/quests/q068-ret-dc.dat new file mode 100755 index 00000000..e3b55e90 Binary files /dev/null and b/system/quests/q068-ret-dc.dat differ diff --git a/system/quests/q068-ret-pc.bin b/system/quests/q068-ret-pc.bin new file mode 100755 index 00000000..8887c348 Binary files /dev/null and b/system/quests/q068-ret-pc.bin differ diff --git a/system/quests/q068-ret-pc.dat b/system/quests/q068-ret-pc.dat new file mode 100755 index 00000000..e3b55e90 Binary files /dev/null and b/system/quests/q068-ret-pc.dat differ diff --git a/system/quests/q073-evt-gc.bin b/system/quests/q073-evt-gc.bin new file mode 100755 index 00000000..d730cc5e Binary files /dev/null and b/system/quests/q073-evt-gc.bin differ diff --git a/system/quests/q073-evt-gc.dat b/system/quests/q073-evt-gc.dat new file mode 100755 index 00000000..c83325e7 Binary files /dev/null and b/system/quests/q073-evt-gc.dat differ diff --git a/system/quests/q073-evt-pc.bin b/system/quests/q073-evt-pc.bin new file mode 100755 index 00000000..86a883b8 Binary files /dev/null and b/system/quests/q073-evt-pc.bin differ diff --git a/system/quests/q073-evt-pc.dat b/system/quests/q073-evt-pc.dat new file mode 100755 index 00000000..c3f0dafc Binary files /dev/null and b/system/quests/q073-evt-pc.dat differ diff --git a/system/quests/q080-vr-dc.bin b/system/quests/q080-vr-dc.bin new file mode 100755 index 00000000..14354d3f Binary files /dev/null and b/system/quests/q080-vr-dc.bin differ diff --git a/system/quests/q080-vr-dc.dat b/system/quests/q080-vr-dc.dat new file mode 100755 index 00000000..46a7ae40 Binary files /dev/null and b/system/quests/q080-vr-dc.dat differ diff --git a/system/quests/q095-evt-gc.bin b/system/quests/q095-evt-gc.bin new file mode 100755 index 00000000..42996df9 Binary files /dev/null and b/system/quests/q095-evt-gc.bin differ diff --git a/system/quests/q095-evt-gc.dat b/system/quests/q095-evt-gc.dat new file mode 100755 index 00000000..0415ca39 Binary files /dev/null and b/system/quests/q095-evt-gc.dat differ diff --git a/system/quests/q095-evt-pc.bin b/system/quests/q095-evt-pc.bin new file mode 100755 index 00000000..c497c077 Binary files /dev/null and b/system/quests/q095-evt-pc.bin differ diff --git a/system/quests/q095-evt-pc.dat b/system/quests/q095-evt-pc.dat new file mode 100755 index 00000000..c9c6ac84 Binary files /dev/null and b/system/quests/q095-evt-pc.dat differ diff --git a/system/quests/q096-evt-gc.bin b/system/quests/q096-evt-gc.bin new file mode 100755 index 00000000..f912f54c Binary files /dev/null and b/system/quests/q096-evt-gc.bin differ diff --git a/system/quests/q096-evt-gc.dat b/system/quests/q096-evt-gc.dat new file mode 100755 index 00000000..5f3c23e2 Binary files /dev/null and b/system/quests/q096-evt-gc.dat differ diff --git a/system/quests/q096-evt-pc.bin b/system/quests/q096-evt-pc.bin new file mode 100755 index 00000000..cb93103a Binary files /dev/null and b/system/quests/q096-evt-pc.bin differ diff --git a/system/quests/q096-evt-pc.dat b/system/quests/q096-evt-pc.dat new file mode 100755 index 00000000..97fb9846 Binary files /dev/null and b/system/quests/q096-evt-pc.dat differ diff --git a/system/quests/q101-ext-bb.bin b/system/quests/q101-ext-bb.bin new file mode 100755 index 00000000..1d01033b Binary files /dev/null and b/system/quests/q101-ext-bb.bin differ diff --git a/system/quests/q101-ext-bb.dat b/system/quests/q101-ext-bb.dat new file mode 100755 index 00000000..7ff83231 Binary files /dev/null and b/system/quests/q101-ext-bb.dat differ diff --git a/system/quests/q101-ext-dc.bin b/system/quests/q101-ext-dc.bin new file mode 100755 index 00000000..42247daa Binary files /dev/null and b/system/quests/q101-ext-dc.bin differ diff --git a/system/quests/q101-ext-dc.dat b/system/quests/q101-ext-dc.dat new file mode 100755 index 00000000..795acebd Binary files /dev/null and b/system/quests/q101-ext-dc.dat differ diff --git a/system/quests/q101-ext-dc1.bin b/system/quests/q101-ext-dc1.bin new file mode 100755 index 00000000..877b8c0f Binary files /dev/null and b/system/quests/q101-ext-dc1.bin differ diff --git a/system/quests/q101-ext-dc1.dat b/system/quests/q101-ext-dc1.dat new file mode 100755 index 00000000..795acebd Binary files /dev/null and b/system/quests/q101-ext-dc1.dat differ diff --git a/system/quests/q101-ext-gc.bin b/system/quests/q101-ext-gc.bin new file mode 100755 index 00000000..fb8eaa07 Binary files /dev/null and b/system/quests/q101-ext-gc.bin differ diff --git a/system/quests/q101-ext-gc.dat b/system/quests/q101-ext-gc.dat new file mode 100755 index 00000000..6acac046 Binary files /dev/null and b/system/quests/q101-ext-gc.dat differ diff --git a/system/quests/q101-ext-pc.bin b/system/quests/q101-ext-pc.bin new file mode 100755 index 00000000..efc62dd8 Binary files /dev/null and b/system/quests/q101-ext-pc.bin differ diff --git a/system/quests/q101-ext-pc.dat b/system/quests/q101-ext-pc.dat new file mode 100755 index 00000000..795acebd Binary files /dev/null and b/system/quests/q101-ext-pc.dat differ diff --git a/system/quests/q102-ext-bb.bin b/system/quests/q102-ext-bb.bin new file mode 100755 index 00000000..a884bdfa Binary files /dev/null and b/system/quests/q102-ext-bb.bin differ diff --git a/system/quests/q102-ext-bb.dat b/system/quests/q102-ext-bb.dat new file mode 100755 index 00000000..74c68ef9 Binary files /dev/null and b/system/quests/q102-ext-bb.dat differ diff --git a/system/quests/q102-ext-dc.bin b/system/quests/q102-ext-dc.bin new file mode 100755 index 00000000..6a4c78fa Binary files /dev/null and b/system/quests/q102-ext-dc.bin differ diff --git a/system/quests/q102-ext-dc.dat b/system/quests/q102-ext-dc.dat new file mode 100755 index 00000000..88aa13d6 Binary files /dev/null and b/system/quests/q102-ext-dc.dat differ diff --git a/system/quests/q102-ext-dc1.bin b/system/quests/q102-ext-dc1.bin new file mode 100755 index 00000000..8f6d6a3e Binary files /dev/null and b/system/quests/q102-ext-dc1.bin differ diff --git a/system/quests/q102-ext-dc1.dat b/system/quests/q102-ext-dc1.dat new file mode 100755 index 00000000..df1bd0c3 Binary files /dev/null and b/system/quests/q102-ext-dc1.dat differ diff --git a/system/quests/q102-ext-gc.bin b/system/quests/q102-ext-gc.bin new file mode 100755 index 00000000..5f357be7 Binary files /dev/null and b/system/quests/q102-ext-gc.bin differ diff --git a/system/quests/q102-ext-gc.dat b/system/quests/q102-ext-gc.dat new file mode 100755 index 00000000..862a5977 Binary files /dev/null and b/system/quests/q102-ext-gc.dat differ diff --git a/system/quests/q102-ext-pc.bin b/system/quests/q102-ext-pc.bin new file mode 100755 index 00000000..2e7204f7 Binary files /dev/null and b/system/quests/q102-ext-pc.bin differ diff --git a/system/quests/q102-ext-pc.dat b/system/quests/q102-ext-pc.dat new file mode 100755 index 00000000..88aa13d6 Binary files /dev/null and b/system/quests/q102-ext-pc.dat differ diff --git a/system/quests/q103-ext-bb.bin b/system/quests/q103-ext-bb.bin new file mode 100755 index 00000000..c347a3f7 Binary files /dev/null and b/system/quests/q103-ext-bb.bin differ diff --git a/system/quests/q103-ext-bb.dat b/system/quests/q103-ext-bb.dat new file mode 100755 index 00000000..163fc98b Binary files /dev/null and b/system/quests/q103-ext-bb.dat differ diff --git a/system/quests/q103-ext-dc1.bin b/system/quests/q103-ext-dc1.bin new file mode 100755 index 00000000..a3038afc Binary files /dev/null and b/system/quests/q103-ext-dc1.bin differ diff --git a/system/quests/q103-ext-dc1.dat b/system/quests/q103-ext-dc1.dat new file mode 100755 index 00000000..d07f949c Binary files /dev/null and b/system/quests/q103-ext-dc1.dat differ diff --git a/system/quests/q103-ext-gc.bin b/system/quests/q103-ext-gc.bin new file mode 100755 index 00000000..5ca34259 Binary files /dev/null and b/system/quests/q103-ext-gc.bin differ diff --git a/system/quests/q103-ext-gc.dat b/system/quests/q103-ext-gc.dat new file mode 100755 index 00000000..cd59428a Binary files /dev/null and b/system/quests/q103-ext-gc.dat differ diff --git a/system/quests/q103-ext-pc.bin b/system/quests/q103-ext-pc.bin new file mode 100755 index 00000000..a47cf417 Binary files /dev/null and b/system/quests/q103-ext-pc.bin differ diff --git a/system/quests/q103-ext-pc.dat b/system/quests/q103-ext-pc.dat new file mode 100755 index 00000000..1e8c391e Binary files /dev/null and b/system/quests/q103-ext-pc.dat differ diff --git a/system/quests/q104-ext-bb.bin b/system/quests/q104-ext-bb.bin new file mode 100755 index 00000000..65319223 Binary files /dev/null and b/system/quests/q104-ext-bb.bin differ diff --git a/system/quests/q104-ext-bb.dat b/system/quests/q104-ext-bb.dat new file mode 100755 index 00000000..f30005cf Binary files /dev/null and b/system/quests/q104-ext-bb.dat differ diff --git a/system/quests/q104-ext-dc.bin b/system/quests/q104-ext-dc.bin new file mode 100755 index 00000000..3dc62816 Binary files /dev/null and b/system/quests/q104-ext-dc.bin differ diff --git a/system/quests/q104-ext-dc.dat b/system/quests/q104-ext-dc.dat new file mode 100755 index 00000000..90635f11 Binary files /dev/null and b/system/quests/q104-ext-dc.dat differ diff --git a/system/quests/q104-ext-dc1.bin b/system/quests/q104-ext-dc1.bin new file mode 100755 index 00000000..97432ac7 Binary files /dev/null and b/system/quests/q104-ext-dc1.bin differ diff --git a/system/quests/q104-ext-dc1.dat b/system/quests/q104-ext-dc1.dat new file mode 100755 index 00000000..26e08abb Binary files /dev/null and b/system/quests/q104-ext-dc1.dat differ diff --git a/system/quests/q104-ext-gc.bin b/system/quests/q104-ext-gc.bin new file mode 100755 index 00000000..1703279a Binary files /dev/null and b/system/quests/q104-ext-gc.bin differ diff --git a/system/quests/q104-ext-gc.dat b/system/quests/q104-ext-gc.dat new file mode 100755 index 00000000..138a775c Binary files /dev/null and b/system/quests/q104-ext-gc.dat differ diff --git a/system/quests/q104-ext-pc.bin b/system/quests/q104-ext-pc.bin new file mode 100755 index 00000000..5cee1e1a Binary files /dev/null and b/system/quests/q104-ext-pc.bin differ diff --git a/system/quests/q104-ext-pc.dat b/system/quests/q104-ext-pc.dat new file mode 100755 index 00000000..90635f11 Binary files /dev/null and b/system/quests/q104-ext-pc.dat differ diff --git a/system/quests/q108-ext-bb.bin b/system/quests/q108-ext-bb.bin new file mode 100755 index 00000000..bf977161 Binary files /dev/null and b/system/quests/q108-ext-bb.bin differ diff --git a/system/quests/q108-ext-bb.dat b/system/quests/q108-ext-bb.dat new file mode 100755 index 00000000..d70f79dc Binary files /dev/null and b/system/quests/q108-ext-bb.dat differ diff --git a/system/quests/q108-ext-dc.bin b/system/quests/q108-ext-dc.bin new file mode 100755 index 00000000..02108acd Binary files /dev/null and b/system/quests/q108-ext-dc.bin differ diff --git a/system/quests/q108-ext-dc.dat b/system/quests/q108-ext-dc.dat new file mode 100755 index 00000000..2099f7a0 Binary files /dev/null and b/system/quests/q108-ext-dc.dat differ diff --git a/system/quests/q108-ext-dc1.bin b/system/quests/q108-ext-dc1.bin new file mode 100755 index 00000000..862c66a2 Binary files /dev/null and b/system/quests/q108-ext-dc1.bin differ diff --git a/system/quests/q108-ext-dc1.dat b/system/quests/q108-ext-dc1.dat new file mode 100755 index 00000000..062c4930 Binary files /dev/null and b/system/quests/q108-ext-dc1.dat differ diff --git a/system/quests/q108-ext-gc.bin b/system/quests/q108-ext-gc.bin new file mode 100755 index 00000000..f66a84d7 Binary files /dev/null and b/system/quests/q108-ext-gc.bin differ diff --git a/system/quests/q108-ext-gc.dat b/system/quests/q108-ext-gc.dat new file mode 100755 index 00000000..062c4930 Binary files /dev/null and b/system/quests/q108-ext-gc.dat differ diff --git a/system/quests/q108-ext-pc.bin b/system/quests/q108-ext-pc.bin new file mode 100755 index 00000000..f8428f2f Binary files /dev/null and b/system/quests/q108-ext-pc.bin differ diff --git a/system/quests/q108-ext-pc.dat b/system/quests/q108-ext-pc.dat new file mode 100755 index 00000000..2099f7a0 Binary files /dev/null and b/system/quests/q108-ext-pc.dat differ diff --git a/system/quests/q109-ext-bb.bin b/system/quests/q109-ext-bb.bin new file mode 100755 index 00000000..4dd0bd36 Binary files /dev/null and b/system/quests/q109-ext-bb.bin differ diff --git a/system/quests/q109-ext-bb.dat b/system/quests/q109-ext-bb.dat new file mode 100755 index 00000000..7f431bdc Binary files /dev/null and b/system/quests/q109-ext-bb.dat differ diff --git a/system/quests/q109-ext-dc.bin b/system/quests/q109-ext-dc.bin new file mode 100755 index 00000000..f1d6c812 Binary files /dev/null and b/system/quests/q109-ext-dc.bin differ diff --git a/system/quests/q109-ext-dc.dat b/system/quests/q109-ext-dc.dat new file mode 100755 index 00000000..3ea8563a Binary files /dev/null and b/system/quests/q109-ext-dc.dat differ diff --git a/system/quests/q109-ext-dc1.bin b/system/quests/q109-ext-dc1.bin new file mode 100755 index 00000000..12051af1 Binary files /dev/null and b/system/quests/q109-ext-dc1.bin differ diff --git a/system/quests/q109-ext-dc1.dat b/system/quests/q109-ext-dc1.dat new file mode 100755 index 00000000..da3fef30 Binary files /dev/null and b/system/quests/q109-ext-dc1.dat differ diff --git a/system/quests/q109-ext-gc.bin b/system/quests/q109-ext-gc.bin new file mode 100755 index 00000000..d331323b Binary files /dev/null and b/system/quests/q109-ext-gc.bin differ diff --git a/system/quests/q109-ext-gc.dat b/system/quests/q109-ext-gc.dat new file mode 100755 index 00000000..3ea8563a Binary files /dev/null and b/system/quests/q109-ext-gc.dat differ diff --git a/system/quests/q109-ext-pc.bin b/system/quests/q109-ext-pc.bin new file mode 100755 index 00000000..3cc48ef0 Binary files /dev/null and b/system/quests/q109-ext-pc.bin differ diff --git a/system/quests/q109-ext-pc.dat b/system/quests/q109-ext-pc.dat new file mode 100755 index 00000000..3ea8563a Binary files /dev/null and b/system/quests/q109-ext-pc.dat differ diff --git a/system/quests/q110-ext-bb.bin b/system/quests/q110-ext-bb.bin new file mode 100755 index 00000000..819da2d7 Binary files /dev/null and b/system/quests/q110-ext-bb.bin differ diff --git a/system/quests/q110-ext-bb.dat b/system/quests/q110-ext-bb.dat new file mode 100755 index 00000000..d23241ba Binary files /dev/null and b/system/quests/q110-ext-bb.dat differ diff --git a/system/quests/q110-ext-dc.bin b/system/quests/q110-ext-dc.bin new file mode 100755 index 00000000..fa14f7ef Binary files /dev/null and b/system/quests/q110-ext-dc.bin differ diff --git a/system/quests/q110-ext-dc.dat b/system/quests/q110-ext-dc.dat new file mode 100755 index 00000000..06100056 Binary files /dev/null and b/system/quests/q110-ext-dc.dat differ diff --git a/system/quests/q110-ext-dc1.bin b/system/quests/q110-ext-dc1.bin new file mode 100755 index 00000000..5f0f473d Binary files /dev/null and b/system/quests/q110-ext-dc1.bin differ diff --git a/system/quests/q110-ext-dc1.dat b/system/quests/q110-ext-dc1.dat new file mode 100755 index 00000000..2a5dd233 Binary files /dev/null and b/system/quests/q110-ext-dc1.dat differ diff --git a/system/quests/q110-ext-gc.bin b/system/quests/q110-ext-gc.bin new file mode 100755 index 00000000..4527fbb9 Binary files /dev/null and b/system/quests/q110-ext-gc.bin differ diff --git a/system/quests/q110-ext-gc.dat b/system/quests/q110-ext-gc.dat new file mode 100755 index 00000000..1763a04c Binary files /dev/null and b/system/quests/q110-ext-gc.dat differ diff --git a/system/quests/q110-ext-pc.bin b/system/quests/q110-ext-pc.bin new file mode 100755 index 00000000..2a188b25 Binary files /dev/null and b/system/quests/q110-ext-pc.bin differ diff --git a/system/quests/q110-ext-pc.dat b/system/quests/q110-ext-pc.dat new file mode 100755 index 00000000..06100056 Binary files /dev/null and b/system/quests/q110-ext-pc.dat differ diff --git a/system/quests/q111-ext-bb.bin b/system/quests/q111-ext-bb.bin new file mode 100755 index 00000000..8d4904b4 Binary files /dev/null and b/system/quests/q111-ext-bb.bin differ diff --git a/system/quests/q111-ext-bb.dat b/system/quests/q111-ext-bb.dat new file mode 100755 index 00000000..837acce9 Binary files /dev/null and b/system/quests/q111-ext-bb.dat differ diff --git a/system/quests/q111-ext-dc.bin b/system/quests/q111-ext-dc.bin new file mode 100755 index 00000000..7099474f Binary files /dev/null and b/system/quests/q111-ext-dc.bin differ diff --git a/system/quests/q111-ext-dc.dat b/system/quests/q111-ext-dc.dat new file mode 100755 index 00000000..0c59ee5b Binary files /dev/null and b/system/quests/q111-ext-dc.dat differ diff --git a/system/quests/q111-ext-dc1.bin b/system/quests/q111-ext-dc1.bin new file mode 100755 index 00000000..5cf87f1b Binary files /dev/null and b/system/quests/q111-ext-dc1.bin differ diff --git a/system/quests/q111-ext-dc1.dat b/system/quests/q111-ext-dc1.dat new file mode 100755 index 00000000..87219c28 Binary files /dev/null and b/system/quests/q111-ext-dc1.dat differ diff --git a/system/quests/q111-ext-gc.bin b/system/quests/q111-ext-gc.bin new file mode 100755 index 00000000..280a65c2 Binary files /dev/null and b/system/quests/q111-ext-gc.bin differ diff --git a/system/quests/q111-ext-gc.dat b/system/quests/q111-ext-gc.dat new file mode 100755 index 00000000..0c59ee5b Binary files /dev/null and b/system/quests/q111-ext-gc.dat differ diff --git a/system/quests/q111-ext-pc.bin b/system/quests/q111-ext-pc.bin new file mode 100755 index 00000000..40e65a95 Binary files /dev/null and b/system/quests/q111-ext-pc.bin differ diff --git a/system/quests/q111-ext-pc.dat b/system/quests/q111-ext-pc.dat new file mode 100755 index 00000000..0c59ee5b Binary files /dev/null and b/system/quests/q111-ext-pc.dat differ diff --git a/system/quests/q117-ext-bb.bin b/system/quests/q117-ext-bb.bin new file mode 100755 index 00000000..aaef6041 Binary files /dev/null and b/system/quests/q117-ext-bb.bin differ diff --git a/system/quests/q117-ext-bb.dat b/system/quests/q117-ext-bb.dat new file mode 100755 index 00000000..37536030 Binary files /dev/null and b/system/quests/q117-ext-bb.dat differ diff --git a/system/quests/q117-ext-dc.bin b/system/quests/q117-ext-dc.bin new file mode 100755 index 00000000..20a024d0 Binary files /dev/null and b/system/quests/q117-ext-dc.bin differ diff --git a/system/quests/q117-ext-dc.dat b/system/quests/q117-ext-dc.dat new file mode 100755 index 00000000..ba64f2fb Binary files /dev/null and b/system/quests/q117-ext-dc.dat differ diff --git a/system/quests/q117-ext-gc.bin b/system/quests/q117-ext-gc.bin new file mode 100755 index 00000000..a02e8cae Binary files /dev/null and b/system/quests/q117-ext-gc.bin differ diff --git a/system/quests/q117-ext-gc.dat b/system/quests/q117-ext-gc.dat new file mode 100755 index 00000000..ac07b53d Binary files /dev/null and b/system/quests/q117-ext-gc.dat differ diff --git a/system/quests/q117-ext-pc.bin b/system/quests/q117-ext-pc.bin new file mode 100755 index 00000000..af3252aa Binary files /dev/null and b/system/quests/q117-ext-pc.bin differ diff --git a/system/quests/q117-ext-pc.dat b/system/quests/q117-ext-pc.dat new file mode 100755 index 00000000..17ef9521 Binary files /dev/null and b/system/quests/q117-ext-pc.dat differ diff --git a/system/quests/q118-vr-bb.bin b/system/quests/q118-vr-bb.bin new file mode 100755 index 00000000..c980867c Binary files /dev/null and b/system/quests/q118-vr-bb.bin differ diff --git a/system/quests/q118-vr-bb.dat b/system/quests/q118-vr-bb.dat new file mode 100755 index 00000000..5c83f58b Binary files /dev/null and b/system/quests/q118-vr-bb.dat differ diff --git a/system/quests/q118-vr-dc.bin b/system/quests/q118-vr-dc.bin new file mode 100755 index 00000000..b0e6fc5e Binary files /dev/null and b/system/quests/q118-vr-dc.bin differ diff --git a/system/quests/q118-vr-dc.dat b/system/quests/q118-vr-dc.dat new file mode 100755 index 00000000..f19ba016 Binary files /dev/null and b/system/quests/q118-vr-dc.dat differ diff --git a/system/quests/q118-vr-gc.bin b/system/quests/q118-vr-gc.bin new file mode 100755 index 00000000..1d3aabf3 Binary files /dev/null and b/system/quests/q118-vr-gc.bin differ diff --git a/system/quests/q118-vr-gc.dat b/system/quests/q118-vr-gc.dat new file mode 100755 index 00000000..d46d9b9c Binary files /dev/null and b/system/quests/q118-vr-gc.dat differ diff --git a/system/quests/q118-vr-pc.bin b/system/quests/q118-vr-pc.bin new file mode 100755 index 00000000..2bcdfc5d Binary files /dev/null and b/system/quests/q118-vr-pc.bin differ diff --git a/system/quests/q118-vr-pc.dat b/system/quests/q118-vr-pc.dat new file mode 100755 index 00000000..f19ba016 Binary files /dev/null and b/system/quests/q118-vr-pc.dat differ diff --git a/system/quests/q124-evt-bb.bin b/system/quests/q124-evt-bb.bin new file mode 100755 index 00000000..7009fb73 Binary files /dev/null and b/system/quests/q124-evt-bb.bin differ diff --git a/system/quests/q124-evt-bb.dat b/system/quests/q124-evt-bb.dat new file mode 100755 index 00000000..df637f13 Binary files /dev/null and b/system/quests/q124-evt-bb.dat differ diff --git a/system/quests/q137-evt-bb.bin b/system/quests/q137-evt-bb.bin new file mode 100755 index 00000000..6463d009 Binary files /dev/null and b/system/quests/q137-evt-bb.bin differ diff --git a/system/quests/q137-evt-bb.dat b/system/quests/q137-evt-bb.dat new file mode 100755 index 00000000..a2b5a0c8 Binary files /dev/null and b/system/quests/q137-evt-bb.dat differ diff --git a/system/quests/q137-evt-dc.bin b/system/quests/q137-evt-dc.bin new file mode 100755 index 00000000..ed91125d Binary files /dev/null and b/system/quests/q137-evt-dc.bin differ diff --git a/system/quests/q137-evt-dc.dat b/system/quests/q137-evt-dc.dat new file mode 100755 index 00000000..6ff9afef Binary files /dev/null and b/system/quests/q137-evt-dc.dat differ diff --git a/system/quests/q137-evt-gc.bin b/system/quests/q137-evt-gc.bin new file mode 100755 index 00000000..9f7ec2d7 Binary files /dev/null and b/system/quests/q137-evt-gc.bin differ diff --git a/system/quests/q137-evt-gc.dat b/system/quests/q137-evt-gc.dat new file mode 100755 index 00000000..2726b410 Binary files /dev/null and b/system/quests/q137-evt-gc.dat differ diff --git a/system/quests/q137-evt-pc.bin b/system/quests/q137-evt-pc.bin new file mode 100755 index 00000000..611a6d7e Binary files /dev/null and b/system/quests/q137-evt-pc.bin differ diff --git a/system/quests/q137-evt-pc.dat b/system/quests/q137-evt-pc.dat new file mode 100755 index 00000000..6ff9afef Binary files /dev/null and b/system/quests/q137-evt-pc.dat differ diff --git a/system/quests/q138-evt-bb.bin b/system/quests/q138-evt-bb.bin new file mode 100755 index 00000000..f2f8ac93 Binary files /dev/null and b/system/quests/q138-evt-bb.bin differ diff --git a/system/quests/q138-evt-bb.dat b/system/quests/q138-evt-bb.dat new file mode 100755 index 00000000..23c39bcb Binary files /dev/null and b/system/quests/q138-evt-bb.dat differ diff --git a/system/quests/q138-evt-dc.bin b/system/quests/q138-evt-dc.bin new file mode 100755 index 00000000..2d43845e Binary files /dev/null and b/system/quests/q138-evt-dc.bin differ diff --git a/system/quests/q138-evt-dc.dat b/system/quests/q138-evt-dc.dat new file mode 100755 index 00000000..8c147eff Binary files /dev/null and b/system/quests/q138-evt-dc.dat differ diff --git a/system/quests/q138-evt-gc.bin b/system/quests/q138-evt-gc.bin new file mode 100755 index 00000000..994a0432 Binary files /dev/null and b/system/quests/q138-evt-gc.bin differ diff --git a/system/quests/q138-evt-gc.dat b/system/quests/q138-evt-gc.dat new file mode 100755 index 00000000..4477aceb Binary files /dev/null and b/system/quests/q138-evt-gc.dat differ diff --git a/system/quests/q138-evt-pc.bin b/system/quests/q138-evt-pc.bin new file mode 100755 index 00000000..e4cd29b7 Binary files /dev/null and b/system/quests/q138-evt-pc.bin differ diff --git a/system/quests/q138-evt-pc.dat b/system/quests/q138-evt-pc.dat new file mode 100755 index 00000000..8c147eff Binary files /dev/null and b/system/quests/q138-evt-pc.dat differ diff --git a/system/quests/q140-evt-pc.bin b/system/quests/q140-evt-pc.bin new file mode 100755 index 00000000..f1f08b8f Binary files /dev/null and b/system/quests/q140-evt-pc.bin differ diff --git a/system/quests/q140-evt-pc.dat b/system/quests/q140-evt-pc.dat new file mode 100755 index 00000000..c9c6ac84 Binary files /dev/null and b/system/quests/q140-evt-pc.dat differ diff --git a/system/quests/q141-vr-bb.bin b/system/quests/q141-vr-bb.bin new file mode 100755 index 00000000..b11dfa57 Binary files /dev/null and b/system/quests/q141-vr-bb.bin differ diff --git a/system/quests/q141-vr-bb.dat b/system/quests/q141-vr-bb.dat new file mode 100755 index 00000000..aa88de7a Binary files /dev/null and b/system/quests/q141-vr-bb.dat differ diff --git a/system/quests/q141-vr-gc.bin b/system/quests/q141-vr-gc.bin new file mode 100755 index 00000000..2fae9f80 Binary files /dev/null and b/system/quests/q141-vr-gc.bin differ diff --git a/system/quests/q141-vr-gc.dat b/system/quests/q141-vr-gc.dat new file mode 100755 index 00000000..836b303b Binary files /dev/null and b/system/quests/q141-vr-gc.dat differ diff --git a/system/quests/q142-vr-bb.bin b/system/quests/q142-vr-bb.bin new file mode 100755 index 00000000..32956995 Binary files /dev/null and b/system/quests/q142-vr-bb.bin differ diff --git a/system/quests/q142-vr-bb.dat b/system/quests/q142-vr-bb.dat new file mode 100755 index 00000000..6c0a0d81 Binary files /dev/null and b/system/quests/q142-vr-bb.dat differ diff --git a/system/quests/q201-evt-bb.bin b/system/quests/q201-evt-bb.bin new file mode 100755 index 00000000..00e86acc Binary files /dev/null and b/system/quests/q201-evt-bb.bin differ diff --git a/system/quests/q201-evt-bb.dat b/system/quests/q201-evt-bb.dat new file mode 100755 index 00000000..845dfb6b Binary files /dev/null and b/system/quests/q201-evt-bb.dat differ diff --git a/system/quests/q201-evt-gc.bin b/system/quests/q201-evt-gc.bin new file mode 100755 index 00000000..9195658d Binary files /dev/null and b/system/quests/q201-evt-gc.bin differ diff --git a/system/quests/q201-evt-gc.dat b/system/quests/q201-evt-gc.dat new file mode 100755 index 00000000..557584ed Binary files /dev/null and b/system/quests/q201-evt-gc.dat differ diff --git a/system/quests/q202-shp-gc.bin b/system/quests/q202-shp-gc.bin new file mode 100755 index 00000000..7ff81d6e Binary files /dev/null and b/system/quests/q202-shp-gc.bin differ diff --git a/system/quests/q202-shp-gc.dat b/system/quests/q202-shp-gc.dat new file mode 100755 index 00000000..fb59ae5c Binary files /dev/null and b/system/quests/q202-shp-gc.dat differ diff --git a/system/quests/q203-vr-bb.bin b/system/quests/q203-vr-bb.bin new file mode 100755 index 00000000..a365aed9 Binary files /dev/null and b/system/quests/q203-vr-bb.bin differ diff --git a/system/quests/q203-vr-bb.dat b/system/quests/q203-vr-bb.dat new file mode 100755 index 00000000..8a59dcfa Binary files /dev/null and b/system/quests/q203-vr-bb.dat differ diff --git a/system/quests/q203-vr-gc.bin b/system/quests/q203-vr-gc.bin new file mode 100755 index 00000000..218370a8 Binary files /dev/null and b/system/quests/q203-vr-gc.bin differ diff --git a/system/quests/q203-vr-gc.dat b/system/quests/q203-vr-gc.dat new file mode 100755 index 00000000..ba41da3a Binary files /dev/null and b/system/quests/q203-vr-gc.dat differ diff --git a/system/quests/q204-shp-gc.bin b/system/quests/q204-shp-gc.bin new file mode 100755 index 00000000..b44a6183 Binary files /dev/null and b/system/quests/q204-shp-gc.bin differ diff --git a/system/quests/q204-shp-gc.dat b/system/quests/q204-shp-gc.dat new file mode 100755 index 00000000..30efd33b Binary files /dev/null and b/system/quests/q204-shp-gc.dat differ diff --git a/system/quests/q213-evt-gc.bin b/system/quests/q213-evt-gc.bin new file mode 100755 index 00000000..41f76ec9 Binary files /dev/null and b/system/quests/q213-evt-gc.bin differ diff --git a/system/quests/q213-evt-gc.dat b/system/quests/q213-evt-gc.dat new file mode 100755 index 00000000..f5e11b21 Binary files /dev/null and b/system/quests/q213-evt-gc.dat differ diff --git a/system/quests/q219-shp-bb.bin b/system/quests/q219-shp-bb.bin new file mode 100755 index 00000000..fe689c14 Binary files /dev/null and b/system/quests/q219-shp-bb.bin differ diff --git a/system/quests/q219-shp-bb.dat b/system/quests/q219-shp-bb.dat new file mode 100755 index 00000000..894d30d3 Binary files /dev/null and b/system/quests/q219-shp-bb.dat differ diff --git a/system/quests/q220-evt-gc.bin b/system/quests/q220-evt-gc.bin new file mode 100755 index 00000000..29e91cad Binary files /dev/null and b/system/quests/q220-evt-gc.bin differ diff --git a/system/quests/q220-evt-gc.dat b/system/quests/q220-evt-gc.dat new file mode 100755 index 00000000..9c3e5a40 Binary files /dev/null and b/system/quests/q220-evt-gc.dat differ diff --git a/system/quests/q222-vr-gc.bin b/system/quests/q222-vr-gc.bin new file mode 100755 index 00000000..169aa77a Binary files /dev/null and b/system/quests/q222-vr-gc.bin differ diff --git a/system/quests/q222-vr-gc.dat b/system/quests/q222-vr-gc.dat new file mode 100755 index 00000000..90f46235 Binary files /dev/null and b/system/quests/q222-vr-gc.dat differ diff --git a/system/quests/q223-twr-bb.bin b/system/quests/q223-twr-bb.bin new file mode 100755 index 00000000..5182f897 Binary files /dev/null and b/system/quests/q223-twr-bb.bin differ diff --git a/system/quests/q223-twr-bb.dat b/system/quests/q223-twr-bb.dat new file mode 100755 index 00000000..faf2594f Binary files /dev/null and b/system/quests/q223-twr-bb.dat differ diff --git a/system/quests/q223-twr-gc.bin b/system/quests/q223-twr-gc.bin new file mode 100755 index 00000000..ef108339 Binary files /dev/null and b/system/quests/q223-twr-gc.bin differ diff --git a/system/quests/q223-twr-gc.dat b/system/quests/q223-twr-gc.dat new file mode 100755 index 00000000..1807f49c Binary files /dev/null and b/system/quests/q223-twr-gc.dat differ diff --git a/system/quests/q224-twr-bb.bin b/system/quests/q224-twr-bb.bin new file mode 100755 index 00000000..1767e9ea Binary files /dev/null and b/system/quests/q224-twr-bb.bin differ diff --git a/system/quests/q224-twr-bb.dat b/system/quests/q224-twr-bb.dat new file mode 100755 index 00000000..8938f18f Binary files /dev/null and b/system/quests/q224-twr-bb.dat differ diff --git a/system/quests/q224-twr-gc.bin b/system/quests/q224-twr-gc.bin new file mode 100755 index 00000000..d5d93f4b Binary files /dev/null and b/system/quests/q224-twr-gc.bin differ diff --git a/system/quests/q224-twr-gc.dat b/system/quests/q224-twr-gc.dat new file mode 100755 index 00000000..ad22a10f Binary files /dev/null and b/system/quests/q224-twr-gc.dat differ diff --git a/system/quests/q230-vr-gc.bin b/system/quests/q230-vr-gc.bin new file mode 100755 index 00000000..551d882b Binary files /dev/null and b/system/quests/q230-vr-gc.bin differ diff --git a/system/quests/q230-vr-gc.dat b/system/quests/q230-vr-gc.dat new file mode 100755 index 00000000..3ab34e75 Binary files /dev/null and b/system/quests/q230-vr-gc.dat differ diff --git a/system/quests/q231-vr-gc.bin b/system/quests/q231-vr-gc.bin new file mode 100755 index 00000000..ecc461b7 Binary files /dev/null and b/system/quests/q231-vr-gc.bin differ diff --git a/system/quests/q231-vr-gc.dat b/system/quests/q231-vr-gc.dat new file mode 100755 index 00000000..d6aea739 Binary files /dev/null and b/system/quests/q231-vr-gc.dat differ diff --git a/system/quests/q232-evt-gc.bin b/system/quests/q232-evt-gc.bin new file mode 100755 index 00000000..7d7eba82 Binary files /dev/null and b/system/quests/q232-evt-gc.bin differ diff --git a/system/quests/q232-evt-gc.dat b/system/quests/q232-evt-gc.dat new file mode 100755 index 00000000..c1f78188 Binary files /dev/null and b/system/quests/q232-evt-gc.dat differ diff --git a/system/quests/q233-ext-bb.bin b/system/quests/q233-ext-bb.bin new file mode 100755 index 00000000..524992a4 Binary files /dev/null and b/system/quests/q233-ext-bb.bin differ diff --git a/system/quests/q233-ext-bb.dat b/system/quests/q233-ext-bb.dat new file mode 100755 index 00000000..80cda09d Binary files /dev/null and b/system/quests/q233-ext-bb.dat differ diff --git a/system/quests/q233-ext-gc.bin b/system/quests/q233-ext-gc.bin new file mode 100755 index 00000000..870e8812 Binary files /dev/null and b/system/quests/q233-ext-gc.bin differ diff --git a/system/quests/q233-ext-gc.dat b/system/quests/q233-ext-gc.dat new file mode 100755 index 00000000..4a80133f Binary files /dev/null and b/system/quests/q233-ext-gc.dat differ diff --git a/system/quests/q234-ext-bb.bin b/system/quests/q234-ext-bb.bin new file mode 100755 index 00000000..5a9ee826 Binary files /dev/null and b/system/quests/q234-ext-bb.bin differ diff --git a/system/quests/q234-ext-bb.dat b/system/quests/q234-ext-bb.dat new file mode 100755 index 00000000..a88a6fb5 Binary files /dev/null and b/system/quests/q234-ext-bb.dat differ diff --git a/system/quests/q234-ext-gc.bin b/system/quests/q234-ext-gc.bin new file mode 100755 index 00000000..9904ab1a Binary files /dev/null and b/system/quests/q234-ext-gc.bin differ diff --git a/system/quests/q234-ext-gc.dat b/system/quests/q234-ext-gc.dat new file mode 100755 index 00000000..75b8e20e Binary files /dev/null and b/system/quests/q234-ext-gc.dat differ diff --git a/system/quests/q235-ext-bb.bin b/system/quests/q235-ext-bb.bin new file mode 100755 index 00000000..3edb7557 Binary files /dev/null and b/system/quests/q235-ext-bb.bin differ diff --git a/system/quests/q235-ext-bb.dat b/system/quests/q235-ext-bb.dat new file mode 100755 index 00000000..9818bcf0 Binary files /dev/null and b/system/quests/q235-ext-bb.dat differ diff --git a/system/quests/q235-ext-gc.bin b/system/quests/q235-ext-gc.bin new file mode 100755 index 00000000..39a0596c Binary files /dev/null and b/system/quests/q235-ext-gc.bin differ diff --git a/system/quests/q235-ext-gc.dat b/system/quests/q235-ext-gc.dat new file mode 100755 index 00000000..3777b25c Binary files /dev/null and b/system/quests/q235-ext-gc.dat differ diff --git a/system/quests/q236-ext-bb.bin b/system/quests/q236-ext-bb.bin new file mode 100755 index 00000000..aa40614c Binary files /dev/null and b/system/quests/q236-ext-bb.bin differ diff --git a/system/quests/q236-ext-bb.dat b/system/quests/q236-ext-bb.dat new file mode 100755 index 00000000..255a6ed9 Binary files /dev/null and b/system/quests/q236-ext-bb.dat differ diff --git a/system/quests/q236-ext-gc.bin b/system/quests/q236-ext-gc.bin new file mode 100755 index 00000000..4d0d0321 Binary files /dev/null and b/system/quests/q236-ext-gc.bin differ diff --git a/system/quests/q236-ext-gc.dat b/system/quests/q236-ext-gc.dat new file mode 100755 index 00000000..8c9a066b Binary files /dev/null and b/system/quests/q236-ext-gc.dat differ diff --git a/system/quests/q237-vr-gc.bin b/system/quests/q237-vr-gc.bin new file mode 100755 index 00000000..2d7c66f7 Binary files /dev/null and b/system/quests/q237-vr-gc.bin differ diff --git a/system/quests/q237-vr-gc.dat b/system/quests/q237-vr-gc.dat new file mode 100755 index 00000000..4324d667 Binary files /dev/null and b/system/quests/q237-vr-gc.dat differ diff --git a/system/quests/q238-vr-gc.bin b/system/quests/q238-vr-gc.bin new file mode 100755 index 00000000..897bc322 Binary files /dev/null and b/system/quests/q238-vr-gc.bin differ diff --git a/system/quests/q238-vr-gc.dat b/system/quests/q238-vr-gc.dat new file mode 100755 index 00000000..ed5a4bcf Binary files /dev/null and b/system/quests/q238-vr-gc.dat differ diff --git a/system/quests/q239-evt-gc.bin b/system/quests/q239-evt-gc.bin new file mode 100755 index 00000000..f1383018 Binary files /dev/null and b/system/quests/q239-evt-gc.bin differ diff --git a/system/quests/q239-evt-gc.dat b/system/quests/q239-evt-gc.dat new file mode 100755 index 00000000..f77472a8 Binary files /dev/null and b/system/quests/q239-evt-gc.dat differ diff --git a/system/quests/q335-evt-gc.bin b/system/quests/q335-evt-gc.bin new file mode 100755 index 00000000..793ad215 Binary files /dev/null and b/system/quests/q335-evt-gc.bin differ diff --git a/system/quests/q335-evt-gc.dat b/system/quests/q335-evt-gc.dat new file mode 100755 index 00000000..e2d35b7c Binary files /dev/null and b/system/quests/q335-evt-gc.dat differ diff --git a/system/quests/q401-gov-bb.bin b/system/quests/q401-gov-bb.bin new file mode 100755 index 00000000..205541d5 Binary files /dev/null and b/system/quests/q401-gov-bb.bin differ diff --git a/system/quests/q401-gov-bb.dat b/system/quests/q401-gov-bb.dat new file mode 100755 index 00000000..502f0179 Binary files /dev/null and b/system/quests/q401-gov-bb.dat differ diff --git a/system/quests/q402-gov-bb.bin b/system/quests/q402-gov-bb.bin new file mode 100755 index 00000000..98b27bd2 Binary files /dev/null and b/system/quests/q402-gov-bb.bin differ diff --git a/system/quests/q402-gov-bb.dat b/system/quests/q402-gov-bb.dat new file mode 100755 index 00000000..33f4546a Binary files /dev/null and b/system/quests/q402-gov-bb.dat differ diff --git a/system/quests/q403-gov-bb.bin b/system/quests/q403-gov-bb.bin new file mode 100755 index 00000000..64822184 Binary files /dev/null and b/system/quests/q403-gov-bb.bin differ diff --git a/system/quests/q403-gov-bb.dat b/system/quests/q403-gov-bb.dat new file mode 100755 index 00000000..d3a0e892 Binary files /dev/null and b/system/quests/q403-gov-bb.dat differ diff --git a/system/quests/q404-gov-bb.bin b/system/quests/q404-gov-bb.bin new file mode 100755 index 00000000..a38df4ba Binary files /dev/null and b/system/quests/q404-gov-bb.bin differ diff --git a/system/quests/q404-gov-bb.dat b/system/quests/q404-gov-bb.dat new file mode 100755 index 00000000..1899074e Binary files /dev/null and b/system/quests/q404-gov-bb.dat differ diff --git a/system/quests/q405-gov-bb.bin b/system/quests/q405-gov-bb.bin new file mode 100755 index 00000000..f9299c8e Binary files /dev/null and b/system/quests/q405-gov-bb.bin differ diff --git a/system/quests/q405-gov-bb.dat b/system/quests/q405-gov-bb.dat new file mode 100755 index 00000000..a40b731c Binary files /dev/null and b/system/quests/q405-gov-bb.dat differ diff --git a/system/quests/q406-gov-bb.bin b/system/quests/q406-gov-bb.bin new file mode 100755 index 00000000..f5bea26e Binary files /dev/null and b/system/quests/q406-gov-bb.bin differ diff --git a/system/quests/q406-gov-bb.dat b/system/quests/q406-gov-bb.dat new file mode 100755 index 00000000..49972b89 Binary files /dev/null and b/system/quests/q406-gov-bb.dat differ diff --git a/system/quests/q407-gov-bb.bin b/system/quests/q407-gov-bb.bin new file mode 100755 index 00000000..47e929e7 Binary files /dev/null and b/system/quests/q407-gov-bb.bin differ diff --git a/system/quests/q407-gov-bb.dat b/system/quests/q407-gov-bb.dat new file mode 100755 index 00000000..3070bd9e Binary files /dev/null and b/system/quests/q407-gov-bb.dat differ diff --git a/system/quests/q408-gov-bb.bin b/system/quests/q408-gov-bb.bin new file mode 100755 index 00000000..a8a805fd Binary files /dev/null and b/system/quests/q408-gov-bb.bin differ diff --git a/system/quests/q408-gov-bb.dat b/system/quests/q408-gov-bb.dat new file mode 100755 index 00000000..3f337441 Binary files /dev/null and b/system/quests/q408-gov-bb.dat differ diff --git a/system/quests/q409-gov-bb.bin b/system/quests/q409-gov-bb.bin new file mode 100755 index 00000000..622cc04c Binary files /dev/null and b/system/quests/q409-gov-bb.bin differ diff --git a/system/quests/q409-gov-bb.dat b/system/quests/q409-gov-bb.dat new file mode 100755 index 00000000..5b5318ec Binary files /dev/null and b/system/quests/q409-gov-bb.dat differ diff --git a/system/quests/q410-gov-bb.bin b/system/quests/q410-gov-bb.bin new file mode 100755 index 00000000..cc574f62 Binary files /dev/null and b/system/quests/q410-gov-bb.bin differ diff --git a/system/quests/q410-gov-bb.dat b/system/quests/q410-gov-bb.dat new file mode 100755 index 00000000..01729760 Binary files /dev/null and b/system/quests/q410-gov-bb.dat differ diff --git a/system/quests/q411-gov-bb.bin b/system/quests/q411-gov-bb.bin new file mode 100755 index 00000000..f4c13175 Binary files /dev/null and b/system/quests/q411-gov-bb.bin differ diff --git a/system/quests/q411-gov-bb.dat b/system/quests/q411-gov-bb.dat new file mode 100755 index 00000000..9f1ff0d3 Binary files /dev/null and b/system/quests/q411-gov-bb.dat differ diff --git a/system/quests/q412-gov-bb.bin b/system/quests/q412-gov-bb.bin new file mode 100755 index 00000000..fdefdbed Binary files /dev/null and b/system/quests/q412-gov-bb.bin differ diff --git a/system/quests/q412-gov-bb.dat b/system/quests/q412-gov-bb.dat new file mode 100755 index 00000000..e24959a0 Binary files /dev/null and b/system/quests/q412-gov-bb.dat differ diff --git a/system/quests/q413-gov-bb.bin b/system/quests/q413-gov-bb.bin new file mode 100755 index 00000000..a0e365e3 Binary files /dev/null and b/system/quests/q413-gov-bb.bin differ diff --git a/system/quests/q413-gov-bb.dat b/system/quests/q413-gov-bb.dat new file mode 100755 index 00000000..91850cea Binary files /dev/null and b/system/quests/q413-gov-bb.dat differ diff --git a/system/quests/q414-gov-bb.bin b/system/quests/q414-gov-bb.bin new file mode 100755 index 00000000..69b8a33c Binary files /dev/null and b/system/quests/q414-gov-bb.bin differ diff --git a/system/quests/q414-gov-bb.dat b/system/quests/q414-gov-bb.dat new file mode 100755 index 00000000..e89866ff Binary files /dev/null and b/system/quests/q414-gov-bb.dat differ diff --git a/system/quests/q415-gov-bb.bin b/system/quests/q415-gov-bb.bin new file mode 100755 index 00000000..a1c713e5 Binary files /dev/null and b/system/quests/q415-gov-bb.bin differ diff --git a/system/quests/q415-gov-bb.dat b/system/quests/q415-gov-bb.dat new file mode 100755 index 00000000..c8ff0bed Binary files /dev/null and b/system/quests/q415-gov-bb.dat differ diff --git a/system/quests/q451-gov-bb.bin b/system/quests/q451-gov-bb.bin new file mode 100755 index 00000000..cc9ee2c8 Binary files /dev/null and b/system/quests/q451-gov-bb.bin differ diff --git a/system/quests/q451-gov-bb.dat b/system/quests/q451-gov-bb.dat new file mode 100755 index 00000000..43def1f8 Binary files /dev/null and b/system/quests/q451-gov-bb.dat differ diff --git a/system/quests/q452-gov-bb.bin b/system/quests/q452-gov-bb.bin new file mode 100755 index 00000000..9ad205d1 Binary files /dev/null and b/system/quests/q452-gov-bb.bin differ diff --git a/system/quests/q452-gov-bb.dat b/system/quests/q452-gov-bb.dat new file mode 100755 index 00000000..142871f9 Binary files /dev/null and b/system/quests/q452-gov-bb.dat differ diff --git a/system/quests/q453-gov-bb.bin b/system/quests/q453-gov-bb.bin new file mode 100755 index 00000000..6326b12e Binary files /dev/null and b/system/quests/q453-gov-bb.bin differ diff --git a/system/quests/q453-gov-bb.dat b/system/quests/q453-gov-bb.dat new file mode 100755 index 00000000..f95dc7fe Binary files /dev/null and b/system/quests/q453-gov-bb.dat differ diff --git a/system/quests/q454-gov-bb.bin b/system/quests/q454-gov-bb.bin new file mode 100755 index 00000000..ac0a2838 Binary files /dev/null and b/system/quests/q454-gov-bb.bin differ diff --git a/system/quests/q454-gov-bb.dat b/system/quests/q454-gov-bb.dat new file mode 100755 index 00000000..a056351f Binary files /dev/null and b/system/quests/q454-gov-bb.dat differ diff --git a/system/quests/q455-gov-bb.bin b/system/quests/q455-gov-bb.bin new file mode 100755 index 00000000..6f164345 Binary files /dev/null and b/system/quests/q455-gov-bb.bin differ diff --git a/system/quests/q455-gov-bb.dat b/system/quests/q455-gov-bb.dat new file mode 100755 index 00000000..b17fdc41 Binary files /dev/null and b/system/quests/q455-gov-bb.dat differ diff --git a/system/quests/q456-gov-bb.bin b/system/quests/q456-gov-bb.bin new file mode 100755 index 00000000..d2a76f26 Binary files /dev/null and b/system/quests/q456-gov-bb.bin differ diff --git a/system/quests/q456-gov-bb.dat b/system/quests/q456-gov-bb.dat new file mode 100755 index 00000000..b0a3ec58 Binary files /dev/null and b/system/quests/q456-gov-bb.dat differ diff --git a/system/quests/q457-gov-bb.bin b/system/quests/q457-gov-bb.bin new file mode 100755 index 00000000..ac7a6138 Binary files /dev/null and b/system/quests/q457-gov-bb.bin differ diff --git a/system/quests/q457-gov-bb.dat b/system/quests/q457-gov-bb.dat new file mode 100755 index 00000000..e709170d Binary files /dev/null and b/system/quests/q457-gov-bb.dat differ diff --git a/system/quests/q458-gov-bb.bin b/system/quests/q458-gov-bb.bin new file mode 100755 index 00000000..f73cf42e Binary files /dev/null and b/system/quests/q458-gov-bb.bin differ diff --git a/system/quests/q458-gov-bb.dat b/system/quests/q458-gov-bb.dat new file mode 100755 index 00000000..67a705df Binary files /dev/null and b/system/quests/q458-gov-bb.dat differ diff --git a/system/quests/q459-gov-bb.bin b/system/quests/q459-gov-bb.bin new file mode 100755 index 00000000..1bac825e Binary files /dev/null and b/system/quests/q459-gov-bb.bin differ diff --git a/system/quests/q459-gov-bb.dat b/system/quests/q459-gov-bb.dat new file mode 100755 index 00000000..b0075d5f Binary files /dev/null and b/system/quests/q459-gov-bb.dat differ diff --git a/system/quests/q460-gov-bb.bin b/system/quests/q460-gov-bb.bin new file mode 100755 index 00000000..ca607d7f Binary files /dev/null and b/system/quests/q460-gov-bb.bin differ diff --git a/system/quests/q460-gov-bb.dat b/system/quests/q460-gov-bb.dat new file mode 100755 index 00000000..f658a46a Binary files /dev/null and b/system/quests/q460-gov-bb.dat differ diff --git a/system/quests/q461-gov-bb.bin b/system/quests/q461-gov-bb.bin new file mode 100755 index 00000000..69338467 Binary files /dev/null and b/system/quests/q461-gov-bb.bin differ diff --git a/system/quests/q461-gov-bb.dat b/system/quests/q461-gov-bb.dat new file mode 100755 index 00000000..8ecf4cd8 Binary files /dev/null and b/system/quests/q461-gov-bb.dat differ diff --git a/system/quests/q462-gov-bb.bin b/system/quests/q462-gov-bb.bin new file mode 100755 index 00000000..0d87c495 Binary files /dev/null and b/system/quests/q462-gov-bb.bin differ diff --git a/system/quests/q462-gov-bb.dat b/system/quests/q462-gov-bb.dat new file mode 100755 index 00000000..7764db84 Binary files /dev/null and b/system/quests/q462-gov-bb.dat differ diff --git a/system/quests/q463-gov-bb.bin b/system/quests/q463-gov-bb.bin new file mode 100755 index 00000000..4c735312 Binary files /dev/null and b/system/quests/q463-gov-bb.bin differ diff --git a/system/quests/q463-gov-bb.dat b/system/quests/q463-gov-bb.dat new file mode 100755 index 00000000..872c0144 Binary files /dev/null and b/system/quests/q463-gov-bb.dat differ diff --git a/system/quests/q464-gov-bb.bin b/system/quests/q464-gov-bb.bin new file mode 100755 index 00000000..89d4f010 Binary files /dev/null and b/system/quests/q464-gov-bb.bin differ diff --git a/system/quests/q464-gov-bb.dat b/system/quests/q464-gov-bb.dat new file mode 100755 index 00000000..54d687f2 Binary files /dev/null and b/system/quests/q464-gov-bb.dat differ diff --git a/system/quests/q465-gov-bb.bin b/system/quests/q465-gov-bb.bin new file mode 100755 index 00000000..33254291 Binary files /dev/null and b/system/quests/q465-gov-bb.bin differ diff --git a/system/quests/q465-gov-bb.dat b/system/quests/q465-gov-bb.dat new file mode 100755 index 00000000..d10e47b6 Binary files /dev/null and b/system/quests/q465-gov-bb.dat differ diff --git a/system/quests/q466-gov-bb.bin b/system/quests/q466-gov-bb.bin new file mode 100755 index 00000000..f80b3a48 Binary files /dev/null and b/system/quests/q466-gov-bb.bin differ diff --git a/system/quests/q466-gov-bb.dat b/system/quests/q466-gov-bb.dat new file mode 100755 index 00000000..b7b65a78 Binary files /dev/null and b/system/quests/q466-gov-bb.dat differ diff --git a/system/quests/q467-gov-bb.bin b/system/quests/q467-gov-bb.bin new file mode 100755 index 00000000..d4dd2066 Binary files /dev/null and b/system/quests/q467-gov-bb.bin differ diff --git a/system/quests/q467-gov-bb.dat b/system/quests/q467-gov-bb.dat new file mode 100755 index 00000000..39fb1981 Binary files /dev/null and b/system/quests/q467-gov-bb.dat differ diff --git a/system/quests/q468-gov-bb.bin b/system/quests/q468-gov-bb.bin new file mode 100755 index 00000000..db3e9d98 Binary files /dev/null and b/system/quests/q468-gov-bb.bin differ diff --git a/system/quests/q468-gov-bb.dat b/system/quests/q468-gov-bb.dat new file mode 100755 index 00000000..39e366da Binary files /dev/null and b/system/quests/q468-gov-bb.dat differ diff --git a/system/quests/q496-evt-gc.bin b/system/quests/q496-evt-gc.bin new file mode 100755 index 00000000..a56b01f1 Binary files /dev/null and b/system/quests/q496-evt-gc.bin differ diff --git a/system/quests/q496-evt-gc.dat b/system/quests/q496-evt-gc.dat new file mode 100755 index 00000000..3d2b23dc Binary files /dev/null and b/system/quests/q496-evt-gc.dat differ diff --git a/system/quests/q701-gov-bb.bin b/system/quests/q701-gov-bb.bin new file mode 100755 index 00000000..df3fb763 Binary files /dev/null and b/system/quests/q701-gov-bb.bin differ diff --git a/system/quests/q701-gov-bb.dat b/system/quests/q701-gov-bb.dat new file mode 100755 index 00000000..1e9c8930 Binary files /dev/null and b/system/quests/q701-gov-bb.dat differ diff --git a/system/quests/q702-gov-bb.bin b/system/quests/q702-gov-bb.bin new file mode 100755 index 00000000..0af2450d Binary files /dev/null and b/system/quests/q702-gov-bb.bin differ diff --git a/system/quests/q702-gov-bb.dat b/system/quests/q702-gov-bb.dat new file mode 100755 index 00000000..facbd719 Binary files /dev/null and b/system/quests/q702-gov-bb.dat differ diff --git a/system/quests/q703-gov-bb.bin b/system/quests/q703-gov-bb.bin new file mode 100755 index 00000000..d7bed9ce Binary files /dev/null and b/system/quests/q703-gov-bb.bin differ diff --git a/system/quests/q703-gov-bb.dat b/system/quests/q703-gov-bb.dat new file mode 100755 index 00000000..a6beab9a Binary files /dev/null and b/system/quests/q703-gov-bb.dat differ diff --git a/system/quests/q704-gov-bb.bin b/system/quests/q704-gov-bb.bin new file mode 100755 index 00000000..3c4d2da1 Binary files /dev/null and b/system/quests/q704-gov-bb.bin differ diff --git a/system/quests/q704-gov-bb.dat b/system/quests/q704-gov-bb.dat new file mode 100755 index 00000000..9697bec2 Binary files /dev/null and b/system/quests/q704-gov-bb.dat differ diff --git a/system/quests/q705-gov-bb.bin b/system/quests/q705-gov-bb.bin new file mode 100755 index 00000000..75ee30d5 Binary files /dev/null and b/system/quests/q705-gov-bb.bin differ diff --git a/system/quests/q705-gov-bb.dat b/system/quests/q705-gov-bb.dat new file mode 100755 index 00000000..7391f3f3 Binary files /dev/null and b/system/quests/q705-gov-bb.dat differ diff --git a/system/quests/q706-gov-bb.bin b/system/quests/q706-gov-bb.bin new file mode 100755 index 00000000..aaaf3c9d Binary files /dev/null and b/system/quests/q706-gov-bb.bin differ diff --git a/system/quests/q706-gov-bb.dat b/system/quests/q706-gov-bb.dat new file mode 100755 index 00000000..b80bad73 Binary files /dev/null and b/system/quests/q706-gov-bb.dat differ diff --git a/system/quests/q707-gov-bb.bin b/system/quests/q707-gov-bb.bin new file mode 100755 index 00000000..80c2693e Binary files /dev/null and b/system/quests/q707-gov-bb.bin differ diff --git a/system/quests/q707-gov-bb.dat b/system/quests/q707-gov-bb.dat new file mode 100755 index 00000000..54ebb5a6 Binary files /dev/null and b/system/quests/q707-gov-bb.dat differ diff --git a/system/quests/q708-gov-bb.bin b/system/quests/q708-gov-bb.bin new file mode 100755 index 00000000..a2a908bd Binary files /dev/null and b/system/quests/q708-gov-bb.bin differ diff --git a/system/quests/q708-gov-bb.dat b/system/quests/q708-gov-bb.dat new file mode 100755 index 00000000..7c96dce8 Binary files /dev/null and b/system/quests/q708-gov-bb.dat differ diff --git a/system/quests/q709-gov-bb.bin b/system/quests/q709-gov-bb.bin new file mode 100755 index 00000000..4740ca4e Binary files /dev/null and b/system/quests/q709-gov-bb.bin differ diff --git a/system/quests/q709-gov-bb.dat b/system/quests/q709-gov-bb.dat new file mode 100755 index 00000000..132fbd9c Binary files /dev/null and b/system/quests/q709-gov-bb.dat differ diff --git a/system/quests/q811-ext-bb.bin b/system/quests/q811-ext-bb.bin new file mode 100755 index 00000000..19fca159 Binary files /dev/null and b/system/quests/q811-ext-bb.bin differ diff --git a/system/quests/q811-ext-bb.dat b/system/quests/q811-ext-bb.dat new file mode 100755 index 00000000..66d32bb8 Binary files /dev/null and b/system/quests/q811-ext-bb.dat differ diff --git a/system/quests/q812-ext-bb.bin b/system/quests/q812-ext-bb.bin new file mode 100755 index 00000000..d41c4a15 Binary files /dev/null and b/system/quests/q812-ext-bb.bin differ diff --git a/system/quests/q812-ext-bb.dat b/system/quests/q812-ext-bb.dat new file mode 100755 index 00000000..eb59e9e3 Binary files /dev/null and b/system/quests/q812-ext-bb.dat differ diff --git a/system/quests/q813-ext-bb.bin b/system/quests/q813-ext-bb.bin new file mode 100755 index 00000000..94156472 Binary files /dev/null and b/system/quests/q813-ext-bb.bin differ diff --git a/system/quests/q813-ext-bb.dat b/system/quests/q813-ext-bb.dat new file mode 100755 index 00000000..1e621f98 Binary files /dev/null and b/system/quests/q813-ext-bb.dat differ diff --git a/system/quests/q814-ext-bb.bin b/system/quests/q814-ext-bb.bin new file mode 100755 index 00000000..31a9888e Binary files /dev/null and b/system/quests/q814-ext-bb.bin differ diff --git a/system/quests/q814-ext-bb.dat b/system/quests/q814-ext-bb.dat new file mode 100755 index 00000000..36330f0f Binary files /dev/null and b/system/quests/q814-ext-bb.dat differ diff --git a/system/quests/q815-ext-bb.bin b/system/quests/q815-ext-bb.bin new file mode 100755 index 00000000..9e3c158d Binary files /dev/null and b/system/quests/q815-ext-bb.bin differ diff --git a/system/quests/q815-ext-bb.dat b/system/quests/q815-ext-bb.dat new file mode 100755 index 00000000..2e07667b Binary files /dev/null and b/system/quests/q815-ext-bb.dat differ diff --git a/system/quests/q999-shp-gc.bin b/system/quests/q999-shp-gc.bin new file mode 100755 index 00000000..abd8bb07 Binary files /dev/null and b/system/quests/q999-shp-gc.bin differ diff --git a/system/quests/q999-shp-gc.dat b/system/quests/q999-shp-gc.dat new file mode 100755 index 00000000..57547b49 Binary files /dev/null and b/system/quests/q999-shp-gc.dat differ