diff --git a/src/Client.hh b/src/Client.hh index 8de04dbd..f61595c2 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -41,19 +41,23 @@ struct Client { IN_INFORMATION_MENU = 0x0080, // Client is at the welcome message (login server only) AT_WELCOME_MESSAGE = 0x0100, - // Client disconnect if it receives B2 (send_function_call) + // Client disconnects if it receives B2 (send_function_call) DOES_NOT_SUPPORT_SEND_FUNCTION_CALL = 0x0200, // Client has already received a 97 (enable saves) command, so don't show // the programs menu anymore SAVE_ENABLED = 0x0400, + // Client requires doubly-encrypted code section in send_function_call + ENCRYPTED_SEND_FUNCTION_CALL = 0x0800, // TODO: Do DCv1 and PC support send_function_call? Here we assume they don't DEFAULT_V1 = DCV1 | NO_MESSAGE_BOX_CLOSE_CONFIRMATION | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, DEFAULT_V2_DC = NO_MESSAGE_BOX_CLOSE_CONFIRMATION, DEFAULT_V2_PC = NO_MESSAGE_BOX_CLOSE_CONFIRMATION | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, DEFAULT_V3_GC = 0x0000, - DEFAULT_V3_GC_PLUS = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, - DEFAULT_V3_GC_EP3 = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | EPISODE_3 | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, + DEFAULT_V3_GC_PLUS = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | ENCRYPTED_SEND_FUNCTION_CALL, + DEFAULT_V3_GC_PLUS_NO_SFC = DEFAULT_V3_GC_PLUS | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, + DEFAULT_V3_GC_EP3 = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | EPISODE_3 | ENCRYPTED_SEND_FUNCTION_CALL, + DEFAULT_V3_GC_EP3_NO_SFC = DEFAULT_V3_GC_EP3 | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL, DEFAULT_V4_BB = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | NO_MESSAGE_BOX_CLOSE_CONFIRMATION | SAVE_ENABLED, }; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 01262d48..bbed96a1 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1383,12 +1383,17 @@ struct S_ConfirmUpdateQuestStatistics_AB { // B2 (S->C): Execute code and/or checksum memory // Client will respond with a B3 command with the same header.flag value as was // sent in the B2. -// This command doesn't work on PSO Plus (v1.2) or Episode 3. Sega presumably -// removed it after taking heat from Nintendo about enabling homebrew on the -// GameCube. On PSO PC, the code section (if included in the B2 command) is -// parsed and relocated, but is not actually executed, so the return_value field -// in the resulting B3 command is always 0. The checksum functionality does work -// on PSO PC, just like the other versions. +// On PSO PC, the code section (if included in the B2 command) is parsed and +// relocated, but is not actually executed, so the return_value field in the +// resulting B3 command is always 0. The checksum functionality does work on PSO +// PC, just like the other versions. +// This command doesn't work on the later JP PSO Plus (v1.5?), US PSO Plus +// (v1.2), or US Episode 3. Sega presumably removed it after taking heat from +// Nintendo about enabling homebrew on the GameCube. On the earlier JP PSO Plus +// (v1.4) and JP Episode 3, this command is implemented as described here, with +// some additional compression and encryption steps added, similarly to how +// download quests are encoded. See send_function_call in SendCommands.cc for +// more details on how this works. struct S_ExecuteCode_B2 { // If code_size == 0, no code is executed, but checksumming may still occur. diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index 6ab945e2..3280249a 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -40,9 +40,10 @@ public: virtual void encrypt(void* data, size_t size, bool advance = true); + uint32_t next(bool advance = true); + protected: void update_stream(); - uint32_t next(bool advance = true); uint32_t stream[PC_STREAM_LENGTH + 1]; uint8_t offset; @@ -54,9 +55,10 @@ public: virtual void encrypt(void* data, size_t size, bool advance = true); + uint32_t next(bool advance = true); + protected: void update_stream(); - uint32_t next(bool advance = true); uint32_t stream[GC_STREAM_LENGTH]; uint16_t offset; diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 2f8ced46..af3568ca 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -13,6 +13,7 @@ #include "PSOProtocol.hh" #include "CommandFormats.hh" +#include "Compression.hh" #include "FileContentsCache.hh" #include "Text.hh" @@ -225,8 +226,8 @@ void send_function_call( if (c->version != GameVersion::GC) { throw logic_error("cannot send function calls to non-GameCube clients"); } - if (c->flags & Client::Flag::EPISODE_3) { - throw logic_error("cannot send function calls to Episode 3 clients"); + if (c->flags & Client::Flag::DOES_NOT_SUPPORT_SEND_FUNCTION_CALL) { + throw logic_error("client does not support function calls"); } string data; @@ -236,6 +237,29 @@ void send_function_call( index = code->index; } + if (c->flags & Client::Flag::ENCRYPTED_SEND_FUNCTION_CALL) { + uint32_t key = random_object(); + + StringWriter w; + w.put_u32b(data.size()); + w.put_u32b(key); + + // Round size up to a multiple of 4 for encryption + data.resize(data.size() + 3 & ~3); + + // For this format, the code section is decrypted without byteswapping on + // the client (GameCube), which is big-endian, so we have to treat the data + // the same way here (hence we can't just use crypt.encrypt). + data = prs_compress(data); + StringReader compressed_r(data); + PSOPCEncryption crypt(key); + while (!compressed_r.eof()) { + w.put_u32b(compressed_r.get_u32b() ^ crypt.next()); + } + + data = move(w.str()); + } + S_ExecuteCode_B2 header = {data.size(), checksum_addr, checksum_size}; StringWriter w; diff --git a/src/Version.cc b/src/Version.cc index 9569aaf8..fb891d87 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -33,16 +33,20 @@ uint16_t flags_for_version(GameVersion version, uint8_t sub_version) { case 0x33: // PSO Ep1&2 EU50HZ case 0x34: // PSO Ep1&2 JP11 return Client::Flag::DEFAULT_V3_GC; + // TODO: Which of these is the first version of JP PSO Plus? That version + // supports encrypted send_function_call; we should set the appropriate flag + // here. case 0x32: // PSO Ep1&2 US12, JP12 case 0x35: // PSO Ep1&2 US12, JP12 case 0x36: // PSO Ep1&2 US12, JP12 case 0x39: // PSO Ep1&2 US12, JP12 - return Client::Flag::DEFAULT_V3_GC_PLUS; + return Client::Flag::DEFAULT_V3_GC_PLUS_NO_SFC; case 0x40: // PSO Ep3 trial case 0x41: // PSO Ep3 US - case 0x42: // PSO Ep3 JP case 0x43: // PSO Ep3 UK - return Client::Flag::DEFAULT_V3_GC_EP3; + return Client::Flag::DEFAULT_V3_GC_EP3_NO_SFC; + case 0x42: // PSO Ep3 JP + return Client::Flag::DEFAULT_V3_GC_PLUS; } return 0; }