diff --git a/README.md b/README.md index 67e869cb..356dff29 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ Current known issues / missing features: - The trade window isn't implemented yet. - PSO PC and PSOBB are essentially entirely untested. Only GC is fairly well-tested. - Add all the chat commands that khyller used to have. (Most, but not all, currently exist in newserv.) -- The command structures are defined in multiple places. Centralize them. ## Usage diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index f8e3f14f..67feb674 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -524,6 +524,19 @@ static void command_lobby_type(shared_ptr, shared_ptr l, //////////////////////////////////////////////////////////////////////////////// // Game commands +static void command_secid(shared_ptr, shared_ptr l, + shared_ptr c, const char16_t* args) { + check_is_game(l, false); + + if (!args[0]) { + c->override_section_id = -1; + send_text_message(l, u"$C6Override section ID\nremoved"); + } else { + c->override_section_id = section_id_for_name(args); + send_text_message(l, u"$C6Override section ID\nset"); + } +} + static void command_password(shared_ptr, shared_ptr l, shared_ptr c, const char16_t* args) { check_is_game(l, true); @@ -534,7 +547,7 @@ static void command_password(shared_ptr, shared_ptr l, send_text_message(l, u"$C6Game unlocked"); } else { - char16cpy(l->password, args, 0x10); + strncpy_t(l->password, args, countof(l->password)); auto encoded = encode_sjis(l->password); send_text_message_printf(l, "$C6Game password:\n%s", encoded.c_str()); @@ -578,7 +591,7 @@ static void command_edit(shared_ptr s, shared_ptr l, check_version(c, GameVersion::BB); string encoded_args = encode_sjis(args); - vector tokens = split(encoded_args, L' '); + vector tokens = split(encoded_args, ' '); if (tokens.size() < 3) { send_text_message(c, u"$C6Not enough arguments"); @@ -909,6 +922,7 @@ static const unordered_map chat_commands({ {u"maxlevel" , {command_max_level , u"Usage:\nmax_level "}}, {u"minlevel" , {command_min_level , u"Usage:\nmin_level "}}, {u"password" , {command_password , u"Usage:\nlock [password]\nomit password to\nunlock game"}}, + {u"secid" , {command_secid , u"Usage:\nsecid [section ID]\nomit section ID to\nrevert to normal"}}, {u"silence" , {command_silence , u"Usage:\nsilence "}}, {u"song" , {command_song , u"Usage:\nsong "}}, {u"type" , {command_lobby_type , u"Usage:\ntype "}}, diff --git a/src/Client.cc b/src/Client.cc index a3e0d2b5..8da09741 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -38,6 +38,7 @@ Client::Client( lobby_client_id(0), lobby_arrow_color(0), next_exp_value(0), + override_section_id(-1), infinite_hp(false), infinite_tp(false), can_chat(true) { diff --git a/src/Client.hh b/src/Client.hh index 0ac60da0..a539974a 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -61,20 +61,21 @@ struct Client { uint32_t proxy_destination_address; uint16_t proxy_destination_port; - // timing & menus + // 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 - // lobby/positioning + // 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) + // Miscellaneous (used by chat commands) uint32_t next_exp_value; // next EXP value to give + int16_t override_section_id; // valid if >= 0 bool infinite_hp; // cheats enabled bool infinite_tp; // cheats enabled bool can_chat; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh new file mode 100644 index 00000000..72059c9b --- /dev/null +++ b/src/CommandFormats.hh @@ -0,0 +1,1259 @@ +#pragma once + +#include + +#include "PSOProtocol.hh" + + + +#pragma pack(push) +#pragma pack(1) + + + +// Names are like [S|C|SC]_CommandName_[Versions]_Numbers +// S/C denotes who sends the command (S = server, C = client, SC = both) +// If versions are not specified, the format is the same for all versions. + +// Sorted by command number. + + + +// 00: Invalid command + +// 01 (S->C): Lobby message box +// Message box appears in lower-right corner; user must press a key to continue + +struct SC_TextHeader_01_06_11 { + le_uint32_t unused; + le_uint32_t guild_card_number; +}; + +// 02 (S->C): Start encryption (except on BB) +// Client will respond with an (encrypted) 9A, 9D, or 9E command + +struct S_ServerInit_DC_GC_02_17 { + char copyright[0x40]; + le_uint32_t server_key; // Key for data sent by server + le_uint32_t client_key; // Key for data sent by client + char after_message[200]; +}; + +struct S_ServerInit_Patch_02 { + char copyright[0x40]; + le_uint32_t server_key; + le_uint32_t client_key; + // BB rejects the command if it's not exactly this size, so we can't add the + // anti-copyright message like we do in the other server init commands +}; + +// 03 (S->C): Start encryption (BB) +// Client will respond with a 93 command + +struct S_ServerInit_BB_03 { + char copyright[0x60]; + uint8_t server_key[0x30]; + uint8_t client_key[0x30]; + char after_message[200]; +}; + +// 04 (S->C): Set guild card number and update client config ("security data") +// Client will respond with a 96 command, but only the first time it receives +// this command (TODO: Is this true? It might instead be that it responds with a +// 96 only if encryption was started with a 17 rather than an 02) + +struct S_UpdateClientConfig_DC_PC_GC_04 { + le_uint32_t player_tag; + le_uint32_t guild_card_number; + // This is how newserv uses this command; other servers do not use the same + // format for the following 0x20 bytes (or may not use it at all) + ClientConfig cfg; +}; + +// 04 (S->C): Request login information (patch server) (no arguments) +// 04 (C->S): Log in (patch server) + +struct C_Login_Patch_04 { + le_uint32_t unused[3]; + char username[0x10]; + char password[0x10]; +}; + +// 05: Disconnect +// No arguments + +// 06: Chat +// Server->client format is same as 01 command +// Client->server format is very similar; we include a zero-length array in this +// struct to make parsing easier +// The guild_card_number field is used only in server->client commands + +struct C_Chat_06 { + le_uint32_t unused[2]; + union { + char dcgc[0]; + char16_t pcbb[0]; + } text; +}; + + +// 07 (S->C): Ship select menu + +// Command is a list of these; header.flag is the entry count. The first entry +// is not included in the count and does not appear on the client. The text of +// the first entry becomes the ship name when the client joins a lobby. +template +struct S_MenuEntry { + le_uint32_t menu_id; + le_uint32_t item_id; + le_uint16_t flags; // should be 0x0F04 + CharT text[EntryLength]; +}; +struct S_MenuEntry_PC_BB_07 : S_MenuEntry { }; +struct S_MenuEntry_DC_GC_07 : S_MenuEntry { }; + +// 08 (C->S): Request game list +// No arguments + +// 08 (S->C): Game list +// Client responds with 09 and 10 commands + +// Command is a list of these; header.flag is the entry count. The first entry +// is not included in the count and does not appear on the client. +template +struct S_GameMenuEntry { + le_uint32_t menu_id; + le_uint32_t game_id; + uint8_t difficulty_tag; // 0x0A = Ep3; else difficulty + 0x22 (so 0x25 = Ult) + uint8_t num_players; + CharT name[0x10]; + uint8_t episode; // 40 = Ep1, 41 = Ep2, 43 = Ep4. Ignored on Ep3 + uint8_t flags; // 02 = locked, 04 = disabled (BB), 10 = battle, 20 = challenge +}; +struct S_GameMenuEntry_PC_BB_08 : S_GameMenuEntry { }; +struct S_GameMenuEntry_GC_08 : S_GameMenuEntry { }; + +// 09 (S->C): Check directory (patch server) + +struct S_CheckDirectory_Patch_09 { + char name[0x40]; +}; + +// 09 (C->S): Menu item info request +// Server will respond with an 11 command, or an A3 if it's the quest menu + +struct C_MenuItemInfoRequest_09 { + le_uint32_t menu_id; + le_uint32_t item_id; +}; + +// 0A: Done checking directory (patch server) +// No arguemnts + +// 0B: Invalid command + +// 0C: Create game (DCv1) +// Format unknown + +// 0D: Invalid command +// 0E: Invalid command +// 0F: Invalid command + +// 10 (C->S): Menu selection + +struct C_MenuSelection { + le_uint32_t menu_id; + le_uint32_t item_id; + // Password is only present when client attempts to join a locked game + union { + char dcgc[0]; + char16_t pcbb[0]; + } password; +}; + +// 11: Ship info +// Same format as 01 command + +// 12: Session complete (patch server) +// No arguments + +// 13 (S->C): Message box (patch server) +// Same as 1A/D5 command + +// 13 (C->S): Confirm file write +// Client sends this in response to each 13 sent by the server +// Format not documented here + +// 13 (S->C): Write online quest file +// Used for downloading online quests + +// Header flag = file chunk index +struct S_WriteFile_13_A7 { + char filename[0x10]; + uint8_t data[0x400]; + le_uint32_t data_size; +}; + +// 14: Invalid command +// 15: Invalid command +// 16: Invalid command + +// 17 (S->C): Start encryption at login server (except on BB) +// Same format as 02 command, but a different copyright string +// Client will respond with a DB command + +// 18: Invalid command + +// 19 (S->C): Reconnect to different address +// Client will disconnect, and reconnect to the given address/port. + +// Because PSO PC and some versions of PSO GC use the same port but different +// protocols, we use a specially-crafted 19 command to send them to two +// different ports depending on the client version. + +struct S_Reconnect_19 { + be_uint32_t address; + le_uint16_t port; + le_uint16_t unused; +}; + +struct S_ReconnectSplit_19 { + be_uint32_t pc_address; + le_uint16_t pc_port; + uint8_t unused1[0x0F]; + uint8_t gc_command; + uint8_t gc_flag; + le_uint16_t gc_size; + be_uint32_t gc_address; + le_uint16_t gc_port; + uint8_t unused2[0xB0 - 0x23]; +}; + +// 1A: Large message box +// Client will usually respond with a D6 command (see D6 for more information) +// Contents are plain text (char on DC/GC, char16_t on PC/BB). There must be at +// least one null character ('\0') before the end of the command data. + +// 1B: Invalid command +// 1C: Invalid command + +// 1D: Ping +// No arguments +// When sent to the client, the client will respond with a 1D command + +// 1E: Invalid command + +// 1F (S->C): Information menu +// Same format and usage as 07 command + +// 20: Invalid command + +// 21: GameGuard control (BB) +// Format unknown + +// 22: GameGuard check (BB) +// An older version of BB used a 4-byte challenge in the flag field (and the +// command had no payload). The latest version uses this 16-byte challenge. + +struct SC_GameCardCheck_BB_22 { + uint8_t data[0x10]; +}; + +// 23: Invalid command +// 24: Invalid command +// 25: Invalid command +// 26: Invalid command +// 27: Invalid command +// 28: Invalid command +// 29: Invalid command +// 2A: Invalid command +// 2B: Invalid command +// 2C: Invalid command +// 2D: Invalid command +// 2E: Invalid command +// 2F: Invalid command +// 30: Invalid command +// 31: Invalid command +// 32: Invalid command +// 33: Invalid command +// 34: Invalid command +// 35: Invalid command +// 36: Invalid command +// 37: Invalid command +// 38: Invalid command +// 39: Invalid command +// 3A: Invalid command +// 3B: Invalid command +// 3C: Invalid command +// 3D: Invalid command +// 3E: Invalid command +// 3F: Invalid command + +// 40: Guild card search + +struct C_GuildCardSearch_40 { + le_uint32_t player_tag; + le_uint32_t searcher_serial_number; + le_uint32_t target_serial_number; +} __attribute__((packed)); + +template +struct S_GuildCardSearchResult { + le_uint32_t player_tag; + le_uint32_t searcher_serial_number; + le_uint32_t result_serial_number; + HeaderT reconnect_command_header; + S_Reconnect_19 reconnect_command; + char location_string[0x44]; + le_uint32_t menu_id; + le_uint32_t lobby_id; + char unused[0x3C]; + CharT name[0x20]; +}; +struct S_GuildCardSearchResult_PC_40 : S_GuildCardSearchResult { }; +struct S_GuildCardSearchResult_DC_GC_40 : S_GuildCardSearchResult { }; +struct S_GuildCardSearchResult_BB_40 : S_GuildCardSearchResult { }; + +// 41: Invalid command +// 42: Invalid command +// 43: Invalid command + +// 44 (C->S): Confirm open file +// The client sends a 44 to confirm each 44 sent by the server +// FOrmat not documented here + +// 44 (S->C): Open file for download +// Used for downloading online quests + +struct S_OpenFile_PC_GC_44_A6 { + char name[0x20]; + le_uint16_t unused; + le_uint16_t flags; + char filename[0x10]; + le_uint32_t file_size; +}; + +struct S_OpenFile_BB_44_A6 { + uint8_t unused[0x22]; + le_uint16_t flags; + char filename[0x10]; + le_uint32_t file_size; + char name[0x18]; +}; + +// 45: Invalid command +// 46: Invalid command +// 47: Invalid command +// 48: Invalid command +// 49: Invalid command +// 4A: Invalid command +// 4B: Invalid command +// 4C: Invalid command +// 4D: Invalid command +// 4E: Invalid command +// 4F: Invalid command +// 50: Invalid command +// 51: Invalid command +// 52: Invalid command +// 53: Invalid command +// 54: Invalid command +// 55: Invalid command +// 56: Invalid command +// 57: Invalid command +// 58: Invalid command +// 59: Invalid command +// 5A: Invalid command +// 5B: Invalid command +// 5C: Invalid command +// 5D: Invalid command +// 5E: Invalid command +// 5F: Invalid command + +// 60: Broadcast command +// When client sends this command, the server should forward it to all players +// in the same game/lobby +// See ReceiveSubcommands for details on contents + +// 61: Player data +// See PSOPlayerDataPC, PSOPlayerDataGC, PSOPlayerDataBB in Player.hh for this +// command's format + +// 62: Target command +// When client sends this command, the server should forward it to the player +// identified by header.flag in the same game/lobby +// See ReceiveSubcommands for details on contents + +// 63: Invalid command + +// 64 (S->C): Join game +// This is sent to the joining player; the other players get a 65 instead + +// Header flag = entry count +template +struct S_JoinGame { + le_uint32_t variations[0x20]; + // Unlike lobby join commands, these are filled in in their slot positions. + // That is, if there's one player in a game with ID 2, then the first two of + // these are blank and the player's data is in the third entry here. + LobbyDataT lobby_data[4]; + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t event; + uint8_t section_id; + uint8_t challenge_mode; + le_uint32_t rare_seed; + uint8_t episode; + uint8_t unused2; // Should be 1 for PSO PC? + uint8_t solo_mode; + uint8_t unused3; + struct PlayerEntry { + PlayerInventory inventory; + DispDataT disp; + }; + // This field is only present if the game (+client) is Episode 3. Similarly to + // lobby_data above, these are always present and filled in in slot positions. + PlayerEntry players_ep3[4]; +}; +struct S_JoinGame_PC_64 : S_JoinGame { }; +struct S_JoinGame_GC_64 : S_JoinGame { }; +struct S_JoinGame_BB_64 : S_JoinGame { }; + +// 65 (S->C): Other player joined game + +// Header flag = entry count (always 1 for 65 and 68; up to 0x0C for 67) +template +struct S_JoinLobby { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + le_uint16_t block_number; + le_uint16_t event; + le_uint32_t unused; + struct Entry { + LobbyDataT lobby_data; + PlayerInventory inventory; + DispDataT disp; + }; + // Note: not all of these will be filled in and sent if the lobby isn't full + // (the command size will be shorter than this struct's size) + Entry entries[12]; + + static inline size_t size(size_t used_entries) { + return offsetof(S_JoinLobby, entries) + used_entries * sizeof(Entry); + } +}; +struct S_JoinLobby_PC_65_67_68 : S_JoinLobby { }; +struct S_JoinLobby_GC_65_67_68 : S_JoinLobby { }; +struct S_JoinLobby_BB_65_67_68 : S_JoinLobby { }; + +// 66 (S->C): Other player left game +// Not sent to the leaving player + +// Header flag = leaving player ID (same as client_id); +struct S_LeaveLobby_66_69 { + uint8_t client_id; + uint8_t leader_id; + le_uint16_t unused; +}; + +// 67 (S->C): Join lobby +// This is sent to the joining player; the other players get a 68 instead +// Same format as 65 command + +// 68 (S->C): Other player joined lobby +// Same format as 65 command + +// 69 (S->C): Other player left lobby +// Not sent to the leaving player +// Same format as 66 command + +// 6A: Invalid command +// 6B: Invalid command + +// 6C: Broadcast command +// Same format and usage as 60 command + +// 6D: Target command +// Same format and usage as 62 command + +// 6E: Invalid command + +// 6F: Set game status +// This command is sent when a player is done loading and other players can then +// join the game +// On BB, this command is sent as 016F if a quest is in progress and the game +// should not be joined by anyone else + +// 70: Invalid command +// 71: Invalid command +// 72: Invalid command +// 73: Invalid command +// 74: Invalid command +// 75: Invalid command +// 76: Invalid command +// 77: Invalid command +// 78: Invalid command +// 79: Invalid command +// 7A: Invalid command +// 7B: Invalid command +// 7C: Invalid command +// 7D: Invalid command +// 7E: Invalid command +// 7F: Invalid command +// 80: Invalid command + +// 81: Simple mail +// Format is the same in both directions. On GC (and probably other versions +// too) the unused space after the text contains uninitialized memory when the +// client sends this command + +struct SC_SimpleMail_GC_81 { + le_uint32_t player_tag; + le_uint32_t from_serial_number; + char from_name[0x10]; + le_uint32_t to_serial_number; + char text[0x200]; +}; + +// 82: Invalid command + +// 83 (S->C): Lobby menu +// This sets the menu item IDs that the client uses for the lobby teleport menu + +// Command is a list of these; header.flag is the entry count (15, or 20 on Ep3) +struct S_LobbyListEntry_83 { + le_uint32_t menu_id; + le_uint32_t item_id; + le_uint32_t unused; +}; + +// 84 (C->S): Choose lobby + +struct C_LobbySelection_84 { + le_uint32_t menu_id; + le_uint32_t item_id; +}; + +// 85: Invalid command +// 86: Invalid command +// 87: Invalid command + +// 88 (S->C): Lobby arrows + +// Command is a list of these; header.flag is the entry count +struct S_ArrowUpdateEntry_88 { + le_uint32_t player_tag; + le_uint32_t serial_number; + le_uint32_t arrow_color; +}; + +// 89 (C->S): Set lobby arrow +// Header flag = arrow color number; no other arguments +// Server should send an 88 command to all players in the lobby + +// 8A (C->S): Request lobby/game name +// No arguments + +// 8A (S->C): Lobby/game name +// Contents is a string (char16_t on PC/BB, char on DC/GC) containing the lobby +// or game name + +// 8B: Invalid command +// 8C: Invalid command +// 8D: Invalid command +// 8E: Invalid command +// 8F: Invalid command +// 90: Invalid command +// 91: Invalid command +// 92: Invalid command + +// 93 (C->S): Log in (BB) + +struct C_Login_BB_93 { + char unused[0x14]; + char username[0x10]; + char unused2[0x20]; + char password[0x10]; + char unused3[0x30]; + ClientConfig cfg; +}; + +// 94: Invalid command + +// 95 (S->C): Request player data +// No arguments +// Client will respond with a 61 command + +// 96 (C->S): Client checksum + +struct C_ClientChecksum_GC_96 { + le_uint64_t checksum; +}; + +// 97 (S->C): Save to memory card +// No arguments + +// 98 (C->S): Leave game +// Same format a 61 command +// Client will send an 84 when it's ready to join a lobby + +// 99 (C->S): Server time accepted +// No arguments + +// 9A (S->C): Verify result +// 02 = license ok +// TODO: figure out the other codes + +// 9A (C->S): Initial login (no password or client config) + +struct C_Login_DC_PC_GC_9A { + char unused[0x20]; + char serial_number[0x10]; + char access_key[0x10]; +}; + +// 9B: Invalid command + +// 9C (C->S): Log in result +// 01 = license ok +// TODO: figure out the other codes + +// 9C (C->S): Register + +struct C_Register_DC_PC_GC_9C { + char unused[8]; + le_uint32_t sub_version; + le_uint32_t unused2; + char serial_number[0x30]; + char access_key[0x30]; + char password[0x30]; +}; + +// 9D (C->S): Log in with client config +// Same as 9E? (We treat them identicall in newserv) + +// 9E (C->S): Log in with client config + +struct C_Login_PC_GC_9D_9E { + le_uint32_t player_tag; // 00 00 01 00 if guild card is set (via 04) + le_uint32_t guild_card_number; // FF FF FF FF if not set + le_uint32_t unused1[2]; + le_uint32_t sub_version; + uint8_t unused2[0x24]; // 00 01 00 00 ... (rest is 00) + char serial_number[0x10]; + char access_key[0x10]; + char serial_number2[0x30]; + char access_key2[0x30]; + char name[0x10]; + // Note: there are 8 bytes at the end of cfg that are technically not + // included in the client config on GC, but the field after it is + // sufficiently large and unused anyway + ClientConfig cfg; + uint8_t unused4[0x5C]; +}; + +// 9F: Invalid command + +// A0 (C->S): Change ship +// No arguments + +// A0 (S->C): Ship select menu +// Same as 07 command + +// A1 (C->S): Change block +// No arguments + +// A1 (S->C): Block select menu +// Same as 07 command + +// A2 (C->S): Request quest list +// No arguments + +// A2 (S->C): Quest menu + +template +struct S_QuestMenuEntry { + le_uint32_t menu_id; + le_uint32_t item_id; + CharT name[0x20]; + CharT short_desc[0x70]; +}; +struct S_QuestMenuEntry_PC_A2_A4 : S_QuestMenuEntry { }; +struct S_QuestMenuEntry_GC_A2_A4 : S_QuestMenuEntry { }; + +struct S_QuestMenuEntry_BB_A2_A4 { + le_uint32_t menu_id; + le_uint32_t item_id; + char16_t name[0x20]; + char16_t short_desc[0x7A]; // Why is this not the same as PC? Mysteries of SEGA's engineering... +}; + +// A3 (S->C): Quest information +// Same format as 1A/D5 command (plain text) + +// A4 (S->C): Download quest menu +// Same format as A2 + +// A5: Invalid command + +// A6: Open file for download +// Used for download quests and GBA games +// Same format as 44 + +// A7: Write download file +// Same format as 13 + +// A8: Invalid command + +// A9: Quest menu closed +// No arguments + +// AA: Invalid command +// AB: Invalid command + +// AC (C->S): Ready to start quest +// AC (S->C): Start quest +// No arguments +// When all players in a game have sent an AC to the server, the server should +// send them all an AC back, which starts the quest for all players at +// (approximately) the same time +// Sending this command to a GC client when it is not waiting to start a quest +// will cause it to crash + +// AD: Invalid command +// AE: Invalid command +// AF: Invalid command + +// B0: Text message +// Same format as 1A/D5 command (plain text) + +// B1 (C->S): Request server time +// No arguments + +// B1 (S->C): Server time +// Contents is a string of format "%Y:%m:%d: %H:%M:%S.000" +// For example: 2022:03:30: 15:36:42.000 + +// B2 (S->C): Write memory +// GC v1.0 and v1.1 only +// Format unknown +// Client will respond with a B3 command + +// B3 (C->S): Write memory response +// GC v1.0 and v1.1 only +// Format unknown + +// B4: Invalid command +// B5: Invalid command +// B6: Invalid command + +// B7: Rank update (Episode 3) + +struct S_RankUpdate_GC_Ep3_B7 { + le_uint32_t rank; + char rank_text[0x0C]; + le_uint32_t meseta; + le_uint32_t max_meseta; + le_uint32_t jukebox_songs_unlocked; +}; + +// B8 (S->C): Send card definitions (Episode 3) +// Contents is a single little-endian le_uint32_t specifying the size of the +// (PRS-compressed) data, followed immediately by the data + +// B9: Invalid command + +// BA: Meseta transaction (Episode 3) + +struct C_Meseta_GC_Ep3_BA { + le_uint32_t transaction_num; + le_uint32_t value; + le_uint32_t unknown_token; +}; + +struct S_Meseta_GC_Ep3_BA { + le_uint32_t remaining_meseta; + le_uint32_t unknown; + le_uint32_t unknown_token; // Should match the token sent by the client +}; + +// BB: Invalid command +// BC: Invalid command +// BD: Invalid command +// BE: Invalid command +// BF: Invalid command + +// C0 (C->S): Request choice search options +// No arguments + +// C0 (S->C): Choice search options + +// Command is a list of these; header.flag is the entry count (incl. top-level) +template +struct S_ChoiceSearchEntry { + // category_ids are nonzero; if the high byte is nonzero then the category can + // be set by the user at any time; otherwise it can't. + le_uint16_t parent_category_id; // 0 for top-level categories + le_uint16_t category_id; + CharT text[0x1C]; +}; +struct S_ChoiceSearchEntry_DC_GC_C0 : S_ChoiceSearchEntry { }; +struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry { }; + +// Top-level categories are things like "Level", "Class", etc. +// Choices for each top-level category immediately follow the category, so +// a reasonable order of items is (for example): +// 00 00 11 01 "Preferred difficulty" +// 11 01 01 01 "Normal" +// 11 01 02 01 "Hard" +// 11 01 03 01 "Very Hard" +// 11 01 04 01 "Ultimate" +// 00 00 22 00 "Character class" +// 22 00 01 00 "HUmar" +// 22 00 02 00 "HUnewearl" +// etc. + +// C1 (C->S): Create game + +template +struct C_CreateGame { + le_uint32_t unused[2]; + CharT name[0x10]; + CharT password[0x10]; + uint8_t difficulty; + uint8_t battle_mode; + uint8_t challenge_mode; + uint8_t episode; // unused on DC/PC +}; +struct C_CreateGame_DC_GC_C1_EC : C_CreateGame { }; +struct C_CreateGame_PC_C1 : C_CreateGame { }; +struct C_CreateGame_BB_C1 : C_CreateGame { + uint8_t solo_mode; + uint8_t unused2[3]; +}; + +// C2 (C->S): Set choice search parameters + +struct C_SetChoiceSearchParameters_C2 { + le_uint16_t disabled; // 0 = enabled, 1 = disabled + le_uint16_t unused; + struct Entry { + le_uint16_t parent_category_id; + le_uint16_t category_id; + }; + Entry entries[0]; +}; + +// C3 (C->S): Execute choice search + +struct C_ExecuteChoiceSearch_C3 { + le_uint32_t unknown; + struct Entry { + le_uint16_t parent_category_id; + le_uint16_t category_id; + }; + Entry entries[0]; +}; + +// C4 (S->C): Choice search results + +// Command is a list of these; header.flag is the entry count +struct S_ChoiceSearchResultEntry_GC_C4 { + le_uint32_t guild_card_number; + char name[0x10]; // No language marker, as usual on GC + char info_string[0x20]; // Usually something like " Lvl " + // Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME" + // If target is in game, for example, "Game Name,BLOCK01,Alexandria" + // If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria" + char locator_string[0x34]; + // Server IP and port for "meet user" option + le_uint32_t server_ip; + le_uint16_t server_port; + le_uint16_t unused1; + le_uint32_t menu_id; + le_uint32_t lobby_id; // These two are guesses + le_uint32_t game_id; // Zero if target is in a lobby rather than a game + uint8_t unused2[0x58]; +}; + +// C5 (S->C): Challenge rank update +// TODO: Document format for this command + +// C6 (C->S): Set blocked senders list + +struct C_SetBlockedSenders_C6 { + le_uint32_t blocked_senders[30]; +}; + +// C7 (C->S): Enable simple mail auto-reply +// Same format as 1A/D5 command (plain text) + +// C8 (C->S): Disable simple mail auto-reply + +// C9: Broadcast command (Episode 3) +// Same as 60, but only send to Episode 3 clients + +// CA (C->S): Ep3 server data request + +// CB: Broadcast command (Episode 3) +// Same as 60, but only send to Episode 3 clients + +// CC: Invalid command +// CD: Invalid command +// CE: Invalid command +// CF: Invalid command + +// D0 (C->S): Execute trade via trade window +// General sequence: client sends D0, server sends D1 to that client, server +// sends D3 to other client, server sends D4 to both (?) clients +// Format unknown + +// D1 (S->C): Confirm trade to initiator +// No arguments + +// D2: Invalid command + +// D3 (S->C): Execute trade with accepter +// Format unknown; appears to be same as D0 + +// D4 (S->C): Close trade +// No arguments + +// D5: Large message box +// Same as 1A command + +// D6 (C->S): Large message box closed (GC) +// No arguments +// DC and PC do not send this command at all. GC v1.0 and v1.1 will send this +// command when any large message box is closed; GC Plus and Episode 3 will send +// this only for large message boxes that are sent before the client has joined +// a lobby. + +// D7 (C->S): Request GBA game file + +struct C_GBAGameRequest_GC_D7 { + char filename[0x10]; +}; + +// D8 (S->C): Info board + +// Command is a list of these; header.flag is the entry count +template +struct S_InfoBoardEntry_D8 { + CharT name[0x10]; + CharT message[0xAC]; +}; +struct S_InfoBoardEntry_PC_BB_D8 : S_InfoBoardEntry_D8 { }; +struct S_InfoBoardEntry_DC_GC_D8 : S_InfoBoardEntry_D8 { }; + +// D9 (C->S): Write info board +// Contents are plain text, like 1A/D5 + +// DA (S->C): Change lobby event +// Header flag = new event number. No other arguments + +// DB (S->C): Verify license (GC) + +struct C_VerifyLicense_GC_DB { + char unused[0x20]; + char serial_number[0x10]; + char access_key[0x10]; + char unused2[0x08]; + le_uint32_t sub_version; + char serial_number2[0x30]; + char access_key2[0x30]; + char password[0x30]; +}; + +// DC: Player menu state (Episode 3) +// No arguments. Client expects server to respond with command = DC, flag = 0 + +// DC: Guild card data (BB) + +struct C_GuildCardDataRequest_BB_DC { + le_uint32_t unknown; + le_uint32_t chunk_index; + le_uint32_t cont; +}; + +struct S_GuildCardHeader_BB_01DC { + le_uint32_t unknown; // should be 1 + le_uint32_t filesize; // 0x0000490 + le_uint32_t checksum; +}; + +// Command 02DC is used to send the guild card file data. It goes like this: +// uint32_t unknown; // 0 +// uint32_t chunk_index; +// uint8_t data[0x6800, or less if last chunk] + +// DD: Invalid command +// DE: Unknown (used by BB) +// DF: Invalid command + +// E0 (S->C): Tournament list (Episode 3) + +// Command is a list of these; header.flag is the entry count. Some servers +// always send 0x740 bytes even if the entries don't fill the space. +struct S_TournamentEntry_GC_Ep3_E0 { + le_uint32_t menu_id; + le_uint32_t item_id; + uint8_t unknown[0x30]; +}; + +// E0 (C->S): Request team and key config (BB) + +// E1: Invalid command + +// E2 (C->S): Tournament control (Episode 3) +// Flag = 0 => request tournament list (server responds with E0) +// Flag = 1 => check tournament +// Flag = 2 => cancel tournament entry +// Flag = 3 => create tournament spectator team +// Flag = 4 => join tournament spectator team + +// E2 (S->C): Tournament entry control (Episode 3) + +// E2 (S->C): Team and key config (BB) +// See KeyAndTeamConfigBB in Player.hh for format + +// E3: Player previews (BB) + +struct C_PlayerPreviewRequest_BB_E3 { + le_uint32_t player_index; + le_uint32_t unused; +}; + +struct S_PlayerPreview_BB_E3 { + le_uint32_t player_index; + PlayerDispDataBBPreview preview; +}; + +// E4: CARD lobby game (Episode 3) + +// Header flag = seated state (1 = present, 0 = leaving) +struct C_CardLobbyGame_GC_E4 { + le_uint16_t table_number; + le_uint16_t seat_number; +}; + +// Header flag = 2 +struct S_CardLobbyGame_GC_E4 { + struct Entry { + le_uint32_t present; // 1 = player present, 0 = no player + le_uint32_t guild_card_number; + }; + Entry entries[4]; +}; + +// E4 (S->C): Player choice or no player present (BB) + +struct S_ApprovePlayerChoice_BB_00E4 { + le_uint32_t player_index; + le_uint32_t unused; +}; + +struct S_PlayerPreview_NoPlayer_BB_E4 { + le_uint32_t player_index; + le_uint32_t error; +}; + +// E5 (C->S): Create character (BB) + +struct C_CreateCharacter_BB_E5 { + le_uint32_t player_index; + PlayerDispDataBBPreview preview; +}; + +// E6: Spectator team list (Episode 3) +// Same format as 08 command + +// E6 (S->C): Set guild card number and update client config (BB) + +struct S_ClientInit_BB_E6 { + le_uint32_t error; + le_uint32_t player_tag; + le_uint32_t guild_card_number; + le_uint32_t team_id; + ClientConfig cfg; + le_uint32_t caps; // should be 0x00000102 +}; + +// E7: Save or load full player data +// See export_bb_player_data() in Player.cc for format + +// E8 (C->S): Client checksum (BB) +// E8 (S->C): Accept client checksum (BB) + +struct S_AcceptClientChecksum_BB_02E8 { + le_uint32_t verify; + le_uint32_t unused; +}; + +// E9: Invalid command +// EA: Team control (BB) + +// EB: Send stream file index and chunks + +// Command is a list of these; header.flag is the entry count. +struct S_StreamFileIndexEntry_BB_01EB { + le_uint32_t size; + le_uint32_t checksum; + le_uint32_t offset; + char filename[0x40]; +} __attribute__((packed)); + +struct S_StreamFileChunk_BB_02EB { + le_uint32_t chunk_index; + uint8_t data[0x6800]; +}; + +// EC: Create game (Episode 3) +// Same format as C1; some fields are unused (e.g. episode, difficulty) +// TODO: Where does the "view battle allowed" flag end up? + +// EC: Leave character select (BB) + +// ED (C->S): Update account data (BB) +// TODO: Actually define these structures and don't just treat them as raw data + +union C_UpdateAccountData_BB_ED { + le_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 +} __attribute__((packed)); + +// EE: Scrolling message (BB) +// Contents are plain text (char16_t) + +// EF: Invalid command +// F0: Invalid command +// F1: Invalid command +// F2: Invalid command +// F3: Invalid command +// F4: Invalid command +// F5: Invalid command +// F6: Invalid command +// F7: Invalid command +// F8: Invalid command +// F9: Invalid command +// FA: Invalid command +// FB: Invalid command +// FC: Invalid command +// FD: Invalid command +// FE: Invalid command +// FF: Invalid command + + + + + +struct S_SendGuildCard_GC { + uint8_t subcommand; + uint8_t subsize; + le_uint16_t unused; + le_uint32_t player_tag; + le_uint32_t serial_number; + char name[0x18]; + char desc[0x6C]; + uint8_t reserved1; + uint8_t reserved2; + uint8_t section_id; + uint8_t char_class; +}; + +struct S_SendGuildCard_BB { + uint8_t subcommand; + uint8_t subsize; + le_uint16_t unused; + le_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; +}; + +struct S_DropItem_BB { + uint8_t subcommand; + uint8_t subsize; + le_uint16_t unused; + uint8_t area; + uint8_t dude; + le_uint16_t request_id; + float x; + float y; + le_uint32_t unused2; + ItemData data; +}; + +struct S_DropStackedItem_BB { + uint8_t subcommand; + uint8_t subsize; + le_uint16_t unused; + le_uint16_t area; + le_uint16_t unused2; + float x; + float y; + le_uint32_t unused3; + ItemData data; +}; + +struct S_PickUpItem_BB { + uint8_t subcommand; + uint8_t subsize; + le_uint16_t client_id; + le_uint16_t client_id2; + le_uint16_t area; + le_uint32_t item_id; +}; + +struct S_CreateInventoryItem_BB { + uint8_t subcommand; + uint8_t subsize; + le_uint16_t client_id; + ItemData item; + le_uint32_t unused; +}; + +struct S_DestroyItem_BB { + uint8_t subcommand; + uint8_t subsize; + le_uint16_t client_id; + le_uint32_t item_id; + le_uint32_t amount; +}; + +struct S_BankContentsHeader_BB { + uint8_t subcommand; + uint8_t unused1; + le_uint16_t unused2; + le_uint32_t size; // same as size in overall command header + le_uint32_t checksum; // can be random; client won't notice + le_uint32_t numItems; + le_uint32_t meseta; +}; + +struct S_ShopContents_BB { + uint8_t subcommand; // B6 + uint8_t size; // 2C regardless of the number of items?? + le_uint16_t params; // 037F + uint8_t shop_type; + uint8_t num_items; + le_uint16_t unused; + ItemData entries[20]; +}; + + + +#pragma pack(pop) diff --git a/src/Lobby.cc b/src/Lobby.cc index eaeba54f..ba4fbefe 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -152,7 +152,7 @@ shared_ptr Lobby::find_client(const char16_t* identifier, (this->clients[x]->license->serial_number == serial_number)) { return this->clients[x]; } - if (identifier && !char16cmp(this->clients[x]->player.disp.name, identifier, 0x10)) { + if (identifier && !char16ncmp(this->clients[x]->player.disp.name, identifier, 0x10)) { return this->clients[x]; } } diff --git a/src/Player.cc b/src/Player.cc index 7524b266..414e2253 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -16,11 +16,29 @@ 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;" +#define PLAYER_FILE_SIGNATURE "newserv player file format; 10 sections present; sequential;" +#define ACCOUNT_FILE_SIGNATURE "newserv account file format; 7 sections present; sequential;" +void PlayerDispDataPCGC::enforce_pc_limits() { + // PC has fewer classes, so we'll substitute some here + if (this->char_class == 11) { + this->char_class = 0; // fomar -> humar + } else if (this->char_class == 10) { + this->char_class = 1; // ramarl -> hunewearl + } else if (this->char_class == 9) { + this->char_class = 5; // hucaseal -> racaseal + } + + // if the player is still not a valid class, make them appear as the "ninja" NPC + if (this->char_class > 8) { + this->extra_model = 0; + this->v2_flags |= 2; + } + this->version = 2; +} + // converts PC/GC player data to BB format PlayerDispDataBB PlayerDispDataPCGC::to_bb() const { PlayerDispDataBB bb; @@ -38,7 +56,7 @@ PlayerDispDataBB PlayerDispDataPCGC::to_bb() const { bb.experience = this->experience; bb.meseta = this->meseta; memset(bb.guild_card, 0, sizeof(bb.guild_card)); - strcpy(bb.guild_card, " 0"); + strncpy(bb.guild_card, " 0", 0x10); bb.unknown3[0] = this->unknown3[0]; bb.unknown3[1] = this->unknown3[1]; bb.name_color = this->name_color; @@ -119,7 +137,7 @@ PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { pre.level = this->level; pre.experience = this->experience; memset(pre.guild_card, 0, sizeof(pre.guild_card)); - strcpy(pre.guild_card, this->guild_card); + strncpy(pre.guild_card, this->guild_card, 0x10); pre.unknown3[0] = this->unknown3[0]; pre.unknown3[1] = this->unknown3[1]; pre.name_color = this->name_color; @@ -142,7 +160,7 @@ PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { pre.proportion_x = this->proportion_x; pre.proportion_y = this->proportion_y; memset(pre.name, 0, sizeof(pre.name)); - char16cpy(pre.name, this->name, 16); + char16ncpy(pre.name, this->name, 16); pre.play_time = this->play_time; return pre; } @@ -151,7 +169,7 @@ void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) { this->level = pre.level; this->experience = pre.experience; memset(this->guild_card, 0, sizeof(this->guild_card)); - strcpy(this->guild_card, pre.guild_card); + strncpy(this->guild_card, pre.guild_card, 0x10); this->unknown3[0] = pre.unknown3[0]; this->unknown3[1] = pre.unknown3[1]; this->name_color = pre.name_color; @@ -174,7 +192,7 @@ void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) { this->proportion_x = pre.proportion_x; this->proportion_y = pre.proportion_y; memset(this->name, 0, sizeof(this->name)); - char16cpy(this->name, pre.name, 16); + char16ncpy(this->name, pre.name, 0x10); this->play_time = 0; } @@ -226,55 +244,16 @@ 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) memset(this->info_board, 0, sizeof(this->info_board)); - char16cpy(this->info_board, bb.info_board, 0xAC); + char16ncpy(this->info_board, bb.info_board, 0xAC); memcpy(&this->blocked, bb.blocked, sizeof(uint32_t) * 30); memset(this->auto_reply, 0, sizeof(this->auto_reply)); if (bb.auto_reply_enabled) { - char16cpy(this->auto_reply, bb.auto_reply, 0xAC); + char16ncpy(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; @@ -285,11 +264,11 @@ PlayerBB Player::export_bb_player_data() const { bb.bank = this->bank; bb.serial_number = this->serial_number; memset(bb.name, 0, sizeof(bb.name)); - char16cpy(bb.name, this->disp.name, 24); + char16ncpy(bb.name, this->disp.name, 24); memset(bb.team_name, 0, sizeof(bb.team_name)); - char16cpy(bb.team_name, this->team_name, 16); + char16ncpy(bb.team_name, this->team_name, 16); memset(bb.guild_card_desc, 0, sizeof(bb.guild_card_desc)); - char16cpy(bb.guild_card_desc, this->guild_card_desc, 0x58); + char16ncpy(bb.guild_card_desc, this->guild_card_desc, 0x58); bb.reserved1 = 0; bb.reserved2 = 0; bb.section_id = this->disp.section_id; @@ -298,9 +277,9 @@ PlayerBB Player::export_bb_player_data() const { memcpy(bb.symbol_chats, this->symbol_chats, 0x04E0); memcpy(bb.shortcuts, this->shortcuts, 0x0A40); memset(bb.auto_reply, 0, sizeof(bb.auto_reply)); - char16cpy(bb.auto_reply, this->auto_reply, 0xAC); + char16ncpy(bb.auto_reply, this->auto_reply, 0xAC); memset(bb.info_board, 0, sizeof(bb.info_board)); - char16cpy(bb.info_board, this->info_board, 0xAC); + char16ncpy(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); @@ -344,13 +323,13 @@ void Player::load_account_data(const string& filename) { memcpy(&this->shortcuts, &account.shortcuts, 0x0A40); memcpy(&this->symbol_chats, &account.symbol_chats, 0x04E0); memset(this->team_name, 0, sizeof(this->team_name)); - char16cpy(this->team_name, account.team_name, 16); + char16ncpy(this->team_name, account.team_name, 16); } void Player::save_account_data(const string& filename) const { SavedAccountBB account; - strcpy(account.signature, ACCOUNT_FILE_SIGNATURE); + strncpy(account.signature, ACCOUNT_FILE_SIGNATURE, sizeof(account.signature)); memcpy(&account.blocked, &this->blocked, sizeof(uint32_t) * 30); account.guild_cards = this->guild_cards; account.key_config = this->key_config; @@ -358,7 +337,7 @@ void Player::save_account_data(const string& filename) const { memcpy(&account.shortcuts, &this->shortcuts, 0x0A40); memcpy(&account.symbol_chats, &this->symbol_chats, 0x04E0); memset(account.team_name, 0, sizeof(account.team_name)); - char16cpy(account.team_name, this->team_name, 16); + char16ncpy(account.team_name, this->team_name, 16); save_file(filename, &account, sizeof(account)); } @@ -371,14 +350,14 @@ void Player::load_player_data(const string& filename) { } memset(this->auto_reply, 0, sizeof(this->auto_reply)); - char16cpy(this->auto_reply, player.auto_reply, 0xAC); + char16ncpy(this->auto_reply, player.auto_reply, 0xAC); this->bank = player.bank; memcpy(&this->challenge_data, &player.challenge_data, 0x0140); this->disp = player.disp; memset(this->guild_card_desc, 0, sizeof(this->guild_card_desc)); - char16cpy(this->guild_card_desc, player.guild_card_desc, 0x58); + char16ncpy(this->guild_card_desc, player.guild_card_desc, 0x58); memset(this->info_board, 0, sizeof(this->info_board)); - char16cpy(this->info_board, player.info_board, 0xAC); + char16ncpy(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); @@ -388,17 +367,17 @@ void Player::load_player_data(const string& filename) { void Player::save_player_data(const string& filename) const { SavedPlayerBB player; - strcpy(player.signature, PLAYER_FILE_SIGNATURE); + strncpy(player.signature, PLAYER_FILE_SIGNATURE, sizeof(player.signature)); player.preview = this->disp.to_preview(); memset(player.auto_reply, 0, sizeof(player.auto_reply)); - char16cpy(player.auto_reply, this->auto_reply, 0xAC); + char16ncpy(player.auto_reply, this->auto_reply, 0xAC); player.bank = this->bank; memcpy(&player.challenge_data, &this->challenge_data, 0x0140); player.disp = this->disp; memset(player.guild_card_desc, 0, sizeof(player.guild_card_desc)); - char16cpy(player.guild_card_desc,this->guild_card_desc, 0x58); + char16ncpy(player.guild_card_desc,this->guild_card_desc, 0x58); memset(player.info_board, 0, sizeof(player.info_board)); - char16cpy(player.info_board, this->info_board, 0xAC); + char16ncpy(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); diff --git a/src/Player.hh b/src/Player.hh index 1f246891..151c1759 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -123,6 +123,7 @@ struct PlayerDispDataPCGC { // 0xD0 in size uint8_t config[0x48]; uint8_t technique_levels[0x14]; + void enforce_pc_limits(); PlayerDispDataBB to_bb() const; } __attribute__((packed)); @@ -163,7 +164,7 @@ struct PlayerDispDataBB { uint32_t level; uint32_t experience; uint32_t meseta; - char guild_card[16]; + char guild_card[0x10]; uint32_t unknown3[2]; uint32_t name_color; uint8_t extra_model; @@ -189,6 +190,7 @@ struct PlayerDispDataBB { uint8_t config[0xE8]; uint8_t technique_levels[0x14]; + inline void enforce_pc_limits() { } PlayerDispDataPCGC to_pcgc() const; PlayerDispDataBBPreview to_preview() const; void apply_preview(const PlayerDispDataBBPreview&); @@ -277,7 +279,8 @@ struct PlayerLobbyDataGC { struct PlayerLobbyDataBB { uint32_t player_tag; uint32_t guild_card; - uint32_t unknown1[5]; + be_uint32_t ip_address; // Guess - the official builds didn't use this, but all other versions have it + uint32_t unknown1[4]; uint32_t client_id; char16_t name[16]; uint32_t unknown2; @@ -310,18 +313,6 @@ struct PSOPlayerDataBB { // for command 0x61 char16_t auto_reply[0]; } __attribute__((packed)); -// PC/GC lobby player data (used in lobby/game join commands) -struct PlayerLobbyJoinDataPCGC { - PlayerInventory inventory; - PlayerDispDataPCGC disp; -} __attribute__((packed)); - -// BB lobby player data (used in lobby/game join commands) -struct PlayerLobbyJoinDataBB { - PlayerInventory inventory; - PlayerDispDataBB disp; -} __attribute__((packed)); - // complete BB player data format (used in E7 command) struct PlayerBB { PlayerInventory inventory; // 0000 // player @@ -416,9 +407,6 @@ struct Player { 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); @@ -434,3 +422,35 @@ std::string filename_for_player_bb(const std::string& username, uint8_t player_i 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); + + + +template +DestT convert_player_disp_data(const SrcT&) { + static_assert(always_false::v, + "unspecialized strcpy_t should never be called"); +} + +template <> +inline PlayerDispDataPCGC convert_player_disp_data( + const PlayerDispDataPCGC& src) { + return src; +} + +template <> +inline PlayerDispDataPCGC convert_player_disp_data( + const PlayerDispDataBB& src) { + return src.to_pcgc(); +} + +template <> +inline PlayerDispDataBB convert_player_disp_data( + const PlayerDispDataPCGC& src) { + return src.to_bb(); +} + +template <> +inline PlayerDispDataBB convert_player_disp_data( + const PlayerDispDataBB& src) { + return src; +} diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index b03ea6d4..f56b5685 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -137,9 +137,11 @@ void ProxyServer::on_client_connect( case GameVersion::GC: { uint32_t server_key = random_object(); uint32_t client_key = random_object(); - string data = prepare_server_init_contents_dc_pc_gc(false, server_key, client_key); - send_command(session->bev.get(), session->version, session->crypt_out.get(), 0x02, - 0, data.data(), data.size(), "unlinked proxy client"); + auto cmd = prepare_server_init_contents_dc_pc_gc( + false, server_key, client_key); + send_command(session->bev.get(), session->version, + session->crypt_out.get(), 0x02, 0, &cmd, sizeof(cmd), + "unlinked proxy client"); session->crypt_out.reset(new PSOGCEncryption(server_key)); session->crypt_in.reset(new PSOGCEncryption(client_key)); break; @@ -191,11 +193,11 @@ void ProxyServer::UnlinkedSession::on_client_input() { if (command != 0x9E) { log(ERROR, "[ProxyServer] Received unexpected command %02hX", command); should_close_unlinked_session = true; - } else if (data.size() < sizeof(LoginCommand_GC_9E) - 0x64) { + } else if (data.size() < sizeof(C_Login_PC_GC_9D_9E) - 0x64) { log(ERROR, "[ProxyServer] Login command is too small"); should_close_unlinked_session = true; } else { - const auto* cmd = reinterpret_cast(data.data()); + const auto* cmd = reinterpret_cast(data.data()); uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); try { license = this->server->state->license_manager->verify_gc( @@ -234,9 +236,6 @@ void ProxyServer::UnlinkedSession::on_client_input() { log(ERROR, "[ProxyServer/%08" PRIX32 "] Client configuration is invalid; cannot open session", license->serial_number); } else { - // If the client goes back to newserv, we need to set the welcome - // message flag so the server will know what to do - client_config.flags |= ClientFlag::AT_WELCOME_MESSAGE; session.reset(new LinkedSession( this->server, this->local_port, @@ -506,11 +505,7 @@ void ProxyServer::LinkedSession::on_client_input() { } uint8_t leaving_id = x; uint8_t leader_id = this->lobby_client_id; - struct { - uint8_t client_id; - uint8_t leader_id; - uint16_t unused; - } __attribute__((packed)) cmd = {leaving_id, leader_id, 0}; + S_LeaveLobby_66_69 cmd = {leaving_id, leader_id, 0}; send_command(this->client_bev.get(), this->version, this->client_output_crypt.get(), 0x69, leaving_id, &cmd, sizeof(cmd), name.c_str()); @@ -519,11 +514,7 @@ void ProxyServer::LinkedSession::on_client_input() { // Restore the newserv client config, so the client gets its newserv // guild card number back and the login server knows e.g. not to show // the welcome message (if the appropriate flag is set) - struct { - uint32_t player_tag; - uint32_t serial_number; - ClientConfig config; - } __attribute__((packed)) update_client_config_cmd = { + S_UpdateClientConfig_DC_PC_GC_04 update_client_config_cmd = { 0x00010000, this->license->serial_number, this->newserv_client_config, @@ -538,7 +529,7 @@ void ProxyServer::LinkedSession::on_client_input() { const auto& port_name = version_to_port_name.at(static_cast( this->version)); - ReconnectCommand_19 reconnect_cmd = { + S_Reconnect_19 reconnect_cmd = { 0, this->server->state->named_port_configuration.at(port_name).port, 0}; // If the client is on a virtual connection, we can use any address @@ -607,11 +598,11 @@ void ProxyServer::LinkedSession::on_server_input() { } // Most servers don't include after_message or have a shorter // after_message than newserv does, so don't require it - if (data.size() < offsetof(ServerInitCommand_GC_02_17, after_message)) { + if (data.size() < offsetof(S_ServerInit_DC_GC_02_17, after_message)) { throw std::runtime_error("init encryption command is too small"); } - const auto* cmd = reinterpret_cast( + const auto* cmd = reinterpret_cast( data.data()); // This doesn't get forwarded to the client, so don't recreate the @@ -632,7 +623,7 @@ void ProxyServer::LinkedSession::on_server_input() { // We don't let the client do this because it believes it already // did (when it was in an unlinked session). if (command == 0x17) { - VerifyLicenseCommand_GC_DB cmd; + C_VerifyLicense_GC_DB cmd; memset(&cmd, 0, sizeof(cmd)); snprintf(cmd.serial_number, sizeof(cmd.serial_number), "%08" PRIX32 "", this->license->serial_number); @@ -654,7 +645,7 @@ void ProxyServer::LinkedSession::on_server_input() { case 0x9A: { should_forward = false; - LoginCommand_GC_9E cmd; + C_Login_PC_GC_9D_9E cmd; memset(&cmd, 0, sizeof(cmd)); if (this->guild_card_number == 0) { @@ -684,25 +675,21 @@ void ProxyServer::LinkedSession::on_server_input() { 0x9E, 0x01, &cmd, - this->guild_card_number ? (offsetof(LoginCommand_GC_9E, cfg) + 0x20) : sizeof(cmd), + this->guild_card_number ? (offsetof(C_Login_PC_GC_9D_9E, cfg) + 0x20) : sizeof(cmd), name.c_str()); break; } case 0x04: { - struct Contents { - uint32_t player_tag; - uint32_t guild_card_number; - uint8_t client_config[0]; - } __attribute__((packed)); - - if (data.size() < sizeof(Contents)) { + // Some servers send a short 04 command if they don't use all of the + // 0x20 bytes available. We should be prepared to handle that. + if (data.size() < offsetof(S_UpdateClientConfig_DC_PC_GC_04, cfg)) { throw std::runtime_error("set security data command is too small"); } bool had_guild_card_number = (this->guild_card_number != 0); - const auto* cmd = reinterpret_cast(data.data()); + const auto* cmd = reinterpret_cast(data.data()); this->guild_card_number = cmd->guild_card_number; log(INFO, "[ProxyServer/%08" PRIX32 "] Guild card number set to %" PRIX32, this->license->serial_number, this->guild_card_number); @@ -720,7 +707,8 @@ void ProxyServer::LinkedSession::on_server_input() { ? "t Lobby Server. Copyright SEGA E" : "t Port Map. Copyright SEGA Enter", 0x20); - memcpy(this->remote_client_config_data, cmd->client_config, min(data.size() - sizeof(Contents), 0x20)); + memcpy(this->remote_client_config_data, &cmd->cfg, + min(data.size() - sizeof(S_UpdateClientConfig_DC_PC_GC_04), 0x20)); // If the guild card number was not set, pretend (to the server) // that this is the first 04 command the client has received. The @@ -739,11 +727,11 @@ void ProxyServer::LinkedSession::on_server_input() { } case 0x19: { - if (data.size() < sizeof(ReconnectCommand_19)) { + if (data.size() < sizeof(S_Reconnect_19)) { throw std::runtime_error("reconnect command is too small"); } - auto* args = reinterpret_cast(data.data()); + auto* args = reinterpret_cast(data.data()); memset(&this->next_destination, 0, sizeof(this->next_destination)); struct sockaddr_in* sin = reinterpret_cast( &this->next_destination); @@ -802,19 +790,12 @@ void ProxyServer::LinkedSession::on_server_input() { bool is_download_quest = (command == 0xA6); - struct OpenFileCommand { - char name[0x20]; - uint16_t unused; - uint16_t flags; - char filename[0x10]; - uint32_t file_size; - }; - if (data.size() < sizeof(OpenFileCommand)) { + if (data.size() < sizeof(S_OpenFile_PC_GC_44_A6)) { log(WARNING, "[ProxyServer/%08" PRIX32 "] Open file command is too small; skipping file", this->license->serial_number); break; } - const auto* cmd = reinterpret_cast(data.data()); + const auto* cmd = reinterpret_cast(data.data()); string output_filename = string_printf("%s.%s.%" PRIu64, cmd->filename, is_download_quest ? "download" : "online", now()); @@ -840,17 +821,12 @@ void ProxyServer::LinkedSession::on_server_input() { break; } - struct WriteFileCommand { - char filename[0x10]; - uint8_t data[0x400]; - uint32_t data_size; - }; - if (data.size() < sizeof(WriteFileCommand)) { + if (data.size() < sizeof(S_WriteFile_13_A7)) { log(WARNING, "[ProxyServer/%08" PRIX32 "] Write file command is too small", this->license->serial_number); break; } - const auto* cmd = reinterpret_cast(data.data()); + const auto* cmd = reinterpret_cast(data.data()); SavingFile* sf = nullptr; try { @@ -933,27 +909,12 @@ void ProxyServer::LinkedSession::on_server_input() { case 0x65: // other player joined game case 0x68: { // other player joined lobby - struct Command { - 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; - struct Entry { - PlayerLobbyDataGC lobby_data; - PlayerLobbyJoinDataPCGC data; - } __attribute__((packed)); - Entry entries[0]; - } __attribute__((packed)); - - size_t expected_size = sizeof(Command) + sizeof(Command::Entry) * flag; + size_t expected_size = offsetof(S_JoinLobby_GC_65_67_68, entries) + sizeof(S_JoinLobby_GC_65_67_68::Entry) * flag; if (data.size() < expected_size) { log(WARNING, "[ProxyServer/%08" PRIX32 "] Lobby join command is incorrect size (expected 0x%zX bytes, received 0x%zX bytes)", this->license->serial_number, expected_size, data.size()); } else { - const auto* cmd = reinterpret_cast(data.data()); + const auto* cmd = reinterpret_cast(data.data()); this->lobby_client_id = cmd->client_id; @@ -964,7 +925,7 @@ void ProxyServer::LinkedSession::on_server_input() { this->license->serial_number, index, x); } else { this->lobby_players[index].guild_card_number = cmd->entries[x].lobby_data.guild_card; - this->lobby_players[index].name = cmd->entries[x].data.disp.name; + this->lobby_players[index].name = cmd->entries[x].disp.name; log(INFO, "[ProxyServer/%08" PRIX32 "] Added lobby player: (%zu) %" PRIu32 " %s", this->license->serial_number, index, this->lobby_players[index].guild_card_number, @@ -982,20 +943,20 @@ void ProxyServer::LinkedSession::on_server_input() { log(WARNING, "[ProxyServer/%08" PRIX32 "] Cleared lobby players", this->license->serial_number); - const size_t expected_size = offsetof(JoinGameCommand_GC_64, player); - const size_t ep3_expected_size = sizeof(JoinGameCommand_GC_64); + const size_t expected_size = offsetof(S_JoinGame_GC_64, players_ep3); + const size_t ep3_expected_size = sizeof(S_JoinGame_GC_64); if (data.size() < expected_size) { log(WARNING, "[ProxyServer/%08" PRIX32 "] Game join command is incorrect size (expected 0x%zX bytes, received 0x%zX bytes)", this->license->serial_number, expected_size, data.size()); } else { - const auto* cmd = reinterpret_cast(data.data()); + const auto* cmd = reinterpret_cast(data.data()); this->lobby_client_id = cmd->client_id; for (size_t x = 0; x < flag; x++) { this->lobby_players[x].guild_card_number = cmd->lobby_data[x].guild_card; if (data.size() >= ep3_expected_size) { - this->lobby_players[x].name = cmd->player[x].disp.name; + this->lobby_players[x].name = cmd->players_ep3[x].disp.name; } else { this->lobby_players[x].name.clear(); } @@ -1010,16 +971,11 @@ void ProxyServer::LinkedSession::on_server_input() { case 0x66: case 0x69: { - struct Command { - uint8_t client_id; - uint8_t leader_id; - uint16_t unused; - } __attribute__((packed)); - if (data.size() < sizeof(Command)) { + if (data.size() < sizeof(S_LeaveLobby_66_69)) { log(WARNING, "[ProxyServer/%08" PRIX32 "] Lobby leave command is incorrect size", this->license->serial_number); } else { - const auto* cmd = reinterpret_cast(data.data()); + const auto* cmd = reinterpret_cast(data.data()); size_t index = cmd->client_id; if (index >= this->lobby_players.size()) { log(WARNING, "[ProxyServer/%08" PRIX32 "] Lobby leave command references missing position", diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ba743f30..ba00d4fd 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -24,6 +24,26 @@ using namespace std; +template +const T& check_size_t( + const string& data, + size_t min_size = sizeof(T), + size_t max_size = sizeof(T)) { + if (data.size() < min_size) { + throw runtime_error(string_printf( + "command too small (expected at least 0x%zX bytes, received 0x%zX bytes)", + min_size, data.size())); + } + if (data.size() > max_size) { + throw runtime_error(string_printf( + "command too large (expected at most 0x%zX bytes, received 0x%zX bytes)", + max_size, data.size())); + } + return *reinterpret_cast(data.data()); +} + + + enum ClientStateBB { // initial connection. server will redirect client to another port. INITIAL_LOGIN = 0x00, @@ -92,9 +112,6 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) } case ServerBehavior::LOGIN_SERVER: - if (!s->welcome_message.empty()) { - c->flags |= ClientFlag::AT_WELCOME_MESSAGE; - } send_server_init(s, c, true); if (s->pre_lobby_event) { send_change_event(c, s->pre_lobby_event); @@ -122,7 +139,9 @@ void process_login_complete(shared_ptr s, shared_ptr c) { send_ep3_rank_update(c); } - if (s->welcome_message.empty() || (c->flags & ClientFlag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION)) { + if (s->welcome_message.empty() || + (c->flags & ClientFlag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION) || + !(c->flags & ClientFlag::AT_WELCOME_MESSAGE)) { c->flags &= ~ClientFlag::AT_WELCOME_MESSAGE; send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); } else { @@ -196,14 +215,13 @@ void process_disconnect(shared_ptr s, shared_ptr c) { //////////////////////////////////////////////////////////////////////////////// void process_verify_license_gc(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // DB - check_size(size, sizeof(VerifyLicenseCommand_GC_DB)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // DB + const auto& cmd = check_size_t(data); - uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); + uint32_t serial_number = strtoul(cmd.serial_number, nullptr, 16); try { - c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, - cmd->password); + c->license = s->license_manager->verify_gc(serial_number, cmd.access_key, + cmd.password); } catch (const exception& e) { if (!s->allow_unregistered_users) { u16string message = u"Login failed: " + decode_sjis(e.what()); @@ -212,33 +230,27 @@ void process_verify_license_gc(shared_ptr s, shared_ptr c, return; } else { auto l = LicenseManager::create_license_gc(serial_number, - cmd->access_key, cmd->password, true); + cmd.access_key, cmd.password, true); s->license_manager->add(l); c->license = l; } } - c->flags |= flags_for_version(c->version, cmd->sub_version); + c->flags |= flags_for_version(c->version, cmd.sub_version); send_command(c, 0x9A, 0x02); } void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 9A - struct Cmd { - char unused[0x20]; - char serial_number[0x10]; - char access_key[0x10]; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // 9A + const auto& cmd = check_size_t(data); - uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); + uint32_t serial_number = strtoul(cmd.serial_number, nullptr, 16); try { if (c->version == GameVersion::GC) { - c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, + c->license = s->license_manager->verify_gc(serial_number, cmd.access_key, nullptr); } else { - c->license = s->license_manager->verify_pc(serial_number, cmd->access_key, + c->license = s->license_manager->verify_pc(serial_number, cmd.access_key, nullptr); } } catch (const exception& e) { @@ -256,28 +268,19 @@ void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, } void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, 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]; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // 9C + const auto& cmd = check_size_t(data); - c->flags |= flags_for_version(c->version, cmd->sub_version); + c->flags |= flags_for_version(c->version, cmd.sub_version); - uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); + uint32_t serial_number = strtoul(cmd.serial_number, nullptr, 16); try { if (c->version == GameVersion::GC) { - c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, - cmd->password); + 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); + c->license = s->license_manager->verify_pc(serial_number, cmd.access_key, + cmd.password); } } catch (const exception& e) { if (!s->allow_unregistered_users) { @@ -289,10 +292,10 @@ void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, shared_ptr l; if (c->version == GameVersion::GC) { l = LicenseManager::create_license_gc(serial_number, - cmd->access_key, cmd->password, true); + cmd.access_key, cmd.password, true); } else { l = LicenseManager::create_license_pc(serial_number, - cmd->access_key, cmd->password, true); + cmd.access_key, cmd.password, true); } s->license_manager->add(l); c->license = l; @@ -303,20 +306,20 @@ void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, } void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 9D 9E - // sometimes the unused bytes aren't sent? - check_size(size, sizeof(LoginCommand_GC_9E) - 0x64, sizeof(LoginCommand_GC_9E)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // 9D 9E + // Sometimes the unused bytes aren't sent + const auto& cmd = check_size_t(data, + sizeof(C_Login_PC_GC_9D_9E) - 0x64, sizeof(C_Login_PC_GC_9D_9E)); - c->flags |= flags_for_version(c->version, cmd->sub_version); + c->flags |= flags_for_version(c->version, cmd.sub_version); - uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); + uint32_t serial_number = strtoul(cmd.serial_number, nullptr, 16); try { if (c->version == GameVersion::GC) { - c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, + c->license = s->license_manager->verify_gc(serial_number, cmd.access_key, nullptr); } else { - c->license = s->license_manager->verify_pc(serial_number, cmd->access_key, + c->license = s->license_manager->verify_pc(serial_number, cmd.access_key, nullptr); } } catch (const exception& e) { @@ -329,8 +332,11 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, } try { - c->import_config(cmd->cfg); + c->import_config(cmd.cfg); } catch (const invalid_argument&) { + // If we can't import the config, assume that the client was not connected + // to newserv before, so we should show the welcome message. + c->flags |= ClientFlag::AT_WELCOME_MESSAGE; c->bb_game_state = 0; c->bb_player_index = 0; } @@ -345,22 +351,13 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, } void process_login_bb(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 93 - struct Cmd { - char unused[0x14]; - char username[0x10]; - char unused2[0x20]; - char password[0x10]; - char unused3[0x30]; - ClientConfig cfg; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // 93 + const auto& cmd = check_size_t(data); c->flags |= flags_for_version(c->version, 0); try { - c->license = s->license_manager->verify_bb(cmd->username, cmd->password); + 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()); @@ -369,7 +366,7 @@ void process_login_bb(shared_ptr s, shared_ptr c, } try { - c->import_config(cmd->cfg); + c->import_config(cmd.cfg); c->bb_game_state++; } catch (const invalid_argument&) { c->bb_game_state = 0; @@ -413,13 +410,14 @@ void process_login_bb(shared_ptr s, shared_ptr c, } void process_client_checksum(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t, const void*) { // 96 + uint16_t, uint32_t, const string& data) { // 96 + check_size_t(data); send_command(c, 0x97, 0x01); } void process_server_time_request(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // B1 - check_size(size, 0); + uint16_t, uint32_t, const string& data) { // B1 + check_size(data.size(), 0); send_server_time(c); } @@ -430,21 +428,10 @@ void process_server_time_request(shared_ptr, shared_ptr c, // 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, uint16_t size, const void* data) { - struct InputCmd { - uint32_t transaction_num; - uint32_t value; - uint32_t unknown_token; - } __attribute__((packed)); - struct OutputCmd { - uint32_t remaining_meseta; - uint32_t unknown; - uint32_t unknown_token; - } __attribute__((packed)); - check_size(size, sizeof(InputCmd)); - const auto* in_cmd = reinterpret_cast(data); + uint16_t command, uint32_t, const string& data) { + const auto& in_cmd = check_size_t(data); - OutputCmd out_cmd = {1000000, 0x80E8, in_cmd->unknown_token}; + S_Meseta_GC_Ep3_BA out_cmd = {1000000, 0x80E8, in_cmd.unknown_token}; auto l = s->find_lobby(c->lobby_id); if (!l || !(l->flags & LobbyFlag::EPISODE_3)) { @@ -455,15 +442,17 @@ void process_ep3_jukebox(shared_ptr s, shared_ptr c, } void process_ep3_menu_challenge(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // DC - check_size(size, 0); - send_command(c, 0xDC); + uint16_t, uint32_t flag, const string& data) { // DC + check_size(data.size(), 0); + if (flag != 0) { + send_command(c, 0xDC); + } } void process_ep3_server_data_request(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // CA - check_size(size, 8, 0xFFFF); - const PSOSubcommand* cmds = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // CA + check_size(data.size(), 8, 0xFFFF); + const PSOSubcommand* cmds = reinterpret_cast(data.data()); auto l = s->find_lobby(c->lobby_id); if (!l || !(l->flags & LobbyFlag::EPISODE_3) || !l->is_game()) { @@ -548,7 +537,7 @@ void process_ep3_server_data_request(shared_ptr s, shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t, const void*) { // E2 + uint16_t, uint32_t, const string&) { // E2 // The client will get stuck here unless we send something. An 01 (lobby // message box) seems to get them unstuck. send_lobby_message_box(c, u"$C6Tournaments are\nnot supported."); @@ -567,27 +556,24 @@ void process_ep3_tournament_control(shared_ptr, shared_ptr // menu commands void process_message_box_closed(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t, const void*) { // D6 + uint16_t, uint32_t, const string& data) { // D6 + check_size(data.size(), 0); if (c->flags & ClientFlag::IN_INFORMATION_MENU) { send_menu(c, u"Information", INFORMATION_MENU_ID, *s->information_menu, false); } else if (c->flags & ClientFlag::AT_WELCOME_MESSAGE) { send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); c->flags &= ~ClientFlag::AT_WELCOME_MESSAGE; + send_update_client_config(c); } } void process_menu_item_info_request(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 09 - struct Cmd { - uint32_t menu_id; - uint32_t item_id; - } __attribute__((packed)); - check_size(size, sizeof(Cmd), sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // 09 + const auto& cmd = check_size_t(data); - switch (cmd->menu_id) { + switch (cmd.menu_id) { case MAIN_MENU_ID: - switch (cmd->item_id) { + switch (cmd.item_id) { case MAIN_MENU_GO_TO_LOBBY: send_ship_info(c, u"Go to the lobby."); break; @@ -610,12 +596,12 @@ void process_menu_item_info_request(shared_ptr s, shared_ptritem_id == INFORMATION_MENU_GO_BACK) { + if (cmd.item_id == INFORMATION_MENU_GO_BACK) { send_ship_info(c, u"Return to the\nmain menu."); } else { try { // we use item_id + 1 here because "go back" is the first item - send_ship_info(c, s->information_menu->at(cmd->item_id + 1).description.c_str()); + send_ship_info(c, s->information_menu->at(cmd.item_id + 1).description.c_str()); } catch (const out_of_range&) { send_ship_info(c, u"$C6No such information exists."); } @@ -623,12 +609,12 @@ void process_menu_item_info_request(shared_ptr s, shared_ptritem_id == PROXY_DESTINATIONS_MENU_GO_BACK) { + if (cmd.item_id == PROXY_DESTINATIONS_MENU_GO_BACK) { send_ship_info(c, u"Return to the\nmain menu."); } else { try { // we use item_id + 1 here because "go back" is the first item - send_ship_info(c, s->proxy_destinations_menu.at(cmd->item_id + 1).description.c_str()); + send_ship_info(c, s->proxy_destinations_menu.at(cmd.item_id + 1).description.c_str()); } catch (const out_of_range&) { send_ship_info(c, u"$C6No such information exists."); } @@ -640,7 +626,7 @@ void process_menu_item_info_request(shared_ptr s, shared_ptrquest_index->get(c->version, cmd->item_id); + auto q = s->quest_index->get(c->version, cmd.item_id); if (!q) { send_quest_info(c, u"$C6Quest does not exist."); break; @@ -656,23 +642,15 @@ void process_menu_item_info_request(shared_ptr s, shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 10 + uint16_t, uint32_t, const string& data) { // 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 pc_bb[0]; - char dc_gc[0]; - } __attribute__((packed)) password; - } __attribute__((packed)); - check_size(size, sizeof(Cmd), sizeof(Cmd) + 0x10 * (1 + uses_unicode)); - const auto* cmd = reinterpret_cast(data); + const auto& cmd = check_size_t(data, + sizeof(C_MenuSelection), sizeof(C_MenuSelection) + 0x10 * (1 + uses_unicode)); - switch (cmd->menu_id) { + switch (cmd.menu_id) { case MAIN_MENU_ID: { - switch (cmd->item_id) { + switch (cmd.item_id) { case MAIN_MENU_GO_TO_LOBBY: { static const vector version_to_port_name({ "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "bb-lobby"}); @@ -710,13 +688,13 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } case INFORMATION_MENU_ID: { - if (cmd->item_id == INFORMATION_MENU_GO_BACK) { + if (cmd.item_id == INFORMATION_MENU_GO_BACK) { c->flags &= ~ClientFlag::IN_INFORMATION_MENU; send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); } else { try { - send_message_box(c, s->information_contents->at(cmd->item_id).c_str()); + send_message_box(c, s->information_contents->at(cmd.item_id).c_str()); } catch (const out_of_range&) { send_message_box(c, u"$C6No such information exists."); } @@ -725,13 +703,13 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } case PROXY_DESTINATIONS_MENU_ID: { - if (cmd->item_id == PROXY_DESTINATIONS_MENU_GO_BACK) { + if (cmd.item_id == PROXY_DESTINATIONS_MENU_GO_BACK) { send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); } else { pair* dest = nullptr; try { - dest = &s->proxy_destinations.at(cmd->item_id); + dest = &s->proxy_destinations.at(cmd.item_id); } catch (const out_of_range&) { } if (!dest) { @@ -760,7 +738,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } case GAME_MENU_ID: { - auto game = s->find_lobby(cmd->item_id); + auto game = s->find_lobby(cmd.item_id); if (!game) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because it no\nlonger exists."); break; @@ -793,15 +771,20 @@ void process_menu_selection(shared_ptr s, shared_ptr c, if (!(c->license->privileges & Privilege::FREE_JOIN_GAMES)) { char16_t password[0x10]; - if (size > sizeof(Cmd)) { + memset(password, 0, sizeof(password)); + if (data.size() > sizeof(C_MenuSelection)) { if (uses_unicode) { - char16cpy(password, cmd->password.pc_bb, 0x10); + size_t max_chars = (data.size() - sizeof(C_MenuSelection)) / sizeof(char16_t); + char16ncpy(password, cmd.password.pcbb, + min(max_chars, countof(password))); } else { - decode_sjis(password, cmd->password.dc_gc, 0x10); + size_t max_chars = (data.size() - sizeof(C_MenuSelection)) / sizeof(char); + decode_sjis(password, cmd.password.dcgc, + min(max_chars, countof(password))); } } - if (game->password[0] && char16cmp(game->password, password, 0x10)) { + if (game->password[0] && char16ncmp(game->password, password, 0x10)) { send_message_box(c, u"$C6Incorrect password."); break; } @@ -831,7 +814,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, shared_ptr l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr; auto quests = s->quest_index->filter(c->version, c->flags & ClientFlag::IS_DCV1, - static_cast(cmd->item_id & 0xFF), + static_cast(cmd.item_id & 0xFF), l.get() ? (l->episode - 1) : -1); if (quests.empty()) { send_lobby_message_box(c, u"$C6There are no quests\navailable in that\ncategory."); @@ -849,7 +832,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, send_lobby_message_box(c, u"$C6Quests are not available."); break; } - auto q = s->quest_index->get(c->version, cmd->item_id); + auto q = s->quest_index->get(c->version, cmd.item_id); if (!q) { send_lobby_message_box(c, u"$C6Quest does not exist."); break; @@ -913,17 +896,12 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } void process_change_lobby(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 84 - struct Cmd { - uint32_t menu_id; - uint32_t item_id; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // 84 + const auto& cmd = check_size_t(data); shared_ptr new_lobby; try { - new_lobby = s->find_lobby(cmd->item_id); + 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\nexist."); return; @@ -938,13 +916,18 @@ void process_change_lobby(shared_ptr s, shared_ptr c, } void process_game_list_request(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // 08 - check_size(size, 0); + uint16_t, uint32_t, const string& data) { // 08 + check_size(data.size(), 0); send_game_menu(c, s); } void process_change_ship(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t, const void*) { // A0 + uint16_t, uint32_t, const string&) { // A0 + // The client actually sends data in this command... looks like nothing + // important (player_tag and guild_card_number are the only discernable + // things, which we already know). We intentionally don't call check_size + // here, but instead just ignore the data. + send_message_box(c, u""); // we do this to avoid the "log window in message box" bug static const vector version_to_port_name({ @@ -956,17 +939,17 @@ void process_change_ship(shared_ptr s, shared_ptr c, } 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); + uint16_t command, uint32_t flag, const string& data) { // A1 + // newserv doesn't have blocks; treat block change as ship change + process_change_ship(s, c, command, flag, data); } //////////////////////////////////////////////////////////////////////////////// // Quest commands void process_quest_list_request(shared_ptr s, shared_ptr c, - uint16_t, uint32_t flag, uint16_t size, const void*) { // A2 - check_size(size, 0); + uint16_t, uint32_t flag, const string& data) { // A2 + check_size(data.size(), 0); if (!s->quest_index) { send_lobby_message_box(c, u"$C6Quests are not available."); @@ -1000,8 +983,8 @@ void process_quest_list_request(shared_ptr s, shared_ptr c, } void process_quest_ready(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // AC - check_size(size, 0); + uint16_t, uint32_t, const string& data) { // AC + check_size(data.size(), 0); auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { @@ -1030,11 +1013,11 @@ void process_quest_ready(shared_ptr s, shared_ptr c, } void process_gba_file_request(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // D7 + uint16_t, uint32_t, const string& data) { // D7 static FileContentsCache file_cache; - string filename(reinterpret_cast(data), size); - filename.resize(strlen(filename.data())); + string filename(data); + strip_trailing_zeroes(filename); auto contents = file_cache.get(filename); send_quest_file(c, filename, *contents, false, false); @@ -1046,27 +1029,30 @@ void process_gba_file_request(shared_ptr, shared_ptr c, // 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 + uint16_t command, uint32_t flag, const string& data) { // 61 98 // Note: we add extra buffer on the end when checking sizes because the // autoreply text is a variable length switch (c->version) { case GameVersion::PC: - check_size(size, sizeof(PSOPlayerDataPC), sizeof(PSOPlayerDataPC) + 2 * 0xAC); - c->player.import(*reinterpret_cast(data)); + check_size(data.size(), sizeof(PSOPlayerDataPC), + sizeof(PSOPlayerDataPC) + sizeof(char16_t) * countof(c->player.auto_reply)); + c->player.import(*reinterpret_cast(data.data())); break; case GameVersion::GC: if (flag == 4) { // Episode 3 - check_size(size, sizeof(PSOPlayerDataGC) + 0x23FC); + check_size(data.size(), sizeof(PSOPlayerDataGC) + 0x23FC); // TODO: import Episode 3 data somewhere } else { - check_size(size, sizeof(PSOPlayerDataGC), sizeof(PSOPlayerDataGC) + 0xAC); + check_size(data.size(), sizeof(PSOPlayerDataGC), + sizeof(PSOPlayerDataGC) + sizeof(char) * countof(c->player.auto_reply)); } - c->player.import(*reinterpret_cast(data)); + c->player.import(*reinterpret_cast(data.data())); break; case GameVersion::BB: - check_size(size, sizeof(PSOPlayerDataBB), sizeof(PSOPlayerDataBB) + 2 * 0xAC); - c->player.import(*reinterpret_cast(data)); + check_size(data.size(), sizeof(PSOPlayerDataBB), + sizeof(PSOPlayerDataBB) + sizeof(char16_t) * countof(c->player.auto_reply)); + c->player.import(*reinterpret_cast(data.data())); break; default: throw logic_error("player data command not implemented for version"); @@ -1114,17 +1100,17 @@ void process_player_data(shared_ptr s, shared_ptr c, // 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); + uint16_t command, uint32_t flag, const string& data) { // 60 62 6C 6D C9 CB (C9 CB are ep3 only) + check_size(data.size(), 4, 0xFFFF); + const PSOSubcommand* sub = reinterpret_cast(data.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); + process_subcommand(s, l, c, command, flag, sub, + data.size() / sizeof(PSOSubcommand)); } //////////////////////////////////////////////////////////////////////////////// @@ -1168,27 +1154,17 @@ void process_chat_generic(shared_ptr s, shared_ptr c, } void process_chat_pc_bb(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 06 - struct Cmd { - uint32_t unused[2]; - char16_t text[0]; - } __attribute__((packed)); - check_size(size, sizeof(Cmd), 0xFFFF); - const auto* cmd = reinterpret_cast(data); - - process_chat_generic(s, c, cmd->text); + uint16_t, uint32_t, const string& data) { // 06 + const auto& cmd = check_size_t(data, sizeof(C_Chat_06), 0xFFFF); + u16string text(cmd.text.pcbb, (data.size() - sizeof(C_Chat_06)) / sizeof(char16_t)); + text.resize(char16len(text.c_str())); + process_chat_generic(s, c, text); } void process_chat_dc_gc(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { - struct Cmd { - uint32_t unused[2]; - char text[0]; - } __attribute__((packed)); - check_size(size, sizeof(Cmd), 0xFFFF); - const auto* cmd = reinterpret_cast(data); - - u16string decoded_s = decode_sjis(cmd->text); + uint16_t, uint32_t, const string& data) { + const auto& cmd = check_size_t(data, sizeof(C_Chat_06), 0xFFFF); + u16string decoded_s = decode_sjis(cmd.text.dcgc, data.size() - sizeof(C_Chat_06)); process_chat_generic(s, c, decoded_s); } @@ -1196,22 +1172,17 @@ void process_chat_dc_gc(shared_ptr s, shared_ptr c, // BB commands void process_key_config_request_bb(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { - check_size(size, 0); + uint16_t, uint32_t, const string& data) { + check_size(data.size(), 0); send_team_and_key_config_bb(c); } void process_player_preview_request_bb(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { - struct Cmd { - uint32_t player_index; - uint32_t unused; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { + const auto& cmd = check_size_t(data); if (c->bb_game_state == ClientStateBB::CHOOSE_PLAYER) { - c->bb_player_index = cmd->player_index; + c->bb_player_index = cmd.player_index; c->bb_game_state++; send_client_init_bb(c, 0); send_approve_player_choice_bb(c); @@ -1228,18 +1199,18 @@ void process_player_preview_request_bb(shared_ptr, shared_ptrplayer_index, &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, nullptr); + send_player_preview_bb(c, cmd.player_index, nullptr); } } } void process_client_checksum_bb(shared_ptr, shared_ptr c, - uint16_t command, uint32_t, uint16_t size, const void*) { - check_size(size, 0); + uint16_t command, uint32_t, const string& data) { + check_size(data.size(), 0); if (command == 0x01E8) { send_accept_client_checksum_bb(c); @@ -1251,23 +1222,16 @@ void process_client_checksum_bb(shared_ptr, shared_ptr c, } void process_guild_card_data_request_bb(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { - struct Cmd { - uint32_t unknown; - uint32_t chunk_index; - uint32_t cont; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); - - if (cmd->cont) { - send_guild_card_chunk_bb(c, cmd->chunk_index); + uint16_t, uint32_t, const string& data) { + const auto& cmd = check_size_t(data); + if (cmd.cont) { + send_guild_card_chunk_bb(c, cmd.chunk_index); } } void process_stream_file_request_bb(shared_ptr, shared_ptr c, - uint16_t command, uint32_t, uint16_t size, const void*) { - check_size(size, 0); + uint16_t command, uint32_t, const string& data) { + check_size(data.size(), 0); if (command == 0x04EB) { send_stream_file_bb(c); @@ -1277,13 +1241,8 @@ void process_stream_file_request_bb(shared_ptr, shared_ptr } void process_create_character_bb(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { - struct Cmd { - uint32_t player_index; - PlayerDispDataBBPreview preview; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { + const auto& cmd = check_size_t(data); if (!c->license) { send_message_box(c, u"$C6You are not logged in."); @@ -1294,11 +1253,11 @@ void process_create_character_bb(shared_ptr s, shared_ptr c return; } - c->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); + c->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); + string template_filename = filename_for_class_template_bb(cmd.preview.char_class); Player p; try { @@ -1309,7 +1268,7 @@ void process_create_character_bb(shared_ptr s, shared_ptr c } try { - p.disp.apply_preview(cmd->preview); + 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."); @@ -1336,45 +1295,36 @@ void process_create_character_bb(shared_ptr s, shared_ptr c } void process_change_account_data_bb(shared_ptr, shared_ptr c, - uint16_t command, uint32_t, 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 - } __attribute__((packed)); - const auto* cmd = reinterpret_cast(data); + uint16_t command, uint32_t, const string& data) { + const auto* cmd = reinterpret_cast(data.data()); switch (command) { case 0x01ED: - check_size(size, sizeof(cmd->option)); + check_size(data.size(), sizeof(cmd->option)); c->player.option_flags = cmd->option; break; case 0x02ED: - check_size(size, sizeof(cmd->symbol_chats)); + check_size(data.size(), sizeof(cmd->symbol_chats)); memcpy(c->player.symbol_chats, cmd->symbol_chats, 0x04E0); break; case 0x03ED: - check_size(size, sizeof(cmd->chat_shortcuts)); + check_size(data.size(), sizeof(cmd->chat_shortcuts)); memcpy(c->player.shortcuts, cmd->chat_shortcuts, 0x0A40); break; case 0x04ED: - check_size(size, sizeof(cmd->key_config)); + check_size(data.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)); + check_size(data.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)); + check_size(data.size(), sizeof(cmd->tech_menu)); memcpy(&c->player.tech_menu_config, cmd->tech_menu, 0x0028); break; case 0x07ED: - check_size(size, sizeof(cmd->customize)); + check_size(data.size(), sizeof(cmd->customize)); memcpy(c->player.disp.config, cmd->customize, 0xE8); break; default: @@ -1383,22 +1333,21 @@ void process_change_account_data_bb(shared_ptr, shared_ptr } void process_return_player_data_bb(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { - check_size(size, sizeof(PlayerBB)); - const PlayerBB* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { + const auto& cmd = check_size_t(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)); + // 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, uint32_t flag, uint16_t size, const void*) { // 89 - check_size(size, 0); + uint16_t, uint32_t flag, const string& data) { // 89 + check_size(data.size(), 0); c->lobby_arrow_color = flag; auto l = s->find_lobby(c->lobby_id); @@ -1408,113 +1357,48 @@ void process_change_arrow_color(shared_ptr s, shared_ptr c, } void process_card_search(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 40 - struct Cmd { - uint32_t player_tag; - uint32_t searcher_serial_number; - uint32_t target_serial_number; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); - + uint16_t, uint32_t, const string& data) { // 40 + const auto& cmd = check_size_t(data); try { - auto result = s->find_client(nullptr, cmd->target_serial_number); + auto result = s->find_client(nullptr, 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&) { } } void process_choice_search(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t, const void*) { // C0 + uint16_t, uint32_t, const string&) { // C0 // TODO: Implement choice search. - // Choice search works like this: - // Client: C0 00 04 00 - // Server: C0 ## SS SS [entries] (# = overall entry count, including top-level and non-top-level) - // struct Entry { - // // this first two are 0 for top-level category, nonzero for choice within category - // uint8_t parent_category; - // uint8_t parent_category_user_settable; - // uint8_t category; - // uint8_t user_settable; // 0 or 1 - // char text[0x1C]; - // } - // Top-level categories are things like "Level", "Class", etc. - // Choices for each top-level category immediately follow the category, so - // a reasonable order of items is (for example): - // 00 00 11 01 "Preferred difficulty" - // 11 01 01 01 "Normal" - // 11 01 02 01 "Hard" - // 11 01 03 01 "Very Hard" - // 11 01 04 01 "Ultimate" - // 00 00 22 00 "Character class" - // 22 00 01 00 "HUmar" - // 22 00 02 00 "HUnewearl" - // etc. - // To set your own params: - // Client: C2 00 SS SS ZZ ZZ 00 00 [entries] - // Entries are the same as struct Entry from above except without the name - // field. The client even sends them for non-settable parameters (the - // server should just ignore those presumably) - // Z = disabled (00 00 = choice search ON, 00 01 = OFF) - // To execute a choice search: - // Client: C3 00 SS SS ?? ?? ?? ?? [entries] - // Entries are the same as struct Entry from above - // Server: C4 ## SS SS [results] - // struct Result { - // uint32_t guild_card_number; - // char name[0x10]; // No language marker, as usual on GC - // char info_string[0x20]; // Usually something like " Lvl " - // // Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME" - // // If target is in game, for example, "Game Name,BLOCK01,Alexandria" - // // If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria" - // char locator_string[0x34]; - // // Server IP and port for "meet user" option - // uint32_t server_ip; - // uint16_t server_port; - // uint16_t unused; - // uint32_t menu_id; - // uint32_t lobby_id; // These two are guesses - // uint32_t game_id; // Zero if target is in a lobby rather than a game - // uint8_t unused[0x58]; - // }; send_text_message(c, u"$C6Choice Search is\nnot supported"); } void process_simple_mail(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // 81 + uint16_t, uint32_t, const string& data) { // 81 if (c->version != GameVersion::GC) { // TODO: implement this for DC, PC, BB send_text_message(c, u"$C6Simple Mail is not\nsupported yet on\nthis platform."); return; } - struct Cmd { - uint32_t player_tag; - uint32_t source_serial_number; - char from_name[16]; - uint32_t target_serial_number; - char data[0x200]; // on GC this appears to contain uninitialized memory! - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + const auto& cmd = check_size_t(data); - auto target = s->find_client(nullptr, cmd->target_serial_number); + auto target = s->find_client(nullptr, cmd.to_serial_number); - // if the sender is blocked, don't forward the mail + // If the sender is blocked, don't forward the mail for (size_t y = 0; y < 30; y++) { if (target->player.blocked[y] == c->license->serial_number) { return; } } - // if the target has auto-reply enabled, send the autoreply + // If the target has auto-reply enabled, send the autoreply if (target->player.auto_reply[0]) { send_simple_mail(c, target->license->serial_number, target->player.disp.name, target->player.auto_reply); } - // forward the message - string message(cmd->data, strnlen(cmd->data, sizeof(cmd->data) / sizeof(cmd->data[0]))); + // Forward the message + string message(cmd.text, strnlen(cmd.text, sizeof(cmd.text) / sizeof(cmd.text[0]))); u16string u16message = decode_sjis(message); send_simple_mail(target, c->license->serial_number, c->player.disp.name, u16message.data()); @@ -1526,54 +1410,41 @@ void process_simple_mail(shared_ptr s, shared_ptr c, // Info board commands void process_info_board_request(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // D8 - check_size(size, 0); - auto l = s->find_lobby(c->lobby_id); - send_info_board(c, l); + uint16_t, uint32_t, const string& data) { // D8 + check_size(data.size(), 0); + send_info_board(c, s->find_lobby(c->lobby_id)); } -void process_write_info_board_pc_bb(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // D9 - check_size(size, 0, 2 * 0xAC); - char16cpy(c->player.info_board, reinterpret_cast(data), 0xAC); +template +void process_write_info_board_t(shared_ptr, shared_ptr c, + uint16_t, uint32_t, const string& data) { // D9 + check_size(data.size(), 0, countof(c->player.info_board) * sizeof(CharT)); + memset(c->player.info_board, 0, sizeof(c->player.info_board)); + strncpy_t(c->player.info_board, reinterpret_cast(data.data()), + min(countof(c->player.info_board), data.size() / sizeof(CharT))); } -void process_write_info_board_dc_gc(shared_ptr, shared_ptr c, - uint16_t, uint32_t, 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, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // C7 - 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, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // C7 - 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); - } +template +void process_set_auto_reply_t(shared_ptr, shared_ptr c, + uint16_t, uint32_t, const string& data) { // C7 + check_size(data.size(), 0, countof(c->player.auto_reply) * sizeof(CharT)); + memset(c->player.auto_reply, 0, sizeof(c->player.auto_reply)); + strncpy_t(c->player.auto_reply, reinterpret_cast(data.data()), + min(countof(c->player.auto_reply), data.size() / sizeof(CharT))); } void process_disable_auto_reply(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // C8 - check_size(size, 0); - c->player.auto_reply[0] = 0; + uint16_t, uint32_t, const string& data) { // C8 + check_size(data.size(), 0); + memset(c->player.auto_reply, 0, sizeof(c->player.auto_reply)); } void process_set_blocked_list(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void* data) { // C6 - check_size(size, 0x78); - memcpy(c->player.blocked, data, 0x78); + uint16_t, uint32_t, const string& data) { // C6 + const auto& cmd = check_size_t(data); + for (size_t x = 0; x < countof(cmd.blocked_senders); x++) { + c->player.blocked[x] = cmd.blocked_senders[x]; + } } @@ -1630,10 +1501,11 @@ shared_ptr create_game_generic(shared_ptr s, } shared_ptr game(new Lobby()); - char16cpy(game->name, name, 0x10); - char16cpy(game->password, password, 0x10); + char16ncpy(game->name, name, countof(game->name)); + char16ncpy(game->password, password, countof(game->name)); game->version = c->version; - game->section_id = c->player.disp.section_id; + game->section_id = c->override_section_id >= 0 + ? c->override_section_id : c->player.disp.section_id; game->episode = episode; game->difficulty = difficulty; if (battle) { @@ -1652,6 +1524,7 @@ shared_ptr create_game_generic(shared_ptr s, game->min_level = min_level; game->max_level = 0xFFFFFFFF; + const uint32_t* variation_maxes = nullptr; if (game->version == GameVersion::BB) { // 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", @@ -1667,28 +1540,28 @@ shared_ptr create_game_generic(shared_ptr s, game->episode - 1, game->difficulty); if (game->mode == 3) { - for (size_t x = 0; x < 0x20; x++) { - game->variations[x] = random_int(0, variation_maxes_solo[(episode - 1)][x] - 1); + if (episode > 0 && episode < 4) { + variation_maxes = variation_maxes_solo[episode - 1]; + } + 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) { } } - 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"); - } + 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] = random_int(0, variation_maxes_online[(episode - 1)][x] - 1); + if (episode > 0 && episode < 4) { + variation_maxes = variation_maxes_online[episode - 1]; } for (size_t x = 0; x < 0x10; x++) { auto filename = string_printf( @@ -1703,8 +1576,18 @@ shared_ptr create_game_generic(shared_ptr s, } else { // In non-BB games, just set the variations (we don't track items/enemies/ // etc.) + if (episode > 0 && episode < 4 && !is_ep3) { + variation_maxes = variation_maxes_online[episode - 1]; + } + } + + if (variation_maxes) { for (size_t x = 0; x < 0x20; x++) { - game->variations[x] = random_int(0, variation_maxes_online[(episode - 1)][x] - 1); + game->variations[x] = random_int(0, variation_maxes[x] - 1); + } + } else { + for (size_t x = 0; x < 0x20; x++) { + game->variations[x] = 0; } } @@ -1712,21 +1595,11 @@ shared_ptr create_game_generic(shared_ptr s, } void process_create_game_pc(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, 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; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // C1 + const auto& cmd = check_size_t(data); - auto game = create_game_generic(s, c, cmd->name, cmd->password, 1, - cmd->difficulty, cmd->battle_mode, cmd->challenge_mode, 0); + 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); @@ -1734,18 +1607,8 @@ void process_create_game_pc(shared_ptr s, shared_ptr c, } void process_create_game_dc_gc(shared_ptr s, shared_ptr c, - uint16_t command, uint32_t, uint16_t size, const void* data) { // C1 EC (EC Ep3 only) - 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; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t command, uint32_t, const string& data) { // C1 EC (EC Ep3 only) + const auto& cmd = check_size_t(data); // only allow EC from Ep3 clients bool client_is_ep3 = c->flags & ClientFlag::EPISODE_3_GAMES; @@ -1753,7 +1616,7 @@ void process_create_game_dc_gc(shared_ptr s, shared_ptr c, return; } - uint8_t episode = cmd->episode; + uint8_t episode = cmd.episode; if (c->version == GameVersion::DC) { episode = 1; } @@ -1761,11 +1624,11 @@ void process_create_game_dc_gc(shared_ptr s, shared_ptr c, episode = 0xFF; } - u16string name = decode_sjis(cmd->name); - u16string password = decode_sjis(cmd->password); + 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); + episode, cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, 0); s->add_lobby(game); s->change_client_lobby(c, game); @@ -1773,24 +1636,12 @@ void process_create_game_dc_gc(shared_ptr s, shared_ptr c, } void process_create_game_bb(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, 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]; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { // C1 + const auto& cmd = check_size_t(data); - auto game = create_game_generic(s, c, cmd->name, cmd->password, - cmd->episode, cmd->difficulty, cmd->battle_mode, cmd->challenge_mode, - cmd->solo_mode); + auto game = create_game_generic(s, c, cmd.name, cmd.password, + cmd.episode, cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, + cmd.solo_mode); s->add_lobby(game); s->change_client_lobby(c, game); @@ -1800,8 +1651,8 @@ void process_create_game_bb(shared_ptr s, shared_ptr c, } void process_lobby_name_request(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // 8A - check_size(size, 0); + uint16_t, uint32_t, const string& data) { // 8A + check_size(data.size(), 0); auto l = s->find_lobby(c->lobby_id); if (!l) { throw invalid_argument("client not in any lobby"); @@ -1810,8 +1661,8 @@ void process_lobby_name_request(shared_ptr s, shared_ptr c, } void process_client_ready(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { // 6F - check_size(size, 0); + uint16_t, uint32_t, const string& data) { // 6F + check_size(data.size(), 0); auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { @@ -1820,11 +1671,8 @@ void process_client_ready(shared_ptr s, shared_ptr c, } c->flags &= (~ClientFlag::LOADING); - // tell the other players to stop waiting for the new player to load send_resume_game(l, c); - // tell the new player the time send_server_time(c); - // get character info send_get_player_info(c); } @@ -1832,7 +1680,7 @@ void process_client_ready(shared_ptr s, shared_ptr c, // Team commands void process_team_command_bb(shared_ptr, shared_ptr c, - uint16_t command, uint32_t, uint16_t, const void*) { // EA + uint16_t command, uint32_t, const string&) { // EA if (command == 0x01EA) { send_lobby_message_box(c, u"$C6Teams are not supported."); @@ -1845,30 +1693,26 @@ void process_team_command_bb(shared_ptr, shared_ptr c, // Patch server commands void process_encryption_ok_patch(shared_ptr, shared_ptr c, - uint16_t, uint32_t, uint16_t size, const void*) { - check_size(size, 0); + uint16_t, uint32_t, const string& data) { + check_size(data.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, uint32_t, uint16_t size, const void* data) { - struct Cmd { - uint32_t unused[3]; - char username[0x10]; - char password[0x10]; - } __attribute__((packed)); - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + uint16_t, uint32_t, const string& data) { + const auto& cmd = check_size_t(data); u16string message = u"\ -$C7NewServ Patch Server v1.0\n\n\ -Please note that this server is for private use only.\n\ +$C7newserv patch server\n\ +\n\ +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\ +and operated completely independently.\n\ +\n\ License check: "; try { - c->license = s->license_manager->verify_bb(cmd->username, cmd->password); + c->license = s->license_manager->verify_bb(cmd.username, cmd.password); message += u"OK"; } catch (const exception& e) { message += decode_sjis(e.what()); @@ -1891,19 +1735,19 @@ License check: "; // Command pointer arrays void process_ignored_command(shared_ptr, shared_ptr, - uint16_t, uint32_t, uint16_t, const void*) { } + uint16_t, uint32_t, const string&) { } void process_unimplemented_command(shared_ptr, shared_ptr, - uint16_t command, uint32_t flag, uint16_t size, const void*) { - log(WARNING, "Unknown command: size=%04X command=%04X flag=%08X\n", - size, command, flag); + uint16_t command, uint32_t flag, const string& data) { + log(WARNING, "Unknown command: size=%04zX command=%04hX flag=%08" PRIX32 "\n", + data.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); + uint16_t command, uint32_t flag, const string& 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 @@ -1973,13 +1817,13 @@ static process_command_t dc_handlers[0x100] = { // C0 nullptr, process_create_game_dc_gc, nullptr, nullptr, - nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_dc_gc, + nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - process_info_board_request, process_write_info_board_dc_gc, nullptr, nullptr, + process_info_board_request, process_write_info_board_t, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // E0 @@ -2056,13 +1900,13 @@ static process_command_t pc_handlers[0x100] = { // C0 nullptr, process_create_game_pc, nullptr, nullptr, - nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_pc_bb, + nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - process_info_board_request, process_write_info_board_pc_bb, nullptr, nullptr, + process_info_board_request, process_write_info_board_t, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // E0 @@ -2141,14 +1985,14 @@ static process_command_t gc_handlers[0x100] = { // C0 process_choice_search, process_create_game_dc_gc, nullptr, nullptr, - nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_dc_gc, + nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_t, process_disable_auto_reply, process_game_command, process_ep3_server_data_request, process_game_command, nullptr, nullptr, nullptr, nullptr, // D0 nullptr, nullptr, nullptr, nullptr, // D0 is process trade nullptr, nullptr, process_message_box_closed, process_gba_file_request, - process_info_board_request, process_write_info_board_dc_gc, nullptr, process_verify_license_gc, + process_info_board_request, process_write_info_board_t, nullptr, process_verify_license_gc, process_ep3_menu_challenge, nullptr, nullptr, nullptr, // E0 @@ -2228,14 +2072,14 @@ static process_command_t bb_handlers[0x100] = { // C0 nullptr, process_create_game_bb, nullptr, nullptr, - nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_pc_bb, + nullptr, nullptr, process_set_blocked_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - process_info_board_request, process_write_info_board_pc_bb, nullptr, nullptr, + process_info_board_request, process_write_info_board_t, nullptr, nullptr, process_guild_card_data_request_bb, nullptr, nullptr, nullptr, // E0 @@ -2306,14 +2150,15 @@ static process_command_t* handlers[6] = { 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) { + uint16_t command, uint32_t flag, const string& data) { string encoded_name = remove_language_marker(encode_sjis(c->player.disp.name)); - print_received_command(command, flag, data, size, c->version, encoded_name.c_str()); + print_received_command(command, flag, data.data(), data.size(), c->version, + encoded_name.c_str()); auto fn = handlers[static_cast(c->version)][command & 0xFF]; if (fn) { - fn(s, c, command, flag, size, data); + fn(s, c, command, flag, data); } else { - process_unimplemented_command(s, c, command, flag, size, data); + process_unimplemented_command(s, c, command, flag, data); } } diff --git a/src/ReceiveCommands.hh b/src/ReceiveCommands.hh index 9bf4b650..b84d74f9 100644 --- a/src/ReceiveCommands.hh +++ b/src/ReceiveCommands.hh @@ -6,42 +6,8 @@ -// These commands' structures are defined here because they're used by both the -// game server and proxy server - -struct VerifyLicenseCommand_GC_DB { - char unused[0x20]; - char serial_number[0x10]; - char access_key[0x10]; - char unused2[0x08]; - uint32_t sub_version; - char serial_number2[0x30]; - char access_key2[0x30]; - char password[0x30]; -} __attribute__((packed)); - -struct LoginCommand_GC_9E { - uint32_t player_tag; // 00 00 01 00 if guild card is set (via 04) - uint32_t guild_card_number; // FF FF FF FF if not set - uint32_t unused1[2]; - uint32_t sub_version; - uint8_t unused2[0x24]; // 00 01 00 00 ... (rest is 00) - char serial_number[0x10]; - char access_key[0x10]; - char serial_number2[0x30]; - char access_key2[0x30]; - char name[0x10]; - // Note: there are 8 bytes at the end of cfg that are technically not - // included in the client config on GC, but the field after it is - // sufficiently large and unused anyway - ClientConfig cfg; - uint8_t unused4[0x5C]; -} __attribute__((packed)); - - - 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); + uint16_t command, uint32_t flag, const std::string& data); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 4a8eb2bd..806aa8f4 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -34,7 +34,7 @@ struct ItemSubcommand { void check_size(uint16_t size, uint16_t min_size, uint16_t max_size) { if (size < min_size) { throw runtime_error(string_printf( - "command too small (expected at least 0x%hX bytes, got 0x%hX bytes)", + "command too small (expected at least 0x%hX bytes, received 0x%hX bytes)", min_size, size)); } if (max_size == 0) { @@ -42,7 +42,7 @@ void check_size(uint16_t size, uint16_t min_size, uint16_t max_size) { } if (size > max_size) { throw runtime_error(string_printf( - "command too large (expected at most 0x%hX bytes, got 0x%hX bytes)", + "command too large (expected at most 0x%hX bytes, received 0x%hX bytes)", max_size, size)); } } @@ -928,9 +928,9 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* 08 */ process_subcommand_unimplemented, /* 09 */ process_subcommand_unimplemented, /* 0A */ process_subcommand_monster_hit, - /* 0B */ process_subcommand_forward_check_size_game, // Box destroyed - /* 0C */ process_subcommand_forward_check_size_game, - /* 0D */ process_subcommand_forward_check_size, + /* 0B */ process_subcommand_forward_check_size_game, + /* 0C */ process_subcommand_forward_check_size_game, // Add condition (poison/slow/etc.) + /* 0D */ process_subcommand_forward_check_size_game, // Remove condition (poison/slow/etc.) /* 0E */ process_subcommand_unimplemented, /* 0F */ process_subcommand_unimplemented, /* 10 */ process_subcommand_unimplemented, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 2dbbdad4..79a7515b 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -11,6 +11,7 @@ #include #include "PSOProtocol.hh" +#include "CommandFormats.hh" #include "FileContentsCache.hh" #include "Text.hh" @@ -18,9 +19,6 @@ using namespace std; -#pragma pack(push) -#pragma pack(1) - static FileContentsCache file_cache; @@ -153,28 +151,31 @@ static const char* dc_lobby_server_copyright = "DreamCast Lobby Server. Copyrigh 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"; -string prepare_server_init_contents_dc_pc_gc( +S_ServerInit_DC_GC_02_17 prepare_server_init_contents_dc_pc_gc( bool initial_connection, uint32_t server_key, uint32_t client_key) { - string ret(sizeof(ServerInitCommand_GC_02_17), '\0'); - auto* cmd = reinterpret_cast(ret.data()); + S_ServerInit_DC_GC_02_17 cmd; + memset(&cmd, 0, sizeof(cmd)); - strcpy(cmd->copyright, initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright); - cmd->server_key = server_key; - cmd->client_key = client_key; - strcpy(cmd->after_message, anti_copyright); - return ret; + strcpy(cmd.copyright, initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright); + cmd.server_key = server_key; + cmd.client_key = client_key; + strcpy(cmd.after_message, anti_copyright); + return cmd; } -static void send_server_init_dc_pc_gc(shared_ptr c, - bool initial_connection, uint8_t command) { +void send_server_init_dc_pc_gc(shared_ptr c, + bool initial_connection) { + // PC uses 17 for all server inits; GC uses it only for the first one + uint8_t command = (initial_connection || (c->version == GameVersion::PC)) + ? 0x17 : 0x02; uint32_t server_key = random_object(); uint32_t client_key = random_object(); - string data = prepare_server_init_contents_dc_pc_gc( + auto cmd = prepare_server_init_contents_dc_pc_gc( initial_connection, server_key, client_key); - send_command(c, command, 0x00, data); + send_command(c, command, 0x00, cmd); switch (c->version) { case GameVersion::DC: @@ -191,22 +192,8 @@ static void send_server_init_dc_pc_gc(shared_ptr c, } } -static void send_server_init_pc(shared_ptr c, bool initial_connection) { - send_server_init_dc_pc_gc(c, initial_connection, 0x17); -} - -static void send_server_init_gc(shared_ptr c, bool initial_connection) { - send_server_init_dc_pc_gc(c, initial_connection, initial_connection ? 0x17 : 0x02); -} - -static void send_server_init_bb(shared_ptr s, shared_ptr c) { - struct { - char copyright[0x60]; - uint8_t server_key[0x30]; - uint8_t client_key[0x30]; - char after_message[200]; - } __attribute__((packed)) cmd; - +void send_server_init_bb(shared_ptr s, shared_ptr c) { + S_ServerInit_BB_03 cmd; memset(&cmd, 0, sizeof(cmd)); strcpy(cmd.copyright, bb_game_server_copyright); random_data(cmd.server_key, 0x30); @@ -220,18 +207,11 @@ static void send_server_init_bb(shared_ptr s, shared_ptr c) sizeof(cmd.client_key))); } -static void send_server_init_patch(shared_ptr c) { - 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 - } __attribute__((packed)) cmd; - +void send_server_init_patch(shared_ptr c) { uint32_t server_key = random_object(); uint32_t client_key = random_object(); + S_ServerInit_Patch_02 cmd; memset(&cmd, 0, sizeof(cmd)); strcpy(cmd.copyright, patch_server_copyright); cmd.server_key = server_key; @@ -244,37 +224,39 @@ static void send_server_init_patch(shared_ptr c) { void send_server_init(shared_ptr s, 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); - } else if (c->version == GameVersion::GC) { - send_server_init_gc(c, initial_connection); - } else if (c->version == GameVersion::BB) { - send_server_init_bb(s, c); - } else { - throw logic_error("unimplemented versioned command"); + switch (c->version) { + case GameVersion::DC: + case GameVersion::PC: + case GameVersion::GC: + send_server_init_dc_pc_gc(c, initial_connection); + break; + case GameVersion::PATCH: + send_server_init_patch(c); + break; + case GameVersion::BB: + send_server_init_bb(s, c); + break; + default: + 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; - } __attribute__((packed)) cmd = { - 0x00010000, - c->license->serial_number, - c->export_config(), - }; + S_UpdateClientConfig_DC_PC_GC_04 cmd; + memset(&cmd, 0, sizeof(cmd)); + cmd.player_tag = 0x00010000; + cmd.guild_card_number = c->license->serial_number; + cmd.cfg = c->export_config(); send_command(c, 0x04, 0x00, cmd); } void send_reconnect(shared_ptr c, uint32_t address, uint16_t port) { - ReconnectCommand_19 cmd = {address, port, 0}; + S_Reconnect_19 cmd = {address, port, 0}; send_command(c, 0x19, 0x00, cmd); } @@ -282,17 +264,7 @@ void send_reconnect(shared_ptr c, uint32_t address, uint16_t port) { // that connect on the same port void send_pc_gc_split_reconnect(shared_ptr c, uint32_t address, uint16_t pc_port, uint16_t gc_port) { - struct { - be_uint32_t pc_address; - uint16_t pc_port; - uint8_t unused1[0x0F]; - uint8_t gc_command; - uint8_t gc_flag; - uint16_t gc_size; - be_uint32_t gc_address; - uint16_t gc_port; - uint8_t unused2[0xB0 - 0x23]; - } __attribute__((packed)) cmd; + S_ReconnectSplit_19 cmd; memset(&cmd, 0, sizeof(cmd)); cmd.pc_address = address; cmd.pc_port = pc_port; @@ -305,24 +277,14 @@ void send_pc_gc_split_reconnect(shared_ptr c, uint32_t address, -// 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 - ClientConfig cfg; - uint32_t caps; // should be 0x00000102 - } __attribute__((packed)) cmd = { - error, - 0x00010000, - c->license->serial_number, - static_cast(random_object()), - c->export_config(), - 0x00000102, - }; + S_ClientInit_BB_E6 cmd; + cmd.error = error; + cmd.player_tag = 0x00010000; + cmd.guild_card_number = c->license->serial_number; + cmd.team_id = static_cast(random_object()); + cmd.cfg = c->export_config(); + cmd.caps = 0x00000102; send_command(c, 0x00E6, 0x00000000, cmd); } @@ -330,50 +292,32 @@ 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; - } __attribute__((packed)) cmd = {player_index, 0x00000002}; + S_PlayerPreview_NoPlayer_BB_E4 cmd = {player_index, 0x00000002}; send_command(c, 0x00E4, 0x00000000, cmd); } else { - struct { - uint32_t player_index; - PlayerDispDataBBPreview preview; - } __attribute__((packed)) cmd = {player_index, *preview}; + S_PlayerPreview_BB_E3 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; - } __attribute__((packed)) cmd = {1, 0}; + S_AcceptClientChecksum_BB_02E8 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; - } __attribute__((packed)) cmd = {1, 0x490, checksum}; + S_GuildCardHeader_BB_01DC cmd = {1, 0x00000490, 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)) { @@ -384,39 +328,26 @@ void send_guild_card_chunk_bb(shared_ptr c, size_t chunk_index) { data_size = 0x6800; } - string contents(8, '\0'); - *reinterpret_cast(contents.data()) = 0; - *reinterpret_cast(contents.data() + 4) = chunk_index; - contents.append(reinterpret_cast(&c->player.guild_cards + chunk_offset), - data_size); + StringWriter w; + w.put_u32l(0); + w.put_u32l(chunk_index); + w.write(&c->player.guild_cards + chunk_offset, data_size); - send_command(c, 0x02DC, 0x00000000, contents); + send_command(c, 0x02DC, 0x00000000, w.str()); } -// 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]; - } __attribute__((packed)); - auto index_data = file_cache.get("system/blueburst/streamfile.ind"); - if (index_data->size() % sizeof(StreamFileEntry)) { + if (index_data->size() % sizeof(S_StreamFileIndexEntry_BB_01EB)) { throw invalid_argument("stream file index not a multiple of entry size"); } - size_t entry_count = index_data->size() / sizeof(StreamFileEntry); + size_t entry_count = index_data->size() / sizeof(S_StreamFileIndexEntry_BB_01EB); send_command(c, 0x01EB, entry_count, index_data); - auto* entries = reinterpret_cast(index_data->data()); + auto* entries = reinterpret_cast(index_data->data()); - struct { - uint32_t chunk_index; - uint8_t data[0x6800]; - } __attribute__((packed)) chunk_cmd; + S_StreamFileChunk_BB_02EB chunk_cmd; chunk_cmd.chunk_index = 0; uint32_t buffer_offset = 0; @@ -453,16 +384,11 @@ void send_stream_file_bb(shared_ptr c) { } } -// 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; - } __attribute__((packed)) cmd = {c->bb_player_index, 1}; + S_ApprovePlayerChoice_BB_00E4 cmd = {c->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()); } @@ -473,10 +399,10 @@ void send_complete_player_bb(shared_ptr c) { // 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); + S_CheckDirectory_Patch_09 cmd; + memset(&cmd, 0, sizeof(cmd)); + strncpy_t(cmd.name, dir, countof(cmd.name)); + send_command(c, 0x09, 0x00, cmd); } @@ -484,74 +410,56 @@ void send_check_directory_patch(shared_ptr c, const char* dir) { //////////////////////////////////////////////////////////////////////////////// // message functions -struct LargeMessageOptionalHeader { - uint32_t unused; - uint32_t serial_number; -} __attribute__((packed)); - -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(data.data()) = - {0, from_serial_number}; - } - data += text; - add_color_inplace(data.data() + (include_header ? sizeof(LargeMessageOptionalHeader) / sizeof(char16_t) : 0), - data.size() - (include_header ? sizeof(LargeMessageOptionalHeader) / sizeof(char16_t) : 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)); - *reinterpret_cast(data.data()) = - {0, from_serial_number}; - } - data += encode_sjis(text); - add_color_inplace(data.data() + (include_header ? sizeof(LargeMessageOptionalHeader) : 0), - data.size() - (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); +void send_text(shared_ptr c, StringWriter& w, uint16_t command, + const char16_t* text) { + if ((c->version == GameVersion::DC) || (c->version == GameVersion::GC)) { + string data = encode_sjis(text); + add_color(w, data.c_str(), data.size()); } else { - send_large_message_dc_gc(c, command, text, from_serial_number, include_header); + add_color(w, text); } + while (w.str().size() & 3) { + w.put_u8(0); + } + send_command(c, command, 0x00, w.str()); +} + +void send_header_text(shared_ptr c, uint16_t command, + uint32_t guild_card_number, const char16_t* text) { + StringWriter w; + w.put(SC_TextHeader_01_06_11({0, guild_card_number})); + send_text(c, w, command, text); +} + +void send_text(shared_ptr c, uint16_t command, + const char16_t* text) { + StringWriter w; + send_text(c, w, command, text); } 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); + uint16_t command = (c->version == GameVersion::PATCH) ? 0x13 : 0x1A; + send_text(c, command, text); } void send_lobby_name(shared_ptr c, const char16_t* text) { - return send_large_message(c, 0x8A, text, 0, false); + send_text(c, 0x8A, text); } void send_quest_info(shared_ptr c, const char16_t* text) { - return send_large_message(c, 0xA3, text, 0, false); + send_text(c, 0xA3, text); } void send_lobby_message_box(shared_ptr c, const char16_t* text) { - return send_large_message(c, 0x01, text, 0, true); + send_header_text(c, 0x01, 0, text); } void send_ship_info(shared_ptr c, const char16_t* text) { - return send_large_message(c, 0x11, text, 0, true); + send_header_text(c, 0x11, 0, text); } void send_text_message(shared_ptr c, const char16_t* text) { - return send_large_message(c, 0xB0, text, 0, true); + send_header_text(c, 0xB0, 0, text); } void send_text_message(shared_ptr l, const char16_t* text) { @@ -563,6 +471,8 @@ void send_text_message(shared_ptr l, const char16_t* text) { } void send_text_message(shared_ptr s, const char16_t* text) { + // TODO: We should have a collection of all clients (even those not in any + // lobby) and use that instead here for (auto& l : s->all_lobbies()) { send_text_message(l, text); } @@ -577,26 +487,18 @@ void send_chat_message(shared_ptr c, uint32_t from_serial_number, data.append(remove_language_marker(from_name)); data.append(u"\x09\x09J"); data.append(text); - send_large_message(c, 0x06, data.c_str(), from_serial_number, true); + send_header_text(c, 0x06, from_serial_number, data.c_str()); } void send_simple_mail_gc(std::shared_ptr c, uint32_t from_serial_number, const char16_t* from_name, const char16_t* text) { - struct { - uint32_t player_tag; - uint32_t from_serial_number; - char from_name[0x10]; - uint32_t to_serial_number; - char text[0x200]; - } __attribute__((packed)) cmd; - + SC_SimpleMail_GC_81 cmd; memset(&cmd, 0, sizeof(cmd)); cmd.player_tag = 0x00010000; cmd.from_serial_number = from_serial_number; encode_sjis(cmd.from_name, from_name, sizeof(cmd.from_name) / sizeof(cmd.from_name[0])); cmd.to_serial_number = c->license->serial_number; encode_sjis(cmd.text, text, sizeof(cmd.text) / sizeof(cmd.text[0])); - send_command(c, 0x81, 0x00, cmd); } @@ -614,58 +516,29 @@ void send_simple_mail(std::shared_ptr c, uint32_t from_serial_number, //////////////////////////////////////////////////////////////////////////////// // 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]; - } __attribute__((packed)); - vector entries; - +template +void send_info_board_t(shared_ptr c, shared_ptr l) { + vector> entries; 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, 0xAC); + memset(&e, 0, sizeof(e)); + strncpy_t(e.name, c->player.disp.name, countof(e.name)); + strncpy_t(e.message, c->player.info_board, countof(e.message)); + add_color_inplace(e.message, countof(e.message)); } - - send_command(c, 0xD8, entries.size(), entries); -} - -static void send_info_board_dc_gc(shared_ptr c, shared_ptr l) { - struct Entry { - char name[0x10]; - char message[0xAC]; - } __attribute__((packed)); - vector entries; - - 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, 0xAC); - } - send_command(c, 0xD8, entries.size(), 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); + send_info_board_t(c, l); } else { - send_info_board_dc_gc(c, l); + send_info_board_t(c, l); } } @@ -674,156 +547,70 @@ void send_info_board(shared_ptr c, shared_ptr 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, +template +void send_card_search_result_t( + 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; - } __attribute__((packed)); - struct { - uint16_t pc_size; - uint8_t pc_command; - uint8_t pc_flag; - } __attribute__((packed)); - } __attribute__((packed)); - uint32_t address; - uint16_t port; - uint16_t unused; - } __attribute__((packed)) destination_command; - char location_string[0x44]; - uint32_t menu_id; - uint32_t lobby_id; - char unused[0x3C]; - char16_t name[0x20]; - } __attribute__((packed)) cmd; - + S_GuildCardSearchResult cmd; memset(&cmd, 0, sizeof(cmd)); cmd.player_tag = 0x00010000; 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; - } + cmd.reconnect_command_header.size = sizeof(cmd.reconnect_command_header) + sizeof(cmd.reconnect_command); + cmd.reconnect_command_header.command = 0x19; + cmd.reconnect_command_header.flag = 0x00; // TODO: make this actually make sense... currently we just take the sockname - // for the target client + // for the target client. This also doesn't work if the client is on a virtual + // connection (the address and port are zero). 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; + cmd.reconnect_command.address = local_addr->sin_addr.s_addr; + cmd.reconnect_command.port = ntohs(local_addr->sin_port); + cmd.reconnect_command.unused = 0; auto encoded_server_name = encode_sjis(s->name); 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(), encoded_server_name.c_str()); + "%s,Block 00,,%s", encoded_lobby_name.c_str(), encoded_server_name.c_str()); } else { - snprintf(cmd.location_string, sizeof(cmd.location_string), "Block 00, ,%s", + snprintf(cmd.location_string, sizeof(cmd.location_string), "Block 00,,%s", encoded_server_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); + strncpy_t(cmd.name, result->player.disp.name, countof(cmd.name)); - send_command(c, 0x40, 0x00, cmd); + send_command(c, 0x41, 0x00, cmd); } -static void send_card_search_result_bb(shared_ptr s, - shared_ptr c, shared_ptr result, +void send_card_search_result( + 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; - } __attribute__((packed)) destination_command; - char location_string[0x44]; - uint32_t menu_id; - uint32_t lobby_id; - char unused[0x3C]; - char16_t name[0x20]; - } __attribute__((packed)) cmd; - - memset(&cmd, 0, sizeof(cmd)); - cmd.player_tag = 0x00010000; - 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; - - auto encoded_server_name = encode_sjis(s->name); - 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(), encoded_server_name.c_str()); + if ((c->version == GameVersion::DC) || (c->version == GameVersion::GC)) { + send_card_search_result_t( + s, c, result, result_lobby); + } else if (c->version == GameVersion::PC) { + send_card_search_result_t( + s, c, result, result_lobby); + } else if (c->version == GameVersion::BB) { + send_card_search_result_t( + s, c, result, result_lobby); } else { - snprintf(cmd.location_string, sizeof(cmd.location_string), "Block 00, ,%s", - encoded_server_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); + throw logic_error("unimplemented versioned command"); } } //////////////////////////////////////////////////////////////////////////////// // 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; - } __attribute__((packed)) cmd; - +void send_guild_card_gc(shared_ptr c, shared_ptr source) { + S_SendGuildCard_GC cmd; + memset(&cmd, 0, sizeof(cmd)); cmd.subcommand = 0x06; cmd.subsize = 0x25; cmd.unused = 0x0000; @@ -832,30 +619,18 @@ static void send_guild_card_gc(shared_ptr c, shared_ptr source) cmd.reserved2 = 1; cmd.serial_number = source->license->serial_number; - encode_sjis(cmd.name, source->player.disp.name, 0x18); + encode_sjis(cmd.name, source->player.disp.name, countof(cmd.name)); remove_language_marker_inplace(cmd.name); - encode_sjis(cmd.desc, source->player.guild_card_desc, 0x6C); + encode_sjis(cmd.desc, source->player.guild_card_desc, countof(cmd.desc)); 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; - } __attribute__((packed)) cmd; - +void send_guild_card_bb(shared_ptr c, shared_ptr source) { + S_SendGuildCard_BB cmd; + memset(&cmd, 0, sizeof(cmd)); cmd.subcommand = 0x06; cmd.subsize = 0x43; cmd.unused = 0x0000; @@ -863,11 +638,11 @@ static void send_guild_card_bb(shared_ptr c, shared_ptr source) cmd.reserved2 = 1; cmd.serial_number = source->license->serial_number; - char16cpy(cmd.name, source->player.disp.name, 0x18); + char16ncpy(cmd.name, source->player.disp.name, countof(cmd.name)); remove_language_marker_inplace(cmd.name); - char16cpy(cmd.team_name, source->player.team_name, 0x10); + char16ncpy(cmd.team_name, source->player.team_name, countof(cmd.team_name)); remove_language_marker_inplace(cmd.team_name); - char16cpy(cmd.desc, source->player.guild_card_desc, 0x58); + char16ncpy(cmd.desc, source->player.guild_card_desc, countof(cmd.desc)); cmd.section_id = source->player.disp.section_id; cmd.char_class = source->player.disp.char_class; @@ -889,88 +664,42 @@ void send_guild_card(shared_ptr c, shared_ptr source) { //////////////////////////////////////////////////////////////////////////////// // 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]; - } __attribute__((packed)); +template +void send_menu_t( + shared_ptr c, + const char16_t* menu_name, + uint32_t menu_id, + const vector& items, + bool is_info_menu) { - vector entries; + 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); + auto& e = entries.back(); + memset(&e, 0, sizeof(e)); + e.menu_id = menu_id; + e.item_id = 0xFFFFFFFF; + e.flags = 0x0004; + strncpy_t(e.text, menu_name, countof(e.text)); } for (const auto& item : items) { - if ((c->version == GameVersion::BB) && (item.flags & MenuItemFlag::INVISIBLE_ON_BB)) { - continue; - } - if ((c->version == GameVersion::PC) && (item.flags & MenuItemFlag::INVISIBLE_ON_PC)) { - continue; - } - if ((item.flags & MenuItemFlag::REQUIRES_MESSAGE_BOXES) && - (c->flags & ClientFlag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION)) { + if (((c->version == GameVersion::DC) && (item.flags & MenuItemFlag::INVISIBLE_ON_DC)) || + ((c->version == GameVersion::PC) && (item.flags & MenuItemFlag::INVISIBLE_ON_PC)) || + ((c->version == GameVersion::GC) && (item.flags & MenuItemFlag::INVISIBLE_ON_GC)) || + ((c->version == GameVersion::BB) && (item.flags & MenuItemFlag::INVISIBLE_ON_BB)) || + ((c->flags & ClientFlag::EPISODE_3_GAMES) && (item.flags & MenuItemFlag::INVISIBLE_ON_GC_EPISODE_3)) || + ((item.flags & MenuItemFlag::REQUIRES_MESSAGE_BOXES) && (c->flags & ClientFlag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION))) { 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]; - } __attribute__((packed)); - - 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::INVISIBLE_ON_DC)) { - continue; - } - if ((c->version == GameVersion::GC) && (item.flags & MenuItemFlag::INVISIBLE_ON_GC)) { - continue; - } - if ((c->flags & ClientFlag::EPISODE_3_GAMES) && (item.flags & MenuItemFlag::INVISIBLE_ON_GC_EPISODE_3)) { - continue; - } - if ((item.flags & MenuItemFlag::REQUIRES_MESSAGE_BOXES) && - (c->flags & ClientFlag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION)) { - 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); + auto& e = entries.back(); + memset(&e, 0, sizeof(e)); + e.menu_id = menu_id; + e.item_id = item.item_id; + e.flags = (c->version == GameVersion::BB) ? 0x0004 : 0x0F04; + strncpy_t(e.text, item.name.c_str(), countof(e.text)); } send_command(c, is_info_menu ? 0x1F : 0x07, entries.size() - 1, entries); @@ -980,33 +709,29 @@ 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); + send_menu_t(c, menu_name, menu_id, items, is_info_menu); } else { - send_menu_dc_gc(c, menu_name, menu_id, items, is_info_menu); + send_menu_t(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; - } __attribute__((packed)); - - vector entries; +template +void send_game_menu_t(shared_ptr c, shared_ptr s) { + vector> entries; { entries.emplace_back(); auto& e = entries.back(); + memset(&e, 0, sizeof(e)); e.menu_id = GAME_MENU_ID; - e.game_id = 0; - char16cpy(e.name, s->name.c_str(), 0x10); + e.game_id = 0x00000000; + e.difficulty_tag = 0x00; + e.num_players = 0x00; + strncpy_t(e.name, s->name.c_str(), countof(e.name)); + e.episode = 0x00; + e.flags = 0x04; } for (shared_ptr l : s->all_lobbies()) { if (!l->is_game()) { @@ -1015,279 +740,88 @@ static void send_game_menu_pc(shared_ptr c, shared_ptr s) { if (l->version != c->version) { continue; } - - entries.emplace_back(); - auto& e = entries.back(); - e.menu_id = GAME_MENU_ID; - - 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; - } __attribute__((packed)); - - vector entries; - { - entries.emplace_back(); - auto& e = entries.back(); - e.menu_id = GAME_MENU_ID; - e.game_id = 0; - encode_sjis(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) { + if (!(l->flags & LobbyFlag::EPISODE_3) != !(c->flags & ClientFlag::EPISODE_3_GAMES)) { continue; } entries.emplace_back(); auto& e = entries.back(); + memset(&e, 0, sizeof(e)); e.menu_id = GAME_MENU_ID; - e.game_id = l->lobby_id; - e.difficulty_tag = ((l->flags & LobbyFlag::EPISODE_3) ? 0x0A : (l->difficulty + 0x22)); + e.difficulty_tag = ((l->flags & LobbyFlag::EPISODE_3) + ? 0x0A : (l->difficulty + 0x22)); e.num_players = l->count_clients(); - e.episode = 0; + e.episode = ((c->version == GameVersion::BB) ? (l->max_clients << 4) : 0) | l->episode; if (l->flags & LobbyFlag::EPISODE_3) { e.flags = (l->password[0] ? 2 : 0); } else { - e.flags = ((l->episode << 6) | (l->mode << 4) | (l->password[0] ? 2 : 0)); + e.flags = ((l->episode << 6) | ((l->mode % 3) << 4) | (l->password[0] ? 2 : 0)) | ((l->mode == 3) ? 4 : 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; - } __attribute__((packed)); - - vector entries; - { - entries.emplace_back(); - auto& e = entries.back(); - e.menu_id = GAME_MENU_ID; - e.game_id = 0; - e.flags = 0x0004; - char16cpy(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; - - 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); + strncpy_t(e.name, l->name, countof(e.name)); } 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); + if ((c->version == GameVersion::DC) || (c->version == GameVersion::GC)) { + send_game_menu_t(c, s); } else { - throw logic_error("unimplemented versioned command"); + send_game_menu_t(c, s); } } -//////////////////////////////////////////////////////////////////////////////// -// 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]; - } __attribute__((packed)); - - vector entries; +template +void send_quest_menu_t( + shared_ptr c, + uint32_t menu_id, + const vector>& quests, + bool is_download_menu) { + vector entries; for (const auto& quest : quests) { entries.emplace_back(); auto& e = entries.back(); + memset(&e, 0, sizeof(e)); 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); - add_color_inplace(e.short_desc, 0x70); + e.item_id = quest->quest_id; + strncpy_t(e.name, quest->name.c_str(), countof(e.name)); + strncpy_t(e.short_desc, quest->short_description.c_str(), countof(e.short_desc)); + add_color_inplace(e.short_desc, countof(e.short_desc)); } - 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]; - } __attribute__((packed)); - - vector entries; +template +void send_quest_menu_t( + shared_ptr c, + uint32_t menu_id, + const vector& items, + bool is_download_menu) { + vector entries; for (const auto& item : items) { entries.emplace_back(); auto& e = entries.back(); + memset(&e, 0, sizeof(e)); 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); + strncpy_t(e.name, item.name.c_str(), 0x20); + strncpy_t(e.short_desc, item.description.c_str(), 0x70); add_color_inplace(e.short_desc, 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]; - } __attribute__((packed)); - - 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); - add_color_inplace(e.short_desc, 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]; - } __attribute__((packed)); - - 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); - add_color_inplace(e.short_desc, 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]; - } __attribute__((packed)); - - 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); - add_color_inplace(e.short_desc, 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]; - } __attribute__((packed)); - - 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); - add_color_inplace(e.short_desc, 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); + send_quest_menu_t(c, menu_id, quests, is_download_menu); } else if (c->version == GameVersion::GC) { - send_quest_menu_gc(c, menu_id, quests, is_download_menu); + send_quest_menu_t(c, menu_id, quests, is_download_menu); } else if (c->version == GameVersion::BB) { - send_quest_menu_bb(c, menu_id, quests, is_download_menu); + send_quest_menu_t(c, menu_id, quests, is_download_menu); } else { throw logic_error("unimplemented versioned command"); } @@ -1296,11 +830,11 @@ void send_quest_menu(shared_ptr c, uint32_t menu_id, 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); + send_quest_menu_t(c, menu_id, items, is_download_menu); } else if (c->version == GameVersion::GC) { - send_quest_menu_gc(c, menu_id, items, is_download_menu); + send_quest_menu_t(c, menu_id, items, is_download_menu); } else if (c->version == GameVersion::BB) { - send_quest_menu_bb(c, menu_id, items, is_download_menu); + send_quest_menu_t(c, menu_id, items, is_download_menu); } else { throw logic_error("unimplemented versioned command"); } @@ -1311,13 +845,7 @@ void send_lobby_list(shared_ptr c, shared_ptr s) { // 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 - } __attribute__((packed)); - - vector entries; + vector entries; for (shared_ptr l : s->all_lobbies()) { if (!(l->flags & LobbyFlag::DEFAULT)) { continue; @@ -1341,77 +869,26 @@ void send_lobby_list(shared_ptr c, shared_ptr s) { //////////////////////////////////////////////////////////////////////////////// // 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 rare_seed; - uint8_t episode; - uint8_t unused2; - uint8_t solo_mode; - uint8_t unused3; - } __attribute__((packed)) cmd; +template +void send_join_game_t(shared_ptr c, shared_ptr l) { + S_JoinGame cmd; + memset(&cmd, 0, sizeof(cmd)); size_t player_count = 0; 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 { + if (l->clients[x]) { cmd.lobby_data[x].player_tag = 0x00010000; cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; - // See comment in send_join_lobby_gc about Episode III behavior here. Even - // though this doesn't apply to PSO PC, it's better to be consistent. + // See comment in send_join_lobby_t about Episode III behavior here cmd.lobby_data[x].ip_address = 0x7F000001; 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.rare_seed = l->rare_seed; - 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) { - JoinGameCommand_GC_64 cmd; - - size_t player_count = 0; - 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 { - cmd.lobby_data[x].player_tag = 0x00010000; - cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; - // See comment in send_join_lobby_gc about Episode III behavior here. - cmd.lobby_data[x].ip_address = 0x7F000001; - cmd.lobby_data[x].client_id = c->lobby_client_id; - encode_sjis(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, 0x10); + strncpy_t(cmd.lobby_data[x].name, l->clients[x]->player.disp.name, + countof(cmd.lobby_data[x].name)); if (l->flags & LobbyFlag::EPISODE_3) { - cmd.player[x].inventory = l->clients[x]->player.inventory; - cmd.player[x].disp = l->clients[x]->player.disp.to_pcgc(); + cmd.players_ep3[x].inventory = l->clients[x]->player.inventory; + cmd.players_ep3[x].disp = convert_player_disp_data( + l->clients[x]->player.disp); } player_count++; } @@ -1419,7 +896,7 @@ static void send_join_game_gc(shared_ptr c, shared_ptr l) { cmd.client_id = c->lobby_client_id; cmd.leader_id = l->leader_id; - cmd.disable_udp = 0x01; + cmd.disable_udp = 0x01; // TODO: This is unused on PC/BB. Is it OK to use 1 here anyway? cmd.difficulty = l->difficulty; cmd.battle_mode = (l->mode == 1) ? 1 : 0; cmd.event = l->event; @@ -1427,233 +904,113 @@ static void send_join_game_gc(shared_ptr c, shared_ptr l) { cmd.challenge_mode = (l->mode == 2) ? 1 : 0; cmd.rare_seed = l->rare_seed; cmd.episode = l->episode; + cmd.unused2 = 0x01; + cmd.solo_mode = (l->mode == 3); + cmd.unused3 = 0x00; - // player data is only sent in Episode III games; in other versions, the + // Player data is only sent in Episode III games; in other versions, the // players send each other their data using 62/6D commands during loading size_t data_size = (l->flags & LobbyFlag::EPISODE_3) - ? sizeof(cmd) : (sizeof(cmd) - sizeof(cmd.player)); + ? sizeof(cmd) : (sizeof(cmd) - sizeof(cmd.players_ep3)); 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 rare_seed; - uint8_t episode; - uint8_t unused2; - uint8_t solo_mode; - uint8_t unused3; - } __attribute__((packed)) cmd; - - size_t player_count = 0; - 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]) { - cmd.lobby_data[x].player_tag = 0x00010000; - 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.rare_seed = l->rare_seed; - 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) { - 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; - } __attribute__((packed)) cmd = { - c->lobby_client_id, - l->leader_id, - 0x01, - lobby_type, - l->block, - l->event, - 0x00000000, - }; - - struct Entry { - PlayerLobbyDataPC lobby_data; - PlayerLobbyJoinDataPCGC data; - } __attribute__((packed)); - vector entries; - - for (size_t x = 0; x < l->max_clients; x++) { - if (!l->clients[x]) { - continue; - } - - entries.emplace_back(); - auto& e = entries.back(); - - e.lobby_data.player_tag = 0x00010000; - e.lobby_data.guild_card = l->clients[x]->license->serial_number; - // See comment in send_join_lobby_gc about Episode III behavior here. Even - // though this doesn't apply to PSO PC, it's better to be consistent. - e.lobby_data.ip_address = 0x7F000001; - 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) { - uint8_t lobby_type = l->type; - if (c->flags & ClientFlag::EPISODE_3_GAMES) { - if ((l->type > 0x14) && (l->type < 0xE9)) { - lobby_type = l->block - 1; +template +void send_join_lobby_t(shared_ptr c, shared_ptr l, + shared_ptr joining_client = nullptr) { + uint8_t command; + if (l->is_game()) { + if (joining_client) { + command = 0x65; + } else { + throw logic_error("send_join_lobby_t should not be used for primary game join command"); } } else { - if ((l->type > 0x11) && (l->type != 0x67) && (l->type != 0xD4) && (l->type < 0xFC)) { + command = joining_client ? 0x68 : 0x67; + + } + + uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; + // Allow non-canonical lobby types on GC + if (c->version == GameVersion::GC) { + if (c->flags & ClientFlag::EPISODE_3_GAMES) { + if ((l->type > 0x14) && (l->type < 0xE9)) { + lobby_type = l->block - 1; + } + } else { + if ((l->type > 0x11) && (l->type != 0x67) && (l->type != 0xD4) && (l->type < 0xFC)) { + lobby_type = l->block - 1; + } + } + } else { + if (lobby_type > 0x0E) { lobby_type = l->block - 1; } } - 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; - } __attribute__((packed)) cmd = { - c->lobby_client_id, - l->leader_id, - 0x01, - lobby_type, - l->block, - l->event, - 0x00000000, - }; + S_JoinLobby cmd; + memset(&cmd, 0, sizeof(cmd)); + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.disable_udp = 0x01; + cmd.lobby_number = lobby_type; + cmd.block_number = l->block; + cmd.event = l->event; + cmd.unused = 0x00000000; - struct Entry { - PlayerLobbyDataGC lobby_data; - PlayerLobbyJoinDataPCGC data; - } __attribute__((packed)); - vector entries; - - for (size_t x = 0; x < l->max_clients; x++) { - if (!l->clients[x]) { - continue; + vector> lobby_clients; + if (joining_client) { + lobby_clients.emplace_back(joining_client); + } else { + for (auto lc : l->clients) { + if (lc) { + lobby_clients.emplace_back(lc); + } } + } - entries.emplace_back(); - auto& e = entries.back(); - + size_t used_entries = 0; + for (const auto& lc : lobby_clients) { + auto& e = cmd.entries[used_entries++]; + memset(&e.lobby_data, 0, sizeof(e.lobby_data)); e.lobby_data.player_tag = 0x00010000; - e.lobby_data.guild_card = l->clients[x]->license->serial_number; + e.lobby_data.guild_card = lc->license->serial_number; // There's a strange behavior (bug? "feature"?) in Episode 3 where the start // button does nothing in the lobby (hence you can't "quit game") if the // client's IP address is zero. So, we fill it in with a fake nonzero value // to avoid this behavior. e.lobby_data.ip_address = 0x7F000001; - 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) { - 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; - } __attribute__((packed)) cmd = { - c->lobby_client_id, - l->leader_id, - 0x01, - lobby_type, - l->block, - l->event, - 0x00000000, - }; - - struct Entry { - PlayerLobbyDataBB lobby_data; - PlayerLobbyJoinDataBB data; - } __attribute__((packed)); - vector entries; - - for (size_t x = 0; x < l->max_clients; x++) { - if (!l->clients[x]) { - continue; + e.lobby_data.client_id = lc->lobby_client_id; + strncpy_t(e.lobby_data.name, lc->player.disp.name, + countof(e.lobby_data.name)); + e.inventory = lc->player.inventory; + e.disp = convert_player_disp_data(lc->player.disp); + if (c->version == GameVersion::PC) { + e.disp.enforce_pc_limits(); } - - entries.emplace_back(); - auto& e = entries.back(); - memset(&e.lobby_data, 0, sizeof(e.lobby_data)); - - e.lobby_data.player_tag = 0x00010000; - 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); + send_command(c, command, used_entries, &cmd, cmd.size(used_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); + send_join_game_t(c, l); } else if (c->version == GameVersion::GC) { - send_join_game_gc(c, l); + send_join_game_t(c, l); } else if (c->version == GameVersion::BB) { - send_join_game_bb(c, l); + send_join_game_t(c, l); } else { throw logic_error("unimplemented versioned command"); } - } else { if (c->version == GameVersion::PC) { - send_join_lobby_pc(c, l); + send_join_lobby_t(c, l); } else if (c->version == GameVersion::GC) { - send_join_lobby_gc(c, l); + send_join_lobby_t(c, l); } else if (c->version == GameVersion::BB) { - send_join_lobby_bb(c, l); + send_join_lobby_t(c, l); } else { throw logic_error("unimplemented versioned command"); } @@ -1668,122 +1025,21 @@ void send_join_lobby(shared_ptr c, shared_ptr l) { } } - - -//////////////////////////////////////////////////////////////////////////////// -// 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, +void send_player_join_notification(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; - } __attribute__((packed)) cmd = { - 0xFF, - l->leader_id, - 0x01, - lobby_type, - l->block, - l->event, - 0x00000000, - {0x00000100, joining_client->license->serial_number, 0x00000000, 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; - } __attribute__((packed)) cmd = { - 0xFF, - l->leader_id, - 0x01, - lobby_type, - l->block, - l->event, - 0x00000000, - {0x00000100, joining_client->license->serial_number, 0x00000000, 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; - } __attribute__((packed)) 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 = 0x00010000; - 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); + send_join_lobby_t(c, l, joining_client); } else if (c->version == GameVersion::GC) { - send_player_join_notification_gc(c, l, joining_client); + send_join_lobby_t(c, l, joining_client); } else if (c->version == GameVersion::BB) { - send_player_join_notification_bb(c, l, joining_client); + send_join_lobby_t(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; - } __attribute__((packed)) cmd = {leaving_client_id, l->leader_id, 0}; + S_LeaveLobby_66_69 cmd = {leaving_client_id, l->leader_id, 0}; send_command(l, l->is_game() ? 0x66 : 0x69, leaving_client_id, cmd); } @@ -1797,12 +1053,7 @@ void send_get_player_info(shared_ptr c) { // arrows void send_arrow_update(shared_ptr l) { - struct Entry { - uint32_t player_tag; - uint32_t serial_number; - uint32_t arrow_color; - } __attribute__((packed)); - vector entries; + vector entries; for (size_t x = 0; x < l->max_clients; x++) { if (!l->clients[x]) { @@ -1909,93 +1160,51 @@ void send_revive_player(shared_ptr l, shared_ptr c) { // 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; - } __attribute__((packed)) cmd = {0x5F, 0x0A, 0x0000, area, from_enemy, request_id, x, y, 0, item}; + S_DropItem_BB 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, 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; - } __attribute__((packed)) cmd = {0x5D, 0x09, 0x0000, area, 0, x, y, 0, item}; + S_DropStackedItem_BB 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; - } __attribute__((packed)) cmd = {0x59, 0x03, c->lobby_client_id, c->lobby_client_id, area, item_id}; + S_PickUpItem_BB 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; - } __attribute__((packed)) cmd = {0xBE, 0x07, c->lobby_client_id, item, 0}; + S_CreateInventoryItem_BB 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; - } __attribute__((packed)) cmd = {0x29, 0x03, c->lobby_client_id, item_id, amount}; + S_DestroyItem_BB cmd = { + 0x29, 0x03, c->lobby_client_id, item_id, amount}; send_command(l, 0x60, 0x00, cmd); } -// sends the player his/her bank data +// sends the player their 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 = random_object(); - 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; - } __attribute__((packed)) cmd = {0xBC, 0, 0, 0, checksum, c->player.bank.num_items, c->player.bank.meseta}; + S_BankContentsHeader_BB 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; @@ -2005,15 +1214,7 @@ void send_bank(shared_ptr c) { // sends the player a shop's contents void send_shop(shared_ptr c, uint8_t shop_type) { - struct { - uint8_t subcommand; // B6 - uint8_t size; // 2C regardless of the number of items?? - uint16_t params; // 037F - uint8_t shop_type; - uint8_t num_items; - uint16_t unused; - ItemData entries[20]; - } __attribute__((packed)) cmd = { + S_ShopContents_BB cmd = { 0xB6, 0x2C, 0x037F, @@ -2049,6 +1250,7 @@ void send_level_up(shared_ptr l, shared_ptr c) { } } + // TODO: Make a real struct for this PSOSubcommand sub[5]; sub[0].byte[0] = 0x30; sub[0].byte[1] = 0x05; @@ -2066,6 +1268,7 @@ void send_level_up(shared_ptr l, shared_ptr c) { // gives a player EXP void send_give_experience(shared_ptr l, shared_ptr c, uint32_t amount) { + // TODO: Make a real struct for this PSOSubcommand sub[2]; sub[0].word[0] = 0x02BF; sub[0].word[1] = c->lobby_client_id; @@ -2092,13 +1295,8 @@ void send_ep3_card_list_update(shared_ptr c) { // 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; - } __attribute__((packed)) cmd = {0, "\0\0\0\0\0\0\0\0\0\0\0", 0x00FFFFFF, 0x00FFFFFF, 0xFFFFFFFF}; + S_RankUpdate_GC_Ep3_B7 cmd = { + 0, "\0\0\0\0\0\0\0\0\0\0\0", 0x00FFFFFF, 0x00FFFFFF, 0xFFFFFFFF}; send_command(c, 0xB7, 0x00, cmd); } @@ -2134,54 +1332,36 @@ void send_ep3_map_data(shared_ptr l, uint32_t map_id) { -static void send_quest_open_file_pc_gc(shared_ptr c, - const string& filename, uint32_t file_size, bool is_download_quest, +template +void send_quest_open_file_t( + 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; - } __attribute__((packed)) cmd; + CommandT cmd; memset(&cmd, 0, sizeof(cmd)); - strncpy(cmd.name, filename.c_str(), 0x1F); cmd.flags = 2 + is_ep3_quest; - strncpy(cmd.filename, filename.c_str(), 0x0F); cmd.file_size = file_size; + strncpy_t(cmd.name, filename.c_str(), countof(cmd.name)); + strncpy_t(cmd.filename, filename.c_str(), countof(cmd.filename)); 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]; - } __attribute__((packed)) cmd; - memset(&cmd, 0, sizeof(cmd)); - cmd.flags = 2 + is_ep3_quest; - strncpy(cmd.filename, filename.c_str(), 0x0F); - 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) { +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; - } __attribute__((packed)) cmd; - memset(cmd.filename, 0, 0x10); - strncpy(cmd.filename, filename, 0x0F); + S_WriteFile_13_A7 cmd; + memset(cmd.filename, 0, countof(cmd.filename)); + strncpy_t(cmd.filename, filename, countof(cmd.filename)); memcpy(cmd.data, data, size); if (size < 0x400) { memset(&cmd.data[size], 0, 0x400 - size); @@ -2195,11 +1375,11 @@ 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); + send_quest_open_file_t( + 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); + send_quest_open_file_t( + c, basename, contents.size(), is_download_quest, is_ep3_quest); } else { throw invalid_argument("cannot send quest files to this version of client"); } @@ -2243,5 +1423,3 @@ void send_change_event(shared_ptr l, uint8_t new_event) { void send_change_event(shared_ptr s, uint8_t new_event) { send_command(s, 0xDA, new_event); } - -#pragma pack(pop) diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 13356543..efcf5945 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -13,6 +13,7 @@ #include "Menu.hh" #include "Quest.hh" #include "Text.hh" +#include "CommandFormats.hh" @@ -33,54 +34,41 @@ void send_command(std::shared_ptr l, uint16_t command, uint32_t flag = 0, void send_command(std::shared_ptr s, uint16_t command, uint32_t flag = 0, const void* data = nullptr, size_t size = 0); -template -void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, - const STRUCT& data) { +template +static void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, + const StructT& data) { send_command(c, command, flag, &data, sizeof(data)); } -template -void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, +template +static void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, const std::string& data) { send_command(c, command, flag, data.data(), data.size()); } -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 std::vector& data) { + send_command(c, command, flag, data.data(), data.size() * sizeof(StructT)); } -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)); +template +void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, + const StructT& data, const std::vector& array_data) { + std::string all_data(reinterpret_cast(&data), sizeof(StructT)); all_data.append(reinterpret_cast(array_data.data()), - array_data.size() * sizeof(ENTRY)); + array_data.size() * sizeof(EntryT)); send_command(c, command, flag, all_data.data(), all_data.size()); } -struct ServerInitCommand_GC_02_17 { - char copyright[0x40]; - uint32_t server_key; - uint32_t client_key; - char after_message[200]; -} __attribute__((packed)); - -std::string prepare_server_init_contents_dc_pc_gc( +S_ServerInit_DC_GC_02_17 prepare_server_init_contents_dc_pc_gc( bool initial_connection, uint32_t server_key, uint32_t client_key); void send_server_init(std::shared_ptr s, std::shared_ptr c, bool initial_connection); void send_update_client_config(std::shared_ptr c); -struct ReconnectCommand_19 { - be_uint32_t address; - uint16_t port; - uint16_t unused; -} __attribute__((packed)); - 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, uint16_t gc_port); @@ -111,9 +99,9 @@ void send_chat_message(std::shared_ptr c, uint32_t from_serial_number, void send_simple_mail(std::shared_ptr c, uint32_t from_serial_number, const char16_t* from_name, const char16_t* text); -template +template __attribute__((format(printf, 2, 3))) void send_text_message_printf( - std::shared_ptr t, const char* format, ...) { + std::shared_ptr t, const char* format, ...) { va_list va; va_start(va, format); std::string buf = string_vprintf(format, va); @@ -124,8 +112,11 @@ __attribute__((format(printf, 2, 3))) void send_text_message_printf( 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_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, @@ -137,25 +128,6 @@ 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); -struct JoinGameCommand_GC_64 { - uint32_t variations[0x20]; - PlayerLobbyDataGC lobby_data[4]; - uint8_t client_id; - uint8_t leader_id; - uint8_t disable_udp; // guess; putting 0 here causes no movement messages to be sent - uint8_t difficulty; - uint8_t battle_mode; - uint8_t event; - uint8_t section_id; - uint8_t challenge_mode; - uint32_t rare_seed; - uint32_t episode; // for PSOPC, this must be 0x00000100 - struct { - PlayerInventory inventory; - PlayerDispDataPCGC disp; - } __attribute__((packed)) player[4]; // only used on ep3 -} __attribute__((packed)); - 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); diff --git a/src/Server.cc b/src/Server.cc index 27c5e19c..6f2e6c9a 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -211,7 +211,7 @@ void Server::receive_and_process_commands(shared_ptr c) { for_each_received_command(c->bev, c->version, c->crypt_in.get(), [this, c](uint16_t command, uint16_t flag, const std::string& data) { try { - process_command(this->state, c, command, flag, data.size(), data.data()); + process_command(this->state, c, command, flag, data); } catch (const exception& e) { log(INFO, "[Server] Error in client stream: %s", e.what()); c->should_disconnect = true; diff --git a/src/ServerState.cc b/src/ServerState.cc index 341897b4..ffe70f55 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -22,37 +22,56 @@ ServerState::ServerState() ep3_menu_song(-1) { memset(&this->default_key_file, 0, sizeof(this->default_key_file)); + vector> ep3_only_lobbies; + for (size_t x = 0; x < 20; x++) { auto lobby_name = decode_sjis(string_printf("LOBBY%zu", x + 1)); + bool is_ep3_only = (x > 14); + shared_ptr l(new Lobby()); l->flags |= LobbyFlag::PUBLIC | LobbyFlag::DEFAULT | LobbyFlag::PERSISTENT | - ((x > 14) ? LobbyFlag::EPISODE_3 : 0); + (is_ep3_only ? LobbyFlag::EPISODE_3 : 0); l->block = x + 1; l->type = x; - char16cpy(l->name, lobby_name.c_str(), 0x24); + char16ncpy(l->name, lobby_name.c_str(), 0x24); l->max_clients = 12; this->add_lobby(l); + + if (!is_ep3_only) { + this->public_lobby_search_order.emplace_back(l); + } else { + ep3_only_lobbies.emplace_back(l); + } } + + this->public_lobby_search_order_ep3 = this->public_lobby_search_order; + this->public_lobby_search_order_ep3.insert( + this->public_lobby_search_order_ep3.begin(), + ep3_only_lobbies.begin(), + ep3_only_lobbies.end()); } void ServerState::add_client_to_available_lobby(shared_ptr c) { - auto it = this->id_to_lobby.lower_bound(0); - for (; it != this->id_to_lobby.end(); it++) { - if (!(it->second->flags & LobbyFlag::PUBLIC)) { - continue; - } + const auto& search_order = (c->flags & ClientFlag::EPISODE_3_GAMES) + ? this->public_lobby_search_order_ep3 + : this->public_lobby_search_order; + + shared_ptr added_to_lobby; + for (const auto& l : search_order) { try { - it->second->add_client(c); + l->add_client(c); + added_to_lobby = l; break; } catch (const out_of_range&) { } } - if (it == this->id_to_lobby.end()) { + if (!added_to_lobby) { + // TODO: Add the user to a dynamically-created private lobby instead 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); + // Send a join message to the joining player, and notifications to all others + this->send_lobby_join_notifications(added_to_lobby, c); } void ServerState::remove_client_from_lobby(shared_ptr c) { diff --git a/src/ServerState.hh b/src/ServerState.hh index ce878d82..85941989 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -59,6 +59,8 @@ struct ServerState { std::u16string welcome_message; std::map> id_to_lobby; + std::vector> public_lobby_search_order; + std::vector> public_lobby_search_order_ep3; std::atomic next_lobby_id; uint8_t pre_lobby_event; int32_t ep3_menu_song; diff --git a/src/Text.cc b/src/Text.cc index 19bddcd3..3c005d71 100644 --- a/src/Text.cc +++ b/src/Text.cc @@ -13,7 +13,7 @@ using namespace std; -int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) { +int char16ncmp(const char16_t* s1, const char16_t* s2, size_t count) { size_t x; for (x = 0; x < count && s1[x] != 0 && s2[x] != 0; x++) { if (s1[x] < s2[x]) { @@ -30,7 +30,7 @@ int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) { return 0; } -void char16cpy(char16_t* dest, const char16_t* src, size_t count) { +void char16ncpy(char16_t* dest, const char16_t* src, size_t count) { size_t x; for (x = 0; x < count && src[x] != 0; x++) { dest[x] = src[x]; @@ -85,6 +85,9 @@ static const vector& unicode_to_sjis_table() { return unicode_to_sjis_table_data; } +// TODO: It looks like these functions are probably wrong. Specifically, we +// don't write the high byte when encoding non-ASCII chars, do we? + void encode_sjis(char* dest, const char16_t* source, size_t max) { const auto& table = unicode_to_sjis_table(); while (*source && (--max)) { @@ -108,25 +111,30 @@ void decode_sjis(char16_t* dest, const char* source, size_t max) { *dest = 0; } -std::string encode_sjis(const char16_t* source) { +std::string encode_sjis(const char16_t* src, size_t src_count) { const auto& table = unicode_to_sjis_table(); string ret; - while (*source) { - ret.push_back(table[*(source++)]); + for (; *src && (src_count > 0); src_count--) { + ret.push_back(table[*(src++)]); }; return ret; } -std::u16string decode_sjis(const char* source) { +std::u16string decode_sjis(const char* src, size_t src_count) { const auto& table = sjis_to_unicode_table(); u16string ret; - while (*source) { - char16_t src_char = *(source++); + while (*src && (src_count > 0)) { + char16_t src_char = *(src++); + src_count--; if (src_char & 0x80) { - src_char = (src_char << 8) | *(source++); + if (src_count == 0) { + return ret; + } + src_char = (src_char << 8) | *(src++); if ((src_char & 0xFF) == 0) { return ret; } + src_count--; } ret.push_back(table[src_char]); }; @@ -145,10 +153,13 @@ std::string encode_sjis(const std::u16string& source) { std::u16string decode_sjis(const std::string& source) { const auto& table = sjis_to_unicode_table(); u16string ret; - for (size_t x = 0; x < source.size(); x++) { - char16_t src_char = source[x]; + for (size_t x = 0; x < source.size();) { + char16_t src_char = source[x++]; if (src_char & 0x80) { - src_char = (src_char << 8) | source[++x]; + if (x == source.size()) { + return ret; + } + src_char = (src_char << 8) | source[x++]; if ((src_char & 0xFF) == 0) { return ret; } @@ -198,7 +209,7 @@ void remove_language_marker_inplace(char* a) { void remove_language_marker_inplace(char16_t* a) { if ((a[0] == '\t') && (a[1] != 'C')) { - char16cpy(a, &a[2], char16len(a) - 2); + char16ncpy(a, &a[2], char16len(a) - 2); } } diff --git a/src/Text.hh b/src/Text.hh index 6a4e14f6..335cdce5 100644 --- a/src/Text.hh +++ b/src/Text.hh @@ -4,21 +4,56 @@ #include #include +#include +#include -int char16cmp(const char16_t* s1, const char16_t* s2, size_t count); -void char16cpy(char16_t* dest, const char16_t* src, size_t count); + +#define countof(F) (sizeof(F) / sizeof(F[0])) + + + +int char16ncmp(const char16_t* s1, const char16_t* s2, size_t count); +void char16ncpy(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 char16_t* source, size_t src_count); +std::u16string decode_sjis(const char* source, size_t src_count); std::string encode_sjis(const std::u16string& source); std::u16string decode_sjis(const std::string& source); + +template +void strncpy_t(DestT*, const SrcT*, size_t) { + static_assert(always_false::v, + "unspecialized strcpy_t should never be called"); +} + +template <> +inline void strncpy_t(char* dest, const char* src, size_t count) { + strncpy(dest, src, count); +} + +template <> +inline void strncpy_t(char* dest, const char16_t* src, size_t count) { + encode_sjis(dest, src, count); +} + +template <> +inline void strncpy_t(char16_t* dest, const char* src, size_t count) { + decode_sjis(dest, src, count); +} + +template <> +inline void strncpy_t(char16_t* dest, const char16_t* src, size_t count) { + char16ncpy(dest, src, count); +} + + + 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); @@ -29,6 +64,7 @@ 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) { @@ -58,6 +94,8 @@ size_t add_color_inplace(T* a, size_t max_chars) { *(d++) = '%'; } else if (*a == 'n') { *(d++) = '#'; + } else if (*a == '\0') { + break; } else { *(d++) = *a; } @@ -70,3 +108,32 @@ size_t add_color_inplace(T* a, size_t max_chars) { return d - orig_d; } + +template +void add_color(StringWriter& w, const T* src, size_t max_input_chars = SIZE_T_MAX) { + for (size_t x = 0; (x < max_input_chars) && *src; x++) { + if (*src == '$') { + w.put('\t'); + } else if (*src == '#') { + w.put('\n'); + } else if (*src == '%') { + src++; + x++; + if (*src == 's') { + w.put('$'); + } else if (*src == '%') { + w.put('%'); + } else if (*src == 'n') { + w.put('#'); + } else if (*src == '\0') { + break; + } else { + w.put(*src); + } + } else { + w.put(*src); + } + src++; + } + w.put(0); +}