add xbox support

This commit is contained in:
Martin Michelsen
2023-11-06 23:06:16 -08:00
parent 4b1f5420f2
commit 71cfced5ee
337 changed files with 2315 additions and 403 deletions
+1 -1
View File
@@ -134,7 +134,7 @@ foreach(LogTestCase IN ITEMS ${LogTestCases})
add_test( add_test(
NAME ${LogTestCase} NAME ${LogTestCase}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_BINARY_DIR}/newserv replay-log ${LogTestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-basic-credentials) COMMAND ${CMAKE_BINARY_DIR}/newserv replay-log ${LogTestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json)
endforeach() endforeach()
file(GLOB ScriptTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.sh) file(GLOB ScriptTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.sh)
+4 -1
View File
@@ -67,7 +67,7 @@ newserv supports several versions of PSO. Specifically:
| GC Ep1&2 Plus | Yes | Yes | Yes | Yes | | GC Ep1&2 Plus | Yes | Yes | Yes | Yes |
| GC Ep3 Trial | Yes | Yes | Partial (4) | Yes | | GC Ep3 Trial | Yes | Yes | Partial (4) | Yes |
| GC Ep3 | Yes | Yes | Yes | Yes | | GC Ep3 | Yes | Yes | Yes | Yes |
| XBOX Ep1&2 | Untested (1) | Untested (1) | Untested (1) | Untested (1) | | Xbox Ep1&2 | Yes | Yes | Yes | Partial (5) |
| BB (vanilla) | Yes | Yes | Yes (2) | Yes | | BB (vanilla) | Yes | Yes | Yes (2) | Yes |
| BB (Tethealla) | Yes | Yes | Yes (2) | Yes | | BB (Tethealla) | Yes | Yes | Yes (2) | Yes |
@@ -76,6 +76,7 @@ newserv supports several versions of PSO. Specifically:
2. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.* 2. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.*
3. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't handle.* 3. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't handle.*
4. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.* 4. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.*
5. *PSO Xbox sessions can be proxied to Sylverant. On Schtserv, Xbox proxy sessions can connect, but Schtserv does not recognize them as Xbox sessions when they join the lobby, so the session fails at that point.*
## Setup ## Setup
@@ -118,6 +119,8 @@ On .dat files, the `LANGUAGE` token may be omitted. If it's present, then that .
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc-e.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the filenames contain `-ret`. For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc-e.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the filenames contain `-ret`.
The GameCube and Xbox quest formats are very similar, but newserv treats them as different. If you want to use the same quest file for GameCube and Xbox clients, you can make one a symbolic link to the other.
The type identifiers (`b`, `c`, `d`, `e`, or `q`) and categories are configurable. See QuestCategories in config.example.json for more information on how to make new categories or edit the existing categories. The type identifiers (`b`, `c`, `d`, `e`, or `q`) and categories are configurable. See QuestCategories in config.example.json for more information on how to make new categories or edit the existing categories.
There are multiple PSO quest formats out there; newserv supports all of them. It can also decode any known format to standard .bin/.dat format. Specifically: There are multiple PSO quest formats out there; newserv supports all of them. It can also decode any known format to standard .bin/.dat format. Specifically:
+4 -10
View File
@@ -31,16 +31,10 @@
## PSO XBOX ## PSO XBOX
- Test the following things: - Fix receiving Guild Cards from non-Xbox players
- Login - Make the Guild Card description field in SavedPlayerDataBB longer to accommodate XB descriptions (0x200 bytes)
- Lobby interactions with GC players - Implement and test voice chat
- Symbol chats: any byteswapping required? - Fix whatever causes Schtserv to not recognize proxy connections as Xbox
- Word Select mapping to other consoles (is it the same as GC? probably?)
- Guild card send/receive, simple mail (+ auto reply), info board
- Crossplay in games
- Check mag correctness in various situations, including on the ground at join time
- Voice chat (both in games and lobbies)
- Quest loading (symlink all XB quests to GC quests by default, make sure they load properly)
## PSOBB ## PSOBB
+3 -5
View File
@@ -383,9 +383,7 @@ static void server_command_exit(shared_ptr<Client> c, const std::string&) {
send_message_box(c, ""); send_message_box(c, "");
} }
const auto& port_name = version_to_login_port_name.at(static_cast<size_t>(c->version())); send_client_to_login_server(c);
auto s = c->require_server_state();
send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at(port_name)->port);
} }
} }
@@ -431,7 +429,7 @@ static void proxy_command_get_player_card(shared_ptr<ProxyServer::LinkedSession>
bool any_card_sent = false; bool any_card_sent = false;
for (const auto& p : ses->lobby_players) { for (const auto& p : ses->lobby_players) {
if (!p.name.empty() && args == p.name) { if (!p.name.empty() && args == p.name) {
send_guild_card(ses->client_channel, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class); send_guild_card(ses->client_channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class);
any_card_sent = true; any_card_sent = true;
} }
} }
@@ -441,7 +439,7 @@ static void proxy_command_get_player_card(shared_ptr<ProxyServer::LinkedSession>
size_t index = stoull(args, nullptr, 0); size_t index = stoull(args, nullptr, 0);
const auto& p = ses->lobby_players.at(index); const auto& p = ses->lobby_players.at(index);
if (!p.name.empty()) { if (!p.name.empty()) {
send_guild_card(ses->client_channel, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class); send_guild_card(ses->client_channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class);
} }
} catch (const exception& e) { } catch (const exception& e) {
send_text_message_printf(ses->client_channel, "Error: %s", e.what()); send_text_message_printf(ses->client_channel, "Error: %s", e.what());
+10 -1
View File
@@ -35,6 +35,8 @@ void Client::Config::set_flags_for_version(GameVersion version, int64_t sub_vers
case GameVersion::GC: case GameVersion::GC:
break; break;
case GameVersion::XB: case GameVersion::XB:
// TODO: Do all versions of XB need this flag? US does, at least.
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
break; break;
case GameVersion::PC: case GameVersion::PC:
@@ -81,7 +83,7 @@ void Client::Config::set_flags_for_version(GameVersion version, int64_t sub_vers
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
break; break;
case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 JP v1.02, at least one version of PSO XB case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 JP v1.02, at least one version of PSO XB
case 0x31: // GC Ep1&2 US v1.00, GC US v1.01, GC EU v1.00, GC JP v1.00 case 0x31: // GC Ep1&2 US v1.00, GC US v1.01, GC EU v1.00, GC JP v1.00, XB US
case 0x34: // GC Ep1&2 JP v1.03 case 0x34: // GC Ep1&2 JP v1.03
// In the case of GC Trial Edition, the IS_GC_TRIAL_EDITION flag is // In the case of GC Trial Edition, the IS_GC_TRIAL_EDITION flag is
// already set when we get here (because the client has used V2 encryption // already set when we get here (because the client has used V2 encryption
@@ -145,6 +147,7 @@ Client::Client(
should_send_to_lobby_server(false), should_send_to_lobby_server(false),
should_send_to_proxy_server(false), should_send_to_proxy_server(false),
bb_connection_phase(0xFF), bb_connection_phase(0xFF),
sub_version(-1),
x(0.0f), x(0.0f),
z(0.0f), z(0.0f),
area(0), area(0),
@@ -307,3 +310,9 @@ void Client::idle_timeout() {
this->log.info("Server is deleted; cannot disconnect client"); this->log.info("Server is deleted; cannot disconnect client");
} }
} }
void Client::suspend_timeouts() {
event_del(this->send_ping_event.get());
event_del(this->idle_timeout_event.get());
this->log.info("Timeouts suspended");
}
+5
View File
@@ -163,6 +163,8 @@ struct Client : public std::enable_shared_from_this<Client> {
bool should_send_to_lobby_server; bool should_send_to_lobby_server;
bool should_send_to_proxy_server; bool should_send_to_proxy_server;
std::unordered_map<std::string, std::function<void()>> disconnect_hooks; std::unordered_map<std::string, std::function<void()>> disconnect_hooks;
std::shared_ptr<XBNetworkLocation> xb_netloc;
parray<le_uint32_t, 3> xb_9E_unknown_a1a;
uint8_t bb_connection_phase; uint8_t bb_connection_phase;
// Patch server // Patch server
@@ -170,6 +172,7 @@ struct Client : public std::enable_shared_from_this<Client> {
// Lobby/positioning // Lobby/positioning
Config config; Config config;
int32_t sub_version;
float x; float x;
float z; float z;
uint32_t area; uint32_t area;
@@ -229,4 +232,6 @@ struct Client : public std::enable_shared_from_this<Client> {
void send_ping(); void send_ping();
static void dispatch_idle_timeout(evutil_socket_t, short, void* ctx); static void dispatch_idle_timeout(evutil_socket_t, short, void* ctx);
void idle_timeout(); void idle_timeout();
void suspend_timeouts();
}; };
+47 -24
View File
@@ -38,9 +38,9 @@
// DC = Both DCv1 and DCv2 // DC = Both DCv1 and DCv2
// PC = PSO PC (v2) // PC = PSO PC (v2)
// GC = PSO GC Episodes 1&2 and/or Episode 3 // GC = PSO GC Episodes 1&2 and/or Episode 3
// XB = PSO XBOX Episodes 1&2 // XB = PSO Xbox Episodes 1&2
// BB = PSO Blue Burst // BB = PSO Blue Burst
// V3 = PSO GC and PSO XBOX (these versions are similar and share many formats) // V3 = PSO GC and PSO Xbox (these versions are similar and share many formats)
// For variable-length commands, generally a zero-length array is included on // For variable-length commands, generally a zero-length array is included on
// the end of the struct if the command is received by newserv, and is omitted // the end of the struct if the command is received by newserv, and is omitted
@@ -988,7 +988,9 @@ struct S_OpenFile_PC_GC_44_A6 {
// those extra bytes are unused, and the client does not fail if they're // those extra bytes are unused, and the client does not fail if they're
// omitted. // omitted.
struct S_OpenFile_XB_44_A6 : S_OpenFile_PC_GC_44_A6 { struct S_OpenFile_XB_44_A6 : S_OpenFile_PC_GC_44_A6 {
parray<uint8_t, 0x18> unused2; pstring<TextEncoding::ASCII, 0x10> xb_filename;
le_uint32_t content_meta;
parray<uint8_t, 4> unused2;
} __packed__; } __packed__;
struct S_OpenFile_BB_44_A6 { struct S_OpenFile_BB_44_A6 {
@@ -1858,21 +1860,22 @@ struct C_Register_BB_9C {
// by its menu ID and item ID. // by its menu ID and item ID.
struct C_Login_DC_PC_GC_9D { struct C_Login_DC_PC_GC_9D {
le_uint32_t player_tag = 0x00010000; // 0x00010000 if guild card is set (via 04) /* 04 */ le_uint32_t player_tag = 0x00010000; // 0x00010000 if guild card is set (via 04)
le_uint32_t guild_card_number = 0; // 0xFFFFFFFF if not set /* 08 */ le_uint32_t guild_card_number = 0; // 0xFFFFFFFF if not set
le_uint32_t unused1 = 0; /* 0C */ le_uint32_t unused1 = 0;
le_uint32_t unused2 = 0; /* 10 */ le_uint32_t unused2 = 0;
le_uint32_t sub_version = 0; /* 14 */ le_uint32_t sub_version = 0;
uint8_t is_extended = 0; // If 1, structure has extended format /* 18 */ uint8_t is_extended = 0; // If 1, structure has extended format
uint8_t language = 0; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES /* 19 */ uint8_t language = 0; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES
parray<uint8_t, 0x2> unused3; // Always zeroes /* 1A */ parray<uint8_t, 0x2> unused3; // Always zeroes
pstring<TextEncoding::ASCII, 0x10> v1_serial_number; /* 1C */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
pstring<TextEncoding::ASCII, 0x10> v1_access_key; /* 2C */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
pstring<TextEncoding::ASCII, 0x10> serial_number; // On XB, this is the XBL gamertag /* 3C */ pstring<TextEncoding::ASCII, 0x10> serial_number; // On XB, this is the XBL gamertag
pstring<TextEncoding::ASCII, 0x10> access_key; // On XB, this is the XBL user ID /* 4C */ pstring<TextEncoding::ASCII, 0x10> access_key; // On XB, this is the XBL user ID
pstring<TextEncoding::ASCII, 0x30> serial_number2; // On XB, this is the XBL gamertag /* 5C */ pstring<TextEncoding::ASCII, 0x30> serial_number2; // On XB, this is the XBL gamertag
pstring<TextEncoding::ASCII, 0x30> access_key2; // On XB, this is the XBL user ID /* 8C */ pstring<TextEncoding::ASCII, 0x30> access_key2; // On XB, this is the XBL user ID
pstring<TextEncoding::ASCII, 0x10> name; /* BC */ pstring<TextEncoding::ASCII, 0x10> name;
/* CC */
} __packed__; } __packed__;
struct C_LoginExtended_DC_GC_9D : C_Login_DC_PC_GC_9D { struct C_LoginExtended_DC_GC_9D : C_Login_DC_PC_GC_9D {
SC_MeetUserExtension<TextEncoding::MARKED> extension; SC_MeetUserExtension<TextEncoding::MARKED> extension;
@@ -1896,7 +1899,10 @@ struct C_LoginExtended_GC_9E : C_Login_GC_9E {
struct C_Login_XB_9E : C_Login_GC_9E { struct C_Login_XB_9E : C_Login_GC_9E {
XBNetworkLocation netloc; XBNetworkLocation netloc;
parray<le_uint32_t, 6> unknown_a1; parray<le_uint32_t, 3> unknown_a1a;
le_uint32_t xb_user_id_high = 0;
le_uint32_t xb_user_id_low = 0;
le_uint32_t unknown_a1b = 0;
} __packed__; } __packed__;
struct C_LoginExtended_XB_9E : C_Login_XB_9E { struct C_LoginExtended_XB_9E : C_Login_XB_9E {
SC_MeetUserExtension<TextEncoding::MARKED> extension; SC_MeetUserExtension<TextEncoding::MARKED> extension;
@@ -2480,6 +2486,10 @@ struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> {
// commands), except for map data requests. This differs from Sega's original // commands), except for map data requests. This differs from Sega's original
// implementation, which sent CA responses via 60 commands instead. // implementation, which sent CA responses via 60 commands instead.
// C9 (C->S): Change connection status (Xbox)
// header.flag specifies if the player's online status should be hidden; 1 means
// shown, 2 means hidden.
// CA (C->S): Server data request (Episode 3) // CA (C->S): Server data request (Episode 3)
// Internal name: SndCardServerData // Internal name: SndCardServerData
// The CA command format is the same as that of the 6xB3 commands, and the // The CA command format is the same as that of the 6xB3 commands, and the
@@ -3699,9 +3709,14 @@ struct G_SendGuildCard_PC_6x06 {
GuildCardPC guild_card; GuildCardPC guild_card;
} __packed__; } __packed__;
struct G_SendGuildCard_V3_6x06 { struct G_SendGuildCard_GC_6x06 {
G_UnusedHeader header; G_UnusedHeader header;
GuildCardV3 guild_card; GuildCardGC guild_card;
} __packed__;
struct G_SendGuildCard_XB_6x06 {
G_UnusedHeader header;
GuildCardXB guild_card;
} __packed__; } __packed__;
struct G_SendGuildCard_BB_6x06 { struct G_SendGuildCard_BB_6x06 {
@@ -4554,7 +4569,7 @@ struct G_SetQuestFlags_6x6F {
// Episode 3 does not send this command at all since the relevant data is sent // Episode 3 does not send this command at all since the relevant data is sent
// to the joining player in the 64 command instead. // to the joining player in the 64 command instead.
struct G_SyncPlayerDispAndInventory_DC_PC_V3_6x70 { struct G_SyncPlayerDispAndInventory_DC_PC_GC_6x70 {
// Offsets in this struct are relative to the overall command header // Offsets in this struct are relative to the overall command header
/* 0004 */ G_ExtendedHeader<G_UnusedHeader> header; /* 0004 */ G_ExtendedHeader<G_UnusedHeader> header;
/* 000C */ le_uint16_t client_id = 0; /* 000C */ le_uint16_t client_id = 0;
@@ -4589,6 +4604,14 @@ struct G_SyncPlayerDispAndInventory_DC_PC_V3_6x70 {
/* 0498 */ /* 0498 */
} __packed__; } __packed__;
struct G_SyncPlayerDispAndInventory_XB_6x70 : G_SyncPlayerDispAndInventory_DC_PC_GC_6x70 {
// Offsets in this struct are relative to the overall command header
/* 0498 */ le_uint32_t xb_user_id_high = 0;
/* 049C */ le_uint32_t xb_user_id_low = 0;
/* 04A0 */ le_uint32_t unknown_a16 = 0;
/* 04A4 */
} __packed__;
// 6x71: Unknown (used while loading into game) // 6x71: Unknown (used while loading into game)
struct G_Unknown_6x71 { struct G_Unknown_6x71 {
@@ -5130,7 +5153,7 @@ struct G_Unknown_6xB2 {
le_uint32_t unknown_a3 = 0; // PSO GC puts 0x00051720 (333600) here le_uint32_t unknown_a3 = 0; // PSO GC puts 0x00051720 (333600) here
} __packed__; } __packed__;
// 6xB3: Unknown (XBOX; voice chat) // 6xB3: Unknown (Xbox; voice chat)
// 6xB3: CARD battle server data request (Episode 3) // 6xB3: CARD battle server data request (Episode 3)
@@ -5176,7 +5199,7 @@ struct G_CardServerDataCommandHeader {
/* 10 */ /* 10 */
} __packed__; } __packed__;
// 6xB4: Unknown (XBOX; voice chat) // 6xB4: Unknown (Xbox; voice chat)
// 6xB4: CARD battle server response (Episode 3) - see 6xB3 above // 6xB4: CARD battle server response (Episode 3) - see 6xB3 above
// 6xB5: CARD battle client command (Episode 3) - see 6xB3 above // 6xB5: CARD battle client command (Episode 3) - see 6xB3 above
+42 -1
View File
@@ -6,7 +6,6 @@
#include <phosg/Time.hh> #include <phosg/Time.hh>
#include "License.hh" #include "License.hh"
#include "Loggers.hh"
using namespace std; using namespace std;
@@ -19,6 +18,9 @@ License::License(const JSON& json)
this->serial_number = json.get_int("SerialNumber"); this->serial_number = json.get_int("SerialNumber");
this->access_key = json.get_string("AccessKey", ""); this->access_key = json.get_string("AccessKey", "");
this->gc_password = json.get_string("GCPassword", ""); this->gc_password = json.get_string("GCPassword", "");
this->xb_gamertag = json.get_string("XBGamerTag", "");
this->xb_user_id = json.get_int("XBUserID", 0);
this->xb_account_id = json.get_int("XBAccountID", 0);
this->bb_username = json.get_string("BBUsername", ""); this->bb_username = json.get_string("BBUsername", "");
this->bb_password = json.get_string("BBPassword", ""); this->bb_password = json.get_string("BBPassword", "");
this->flags = json.get_int("Flags", 0); this->flags = json.get_int("Flags", 0);
@@ -32,6 +34,9 @@ JSON License::json() const {
{"SerialNumber", this->serial_number}, {"SerialNumber", this->serial_number},
{"AccessKey", this->access_key}, {"AccessKey", this->access_key},
{"GCPassword", this->gc_password}, {"GCPassword", this->gc_password},
{"XBGamerTag", this->xb_gamertag},
{"XBUserID", this->xb_user_id},
{"XBAccountID", this->xb_account_id},
{"BBUsername", this->bb_username}, {"BBUsername", this->bb_username},
{"BBPassword", this->bb_password}, {"BBPassword", this->bb_password},
{"Flags", this->flags}, {"Flags", this->flags},
@@ -62,6 +67,15 @@ string License::str() const {
if (!this->gc_password.empty()) { if (!this->gc_password.empty()) {
tokens.emplace_back("gc_password=" + this->gc_password); tokens.emplace_back("gc_password=" + this->gc_password);
} }
if (!this->xb_gamertag.empty()) {
tokens.emplace_back("xb_gamertag=" + this->xb_gamertag);
}
if (this->xb_user_id != 0) {
tokens.emplace_back(string_printf("xb_user_id=%016" PRIX64, this->xb_user_id));
}
if (this->xb_account_id != 0) {
tokens.emplace_back(string_printf("xb_account_id=%016" PRIX64, this->xb_account_id));
}
if (!this->bb_username.empty()) { if (!this->bb_username.empty()) {
tokens.emplace_back("bb_username=" + this->bb_username); tokens.emplace_back("bb_username=" + this->bb_username);
} }
@@ -156,6 +170,9 @@ void LicenseIndex::add(shared_ptr<License> l) {
if (!l->bb_username.empty()) { if (!l->bb_username.empty()) {
this->bb_username_to_license[l->bb_username] = l; this->bb_username_to_license[l->bb_username] = l;
} }
if (!l->xb_gamertag.empty()) {
this->xb_gamertag_to_license[l->xb_gamertag] = l;
}
} }
void LicenseIndex::remove(uint32_t serial_number) { void LicenseIndex::remove(uint32_t serial_number) {
@@ -164,6 +181,9 @@ void LicenseIndex::remove(uint32_t serial_number) {
if (!l->bb_username.empty()) { if (!l->bb_username.empty()) {
this->bb_username_to_license.erase(l->bb_username); this->bb_username_to_license.erase(l->bb_username);
} }
if (!l->xb_gamertag.empty()) {
this->xb_gamertag_to_license.erase(l->xb_gamertag);
}
} }
shared_ptr<License> LicenseIndex::verify_v1_v2(uint32_t serial_number, const string& access_key) const { shared_ptr<License> LicenseIndex::verify_v1_v2(uint32_t serial_number, const string& access_key) const {
@@ -223,6 +243,27 @@ shared_ptr<License> LicenseIndex::verify_gc(uint32_t serial_number, const string
} }
} }
shared_ptr<License> LicenseIndex::verify_xb(const string& gamertag, uint64_t user_id, uint64_t account_id) const {
if (user_id == 0 || account_id == 0) {
throw incorrect_access_key();
}
try {
auto& license = this->xb_gamertag_to_license.at(gamertag);
if (license->xb_user_id && (license->xb_user_id != user_id)) {
throw incorrect_access_key();
}
if (license->xb_account_id && (license->xb_account_id != account_id)) {
throw incorrect_access_key();
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
throw invalid_argument("user is banned");
}
return license;
} catch (const out_of_range&) {
throw missing_license();
}
}
shared_ptr<License> LicenseIndex::verify_bb(const string& username, const string& password) const { shared_ptr<License> LicenseIndex::verify_bb(const string& username, const string& password) const {
if (username.empty() || password.empty()) { if (username.empty() || password.empty()) {
throw no_username(); throw no_username();
+5
View File
@@ -33,6 +33,9 @@ struct License {
uint32_t serial_number = 0; uint32_t serial_number = 0;
std::string access_key; std::string access_key;
std::string gc_password; std::string gc_password;
std::string xb_gamertag;
uint64_t xb_user_id = 0;
uint64_t xb_account_id = 0;
std::string bb_username; std::string bb_username;
std::string bb_password; std::string bb_password;
@@ -84,9 +87,11 @@ public:
std::shared_ptr<License> verify_v1_v2(uint32_t serial_number, const std::string& access_key) const; std::shared_ptr<License> verify_v1_v2(uint32_t serial_number, const std::string& access_key) const;
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key) const; std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key) const;
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key, const std::string& password) const; std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key, const std::string& password) const;
std::shared_ptr<License> verify_xb(const std::string& gamertag, uint64_t user_id, uint64_t account_id) const;
std::shared_ptr<License> verify_bb(const std::string& username, const std::string& password) const; std::shared_ptr<License> verify_bb(const std::string& username, const std::string& password) const;
protected: protected:
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license; std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
std::unordered_map<std::string, std::shared_ptr<License>> xb_gamertag_to_license;
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license; std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
}; };
+3 -2
View File
@@ -257,9 +257,10 @@ void XBNetworkLocation::clear() {
this->external_ipv4_address = 0; this->external_ipv4_address = 0;
this->port = 0; this->port = 0;
this->mac_address.clear(0); this->mac_address.clear(0);
this->unknown_a1.clear(0); this->unknown_a1 = 0;
this->unknown_a2 = 0;
this->account_id = 0; this->account_id = 0;
this->unknown_a2.clear(0); this->unknown_a3.clear(0);
} }
void PlayerLobbyDataXB::clear() { void PlayerLobbyDataXB::clear() {
+19 -6
View File
@@ -210,9 +210,7 @@ struct GuildCardPC {
/* F0 */ /* F0 */
} __attribute__((packed)); } __attribute__((packed));
// TODO: Is this the same for XB as it is for GC? (This struct is based on the struct GuildCardGC {
// GC format)
struct GuildCardV3 {
/* 00 */ le_uint32_t player_tag = 0; /* 00 */ le_uint32_t player_tag = 0;
/* 04 */ le_uint32_t guild_card_number = 0; /* 04 */ le_uint32_t guild_card_number = 0;
/* 08 */ pstring<TextEncoding::ASCII, 0x18> name; /* 08 */ pstring<TextEncoding::ASCII, 0x18> name;
@@ -224,6 +222,20 @@ struct GuildCardV3 {
/* 90 */ /* 90 */
} __attribute__((packed)); } __attribute__((packed));
struct GuildCardXB {
/* 0000 */ le_uint32_t player_tag = 0;
/* 0004 */ le_uint32_t guild_card_number = 0;
/* 0008 */ le_uint32_t xb_user_id_high = 0;
/* 000C */ le_uint32_t xb_user_id_low = 0;
/* 0010 */ pstring<TextEncoding::ASCII, 0x18> name;
/* 0028 */ pstring<TextEncoding::MARKED, 0x200> description;
/* 0228 */ uint8_t present = 0;
/* 0229 */ uint8_t language = 0;
/* 022A */ uint8_t section_id = 0;
/* 022B */ uint8_t char_class = 0;
/* 022C */
} __attribute__((packed));
struct GuildCardBB { struct GuildCardBB {
/* 0000 */ le_uint32_t guild_card_number = 0; /* 0000 */ le_uint32_t guild_card_number = 0;
/* 0004 */ pstring<TextEncoding::UTF16, 0x18> name; /* 0004 */ pstring<TextEncoding::UTF16, 0x18> name;
@@ -266,11 +278,12 @@ struct PlayerLobbyDataDCGC {
struct XBNetworkLocation { struct XBNetworkLocation {
le_uint32_t internal_ipv4_address = 0x0A0A0A0A; le_uint32_t internal_ipv4_address = 0x0A0A0A0A;
le_uint32_t external_ipv4_address = 0x23232323; le_uint32_t external_ipv4_address = 0x23232323;
le_uint16_t port = 9100; le_uint16_t port = 9500;
parray<uint8_t, 6> mac_address = 0x77; parray<uint8_t, 6> mac_address = 0x77;
parray<le_uint32_t, 2> unknown_a1; le_uint32_t unknown_a1;
le_uint32_t unknown_a2;
le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF; le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
parray<le_uint32_t, 4> unknown_a2; parray<le_uint32_t, 4> unknown_a3;
void clear(); void clear();
} __attribute__((packed)); } __attribute__((packed));
+148 -112
View File
@@ -227,12 +227,11 @@ static HandlerResult S_V123P_02_17(
if (!ses->license) { if (!ses->license) {
ses->log.info("No license in linked session"); ses->log.info("No license in linked session");
// We have to forward the command before setting up encryption, so the // We have to forward the command BEFORE setting up encryption, so the
// client will be able to understand it. // client will be able to understand what we sent.
forward_command(ses, false, command, flag, data); forward_command(ses, false, command, flag, data);
if ((ses->version() == GameVersion::GC) || if ((ses->version() == GameVersion::GC) || (ses->version() == GameVersion::XB)) {
(ses->version() == GameVersion::XB)) {
ses->server_channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key)); ses->server_channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key));
ses->server_channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key)); ses->server_channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key));
ses->client_channel.crypt_in.reset(new PSOV3Encryption(cmd.client_key)); ses->client_channel.crypt_in.reset(new PSOV3Encryption(cmd.client_key));
@@ -270,130 +269,164 @@ static HandlerResult S_V123P_02_17(
// because it believes it already did (when it was in an unlinked session, or // because it believes it already did (when it was in an unlinked session, or
// in the patch server case, during the current session due to a hidden // in the patch server case, during the current session due to a hidden
// redirect). // redirect).
if (ses->version() == GameVersion::PATCH) { switch (ses->version()) {
ses->server_channel.send(0x02); case GameVersion::PATCH:
return HandlerResult::Type::SUPPRESS; ses->server_channel.send(0x02);
return HandlerResult::Type::SUPPRESS;
} else if ((ses->version() == GameVersion::DC) || (ses->version() == GameVersion::PC)) { case GameVersion::DC:
if (ses->config.check_flag(Client::Flag::IS_DC_V1)) { case GameVersion::PC:
if (command == 0x17) { if (ses->config.check_flag(Client::Flag::IS_DC_V1)) {
C_LoginV1_DC_PC_V3_90 cmd; if (command == 0x17) {
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number)); C_LoginV1_DC_PC_V3_90 cmd;
cmd.access_key.encode(ses->license->access_key); cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
cmd.access_key.clear_after(8); cmd.access_key.encode(ses->license->access_key);
ses->server_channel.send(0x90, 0x00, &cmd, sizeof(cmd)); cmd.access_key.clear_after(8);
return HandlerResult::Type::SUPPRESS; ses->server_channel.send(0x90, 0x00, &cmd, sizeof(cmd));
} else { return HandlerResult::Type::SUPPRESS;
C_LoginV1_DC_93 cmd;
if (ses->remote_guild_card_number < 0) {
cmd.player_tag = 0xFFFF0000;
cmd.guild_card_number = 0xFFFFFFFF;
} else { } else {
cmd.player_tag = 0x00010000; C_LoginV1_DC_93 cmd;
cmd.guild_card_number = ses->remote_guild_card_number; if (ses->remote_guild_card_number < 0) {
cmd.player_tag = 0xFFFF0000;
cmd.guild_card_number = 0xFFFFFFFF;
} else {
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.unknown_a1 = 0;
cmd.unknown_a2 = 0;
cmd.sub_version = ses->sub_version;
cmd.is_extended = 0;
cmd.language = ses->language();
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
cmd.access_key.encode(ses->license->access_key);
cmd.access_key.clear_after(8);
cmd.hardware_id.encode(ses->hardware_id);
cmd.name.encode(ses->character_name);
ses->server_channel.send(0x93, 0x00, &cmd, sizeof(cmd));
return HandlerResult::Type::SUPPRESS;
}
} else { // DCv2 or PC
if (command == 0x17) {
C_Login_DC_PC_V3_9A cmd;
if (ses->remote_guild_card_number < 0) {
cmd.player_tag = 0xFFFF0000;
cmd.guild_card_number = 0xFFFFFFFF;
} else {
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.sub_version = ses->sub_version;
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
cmd.access_key.encode(ses->license->access_key);
cmd.access_key.clear_after(8);
cmd.serial_number2 = cmd.serial_number;
cmd.access_key2 = cmd.access_key;
// TODO: We probably should set email_address, but we currently don't
// keep that value anywhere in the session object, nor is it saved in
// the License object.
ses->server_channel.send(0x9A, 0x00, &cmd, sizeof(cmd));
return HandlerResult::Type::SUPPRESS;
} else {
C_Login_DC_PC_GC_9D cmd;
if (ses->remote_guild_card_number < 0) {
cmd.player_tag = 0xFFFF0000;
cmd.guild_card_number = 0xFFFFFFFF;
} else {
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.sub_version = ses->sub_version;
cmd.is_extended = 0;
cmd.language = ses->language();
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
cmd.access_key.encode(ses->license->access_key);
cmd.access_key.clear_after(8);
cmd.serial_number2 = cmd.serial_number;
cmd.access_key2 = cmd.access_key;
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
cmd.name.encode(" ", ses->language());
} else {
cmd.name.encode(ses->character_name);
}
ses->server_channel.send(0x9D, 0x00, &cmd, sizeof(cmd));
return HandlerResult::Type::SUPPRESS;
} }
cmd.unknown_a1 = 0;
cmd.unknown_a2 = 0;
cmd.sub_version = ses->sub_version;
cmd.is_extended = 0;
cmd.language = ses->language();
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
cmd.access_key.encode(ses->license->access_key);
cmd.access_key.clear_after(8);
cmd.hardware_id.encode(ses->hardware_id);
cmd.name.encode(ses->character_name);
ses->server_channel.send(0x93, 0x00, &cmd, sizeof(cmd));
return HandlerResult::Type::SUPPRESS;
} }
} else { // DCv2 or PC throw logic_error("DC/PC init command not handled");
case GameVersion::GC:
if (command == 0x17) { if (command == 0x17) {
C_Login_DC_PC_V3_9A cmd; C_VerifyLicense_V3_DB cmd;
if (ses->remote_guild_card_number < 0) {
cmd.player_tag = 0xFFFF0000;
cmd.guild_card_number = 0xFFFFFFFF;
} else {
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.sub_version = ses->sub_version;
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number)); cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
cmd.access_key.encode(ses->license->access_key); cmd.access_key.encode(ses->license->access_key);
cmd.access_key.clear_after(8); cmd.sub_version = ses->sub_version;
cmd.serial_number2 = cmd.serial_number; cmd.serial_number2 = cmd.serial_number;
cmd.access_key2 = cmd.access_key; cmd.access_key2 = cmd.access_key;
// TODO: We probably should set email_address, but we currently don't cmd.password.encode(ses->license->gc_password);
// keep that value anywhere in the session object, nor is it saved in ses->server_channel.send(0xDB, 0x00, &cmd, sizeof(cmd));
// the License object.
ses->server_channel.send(0x9A, 0x00, &cmd, sizeof(cmd));
return HandlerResult::Type::SUPPRESS; return HandlerResult::Type::SUPPRESS;
} else {
C_Login_DC_PC_GC_9D cmd; } else if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN)) {
if (ses->remote_guild_card_number < 0) { uint32_t guild_card_number;
cmd.player_tag = 0xFFFF0000; if (ses->remote_guild_card_number >= 0) {
cmd.guild_card_number = 0xFFFFFFFF; guild_card_number = ses->remote_guild_card_number;
log_info("Using Guild Card number %" PRIu32 " from session", guild_card_number);
} else { } else {
cmd.player_tag = 0x00010000; guild_card_number = random_object<uint32_t>();
cmd.guild_card_number = ses->remote_guild_card_number; log_info("Using Guild Card number %" PRIu32 " from random generator", guild_card_number);
} }
uint32_t fake_serial_number = random_object<uint32_t>() & 0x7FFFFFFF;
uint64_t fake_access_key = random_object<uint64_t>();
string fake_access_key_str = string_printf("00000000000%" PRIu64, fake_access_key);
if (fake_access_key_str.size() > 12) {
fake_access_key_str = fake_access_key_str.substr(fake_access_key_str.size() - 12);
}
C_LoginExtended_GC_9E cmd;
cmd.player_tag = 0x00010000;
cmd.guild_card_number = guild_card_number;
cmd.unused1 = 0; cmd.unused1 = 0;
cmd.unused2 = 0; cmd.unused2 = 0;
cmd.sub_version = ses->sub_version; cmd.sub_version = ses->sub_version;
cmd.is_extended = 0; cmd.is_extended = 0;
cmd.language = ses->language(); cmd.language = ses->language();
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number)); cmd.serial_number.encode(string_printf("%08" PRIX32, fake_serial_number));
cmd.access_key.encode(ses->license->access_key); cmd.access_key.encode(fake_access_key_str);
cmd.access_key.clear_after(8);
cmd.serial_number2 = cmd.serial_number; cmd.serial_number2 = cmd.serial_number;
cmd.access_key2 = cmd.access_key; cmd.access_key2 = cmd.access_key;
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) { if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
cmd.name.encode(" ", ses->language()); cmd.name.encode(" ", ses->language());
} else { } else {
cmd.name.encode(ses->character_name); cmd.name.encode(ses->character_name, ses->language());
} }
ses->server_channel.send(0x9D, 0x00, &cmd, sizeof(cmd)); cmd.client_config = ses->remote_client_config_data;
ses->server_channel.send(0x9E, 0x01, &cmd, sizeof(C_Login_GC_9E));
return HandlerResult::Type::SUPPRESS; return HandlerResult::Type::SUPPRESS;
}
}
} else if (ses->version() == GameVersion::GC) {
if (command == 0x17) {
C_VerifyLicense_V3_DB cmd;
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
cmd.access_key.encode(ses->license->access_key);
cmd.sub_version = ses->sub_version;
cmd.serial_number2 = cmd.serial_number;
cmd.access_key2 = cmd.access_key;
cmd.password.encode(ses->license->gc_password);
ses->server_channel.send(0xDB, 0x00, &cmd, sizeof(cmd));
return HandlerResult::Type::SUPPRESS;
} else if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN)) {
uint32_t guild_card_number;
if (ses->remote_guild_card_number >= 0) {
guild_card_number = ses->remote_guild_card_number;
log_info("Using Guild Card number %" PRIu32 " from session", guild_card_number);
} else { } else {
guild_card_number = random_object<uint32_t>(); // For command 02, send the same as if we had received 9A from the server
log_info("Using Guild Card number %" PRIu32 " from random generator", guild_card_number); return S_G_9A(ses, command, flag, data);
} }
throw logic_error("GC init command not handled");
uint32_t fake_serial_number = random_object<uint32_t>() & 0x7FFFFFFF; case GameVersion::XB: {
uint64_t fake_access_key = random_object<uint64_t>(); C_LoginExtended_XB_9E cmd;
string fake_access_key_str = string_printf("00000000000%" PRIu64, fake_access_key); if (ses->remote_guild_card_number < 0) {
if (fake_access_key_str.size() > 12) { cmd.player_tag = 0xFFFF0000;
fake_access_key_str = fake_access_key_str.substr(fake_access_key_str.size() - 12); cmd.guild_card_number = 0xFFFFFFFF;
} else {
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
} }
C_LoginExtended_GC_9E cmd;
cmd.player_tag = 0x00010000;
cmd.guild_card_number = guild_card_number;
cmd.unused1 = 0; cmd.unused1 = 0;
cmd.unused2 = 0; cmd.unused2 = 0;
cmd.sub_version = ses->sub_version; cmd.sub_version = ses->sub_version;
cmd.is_extended = 0; cmd.is_extended = (ses->remote_guild_card_number < 0) ? 1 : 0;
cmd.language = ses->language(); cmd.language = ses->language();
cmd.serial_number.encode(string_printf("%08" PRIX32, fake_serial_number)); cmd.serial_number.encode(ses->license->xb_gamertag);
cmd.access_key.encode(fake_access_key_str); cmd.access_key.encode(string_printf("%016" PRIX64, ses->license->xb_user_id));
cmd.serial_number2 = cmd.serial_number; cmd.serial_number2 = cmd.serial_number;
cmd.access_key2 = cmd.access_key; cmd.access_key2 = cmd.access_key;
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) { if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
@@ -402,19 +435,21 @@ static HandlerResult S_V123P_02_17(
cmd.name.encode(ses->character_name, ses->language()); cmd.name.encode(ses->character_name, ses->language());
} }
cmd.client_config = ses->remote_client_config_data; cmd.client_config = ses->remote_client_config_data;
ses->server_channel.send(0x9E, 0x01, &cmd, sizeof(C_Login_GC_9E)); if (ses->wrapped_client && ses->wrapped_client->xb_netloc) {
cmd.netloc = *ses->wrapped_client->xb_netloc;
cmd.unknown_a1a = ses->wrapped_client->xb_9E_unknown_a1a;
} else {
cmd.netloc.account_id = ses->license->xb_account_id;
}
cmd.xb_user_id_high = (ses->license->xb_user_id >> 32) & 0xFFFFFFFF;
cmd.xb_user_id_low = ses->license->xb_user_id & 0xFFFFFFFF;
ses->server_channel.send(
0x9E, 0x01, &cmd,
cmd.is_extended ? sizeof(C_LoginExtended_XB_9E) : sizeof(C_Login_XB_9E));
return HandlerResult::Type::SUPPRESS; return HandlerResult::Type::SUPPRESS;
} else {
// For command 02, send the same as if we had received 9A from the server
return S_G_9A(ses, command, flag, data);
} }
default:
} else if (ses->version() == GameVersion::XB) { throw logic_error("invalid game version in server init handler");
throw runtime_error("xbox licenses are not implemented");
} else {
throw logic_error("invalid game version in server init handler");
} }
} }
@@ -1679,7 +1714,8 @@ static HandlerResult C_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t c
constexpr on_command_t C_D_6x = &C_6x<G_SendGuildCard_DC_6x06>; constexpr on_command_t C_D_6x = &C_6x<G_SendGuildCard_DC_6x06>;
constexpr on_command_t C_P_6x = &C_6x<G_SendGuildCard_PC_6x06>; constexpr on_command_t C_P_6x = &C_6x<G_SendGuildCard_PC_6x06>;
constexpr on_command_t C_GX_6x = &C_6x<G_SendGuildCard_V3_6x06>; constexpr on_command_t C_G_6x = &C_6x<G_SendGuildCard_GC_6x06>;
constexpr on_command_t C_X_6x = &C_6x<G_SendGuildCard_XB_6x06>;
constexpr on_command_t C_B_6x = &C_6x<G_SendGuildCard_BB_6x06>; constexpr on_command_t C_B_6x = &C_6x<G_SendGuildCard_BB_6x06>;
template <> template <>
@@ -1818,9 +1854,9 @@ static on_command_t handlers[0x100][6][2] = {
/* 5E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 5E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* 5F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 5F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
// CMD S-PATCH C-PATCH S-DC C-DC S-PC C-PC S-GC C-GC S-XB C-XB S-BB C-BB // CMD S-PATCH C-PATCH S-DC C-DC S-PC C-PC S-GC C-GC S-XB C-XB S-BB C-BB
/* 60 */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_GX_6x}, {S_6x, C_GX_6x}, {S_6x, C_B_6x}}, /* 60 */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_G_6x}, {S_6x, C_X_6x}, {S_6x, C_B_6x}},
/* 61 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}}, /* 61 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}},
/* 62 */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_GX_6x}, {S_6x, C_GX_6x}, {S_6x, C_B_6x}}, /* 62 */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_G_6x}, {S_6x, C_X_6x}, {S_6x, C_B_6x}},
/* 63 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 63 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* 64 */ {{S_invalid, nullptr}, {S_D_64, nullptr}, {S_P_64, nullptr}, {S_G_64, nullptr}, {S_X_64, nullptr}, {S_B_64, nullptr}}, /* 64 */ {{S_invalid, nullptr}, {S_D_64, nullptr}, {S_P_64, nullptr}, {S_G_64, nullptr}, {S_X_64, nullptr}, {S_B_64, nullptr}},
/* 65 */ {{S_invalid, nullptr}, {S_DG_65_67_68_EB, nullptr}, {S_P_65_67_68, nullptr}, {S_DG_65_67_68_EB, nullptr}, {S_X_65_67_68, nullptr}, {S_B_65_67_68, nullptr}}, /* 65 */ {{S_invalid, nullptr}, {S_DG_65_67_68_EB, nullptr}, {S_P_65_67_68, nullptr}, {S_DG_65_67_68_EB, nullptr}, {S_X_65_67_68, nullptr}, {S_B_65_67_68, nullptr}},
@@ -1830,8 +1866,8 @@ static on_command_t handlers[0x100][6][2] = {
/* 69 */ {{S_invalid, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}}, /* 69 */ {{S_invalid, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}, {S_66_69_E9, nullptr}},
/* 6A */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 6A */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* 6B */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 6B */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* 6C */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_GX_6x}, {S_6x, C_GX_6x}, {S_6x, C_B_6x}}, /* 6C */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_G_6x}, {S_6x, C_X_6x}, {S_6x, C_B_6x}},
/* 6D */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_GX_6x}, {S_6x, C_GX_6x}, {S_6x, C_B_6x}}, /* 6D */ {{S_invalid, nullptr}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_G_6x}, {S_6x, C_X_6x}, {S_6x, C_B_6x}},
/* 6E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 6E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* 6F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 6F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* 70 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, /* 70 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
+45 -26
View File
@@ -526,6 +526,15 @@ std::shared_ptr<ServerState> ProxyServer::LinkedSession::require_server_state()
return this->require_server()->state; return this->require_server()->state;
} }
void ProxyServer::LinkedSession::resume_xb(shared_ptr<Client> c) {
this->sub_version = c->sub_version;
this->character_name = c->game_data.player()->disp.name.decode();
this->config = c->config;
this->wrapped_client = c;
this->resume_inner(std::move(c->channel), detector_crypt);
c->suspend_timeouts();
}
void ProxyServer::LinkedSession::resume( void ProxyServer::LinkedSession::resume(
Channel&& client_channel, Channel&& client_channel,
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt, shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
@@ -711,31 +720,43 @@ void ProxyServer::LinkedSession::send_to_game_server(const char* error_message)
this->client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd)); this->client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd));
} }
const auto& port_name = version_to_login_port_name.at(static_cast<size_t>(this->version())); if (this->version() == GameVersion::XB) {
if (!this->wrapped_client) {
S_Reconnect_19 reconnect_cmd = {{0, s->name_to_port_config.at(port_name)->port, 0}}; throw logic_error("wrapped client is missing from XB proxy session");
// If the client is on a virtual connection, we can use any address
// here and they should be able to connect back to the game server. If
// the client is on a real connection, we'll use the sockname of the
// existing connection (like we do in the server 19 command handler).
if (this->client_channel.is_virtual_connection) {
struct sockaddr_in* dest_sin = reinterpret_cast<struct sockaddr_in*>(&this->next_destination);
if (dest_sin->sin_family != AF_INET) {
throw logic_error("ss not AF_INET");
} }
reconnect_cmd.address.store_raw(dest_sin->sin_addr.s_addr); this->wrapped_client->should_disconnect = false;
s->game_server->connect_client(this->wrapped_client, std::move(this->client_channel));
on_login_complete(this->wrapped_client);
this->disconnect_action = DisconnectAction::CLOSE_IMMEDIATELY;
this->disconnect();
} else { } else {
const struct sockaddr_in* sin = reinterpret_cast<const struct sockaddr_in*>( const auto& port_name = version_to_login_port_name.at(static_cast<size_t>(this->version()));
&this->client_channel.local_addr);
if (sin->sin_family != AF_INET) {
throw logic_error("existing connection is not ipv4");
}
reconnect_cmd.address.store_raw(sin->sin_addr.s_addr);
}
this->client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd)); S_Reconnect_19 reconnect_cmd = {{0, s->name_to_port_config.at(port_name)->port, 0}};
this->disconnect_action = DisconnectAction::CLOSE_IMMEDIATELY;
// If the client is on a virtual connection, we can use any address
// here and they should be able to connect back to the game server. If
// the client is on a real connection, we'll use the sockname of the
// existing connection (like we do in the server 19 command handler).
if (this->client_channel.is_virtual_connection) {
struct sockaddr_in* dest_sin = reinterpret_cast<struct sockaddr_in*>(&this->next_destination);
if (dest_sin->sin_family != AF_INET) {
throw logic_error("ss not AF_INET");
}
reconnect_cmd.address.store_raw(dest_sin->sin_addr.s_addr);
} else {
const struct sockaddr_in* sin = reinterpret_cast<const struct sockaddr_in*>(
&this->client_channel.local_addr);
if (sin->sin_family != AF_INET) {
throw logic_error("existing connection is not ipv4");
}
reconnect_cmd.address.store_raw(sin->sin_addr.s_addr);
}
this->client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd));
this->disconnect_action = DisconnectAction::CLOSE_IMMEDIATELY;
}
} }
} }
@@ -762,8 +783,7 @@ void ProxyServer::LinkedSession::disconnect() {
// Set a timeout to delete the session entirely (in case the client doesn't // Set a timeout to delete the session entirely (in case the client doesn't
// reconnect) // reconnect)
struct timeval tv = usecs_to_timeval(this->timeout_for_disconnect_action( struct timeval tv = usecs_to_timeval(this->timeout_for_disconnect_action(this->disconnect_action));
this->disconnect_action));
event_add(this->timeout_event.get(), &tv); event_add(this->timeout_event.get(), &tv);
} }
@@ -818,8 +838,7 @@ shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session_by_name(
shared_ptr<ProxyServer::LinkedSession> ProxyServer::create_licensed_session( shared_ptr<ProxyServer::LinkedSession> ProxyServer::create_licensed_session(
shared_ptr<License> l, uint16_t local_port, GameVersion version, shared_ptr<License> l, uint16_t local_port, GameVersion version,
const Client::Config& config) { const Client::Config& config) {
shared_ptr<LinkedSession> session(new LinkedSession( shared_ptr<LinkedSession> session(new LinkedSession(this->shared_from_this(), local_port, version, l, config));
this->shared_from_this(), local_port, version, l, config));
auto emplace_ret = this->id_to_session.emplace(session->id, session); auto emplace_ret = this->id_to_session.emplace(session->id, session);
if (!emplace_ret.second) { if (!emplace_ret.second) {
throw runtime_error("session already exists for this license"); throw runtime_error("session already exists for this license");
+4 -1
View File
@@ -75,8 +75,9 @@ public:
struct LobbyPlayer { struct LobbyPlayer {
uint32_t guild_card_number = 0; uint32_t guild_card_number = 0;
uint64_t xb_user_id = 0;
std::string name; std::string name;
uint8_t language; uint8_t language = 0;
uint8_t section_id = 0; uint8_t section_id = 0;
uint8_t char_class = 0; uint8_t char_class = 0;
}; };
@@ -90,6 +91,7 @@ public:
bool is_in_quest; bool is_in_quest;
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt; std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
std::shared_ptr<Client> wrapped_client;
struct SavingFile { struct SavingFile {
std::string basename; std::string basename;
@@ -143,6 +145,7 @@ public:
return this->client_channel.language; return this->client_channel.language;
} }
void resume_xb(std::shared_ptr<Client> wrapped_client);
void resume( void resume(
Channel&& client_channel, Channel&& client_channel,
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt, std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
+72 -33
View File
@@ -352,7 +352,7 @@ string VersionedQuest::bin_filename() const {
if (this->episode == Episode::EP3) { if (this->episode == Episode::EP3) {
return string_printf("m%06" PRIu32 "p_e.bin", this->quest_number); return string_printf("m%06" PRIu32 "p_e.bin", this->quest_number);
} else { } else {
return string_printf("q%" PRIu32 ".bin", this->quest_number); return string_printf("quest%" PRIu32 ".bin", this->quest_number);
} }
} }
@@ -360,7 +360,15 @@ string VersionedQuest::dat_filename() const {
if (this->episode == Episode::EP3) { if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .dat files"); throw logic_error("Episode 3 quests do not have .dat files");
} else { } else {
return string_printf("q%" PRIu32 ".dat", this->quest_number); return string_printf("quest%" PRIu32 ".dat", this->quest_number);
}
}
string VersionedQuest::xb_filename() const {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have Xbox filenames");
} else {
return string_printf("quest%" PRIu32 "_%c.dat", this->quest_number, tolower(char_for_language_code(this->language)));
} }
} }
@@ -370,6 +378,7 @@ string VersionedQuest::encode_qst() const {
*this->dat_contents, *this->dat_contents,
this->name, this->name,
this->quest_number, this->quest_number,
this->language,
this->version, this->version,
this->is_dlq_encoded); this->is_dlq_encoded);
} }
@@ -1115,14 +1124,15 @@ pair<string, string> decode_qst_data(const string& data) {
// the first 4 bytes in the file: // the first 4 bytes in the file:
// - BB: 58 00 44 00 or 58 00 A6 00 // - BB: 58 00 44 00 or 58 00 A6 00
// - PC: 3C 00 44 ?? or 3C 00 A6 ?? // - PC: 3C 00 44 ?? or 3C 00 A6 ??
// - DC/V3: 44 ?? 3C 00 or A6 ?? 3C 00 // - DC/GC: 44 ?? 3C 00 or A6 ?? 3C 00
// - XB: 44 ?? 54 00 or A6 ?? 54 00
StringReader r(data); StringReader r(data);
uint32_t signature = r.get_u32b(); uint32_t signature = r.get_u32b();
if (signature == 0x58004400 || signature == 0x5800A600) { if ((signature == 0x58004400) || (signature == 0x5800A600)) {
return decode_qst_data_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(data); return decode_qst_data_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(data);
} else if ((signature & 0xFFFFFF00) == 0x3C004400 || (signature & 0xFFFFFF00) == 0x3C00A600) { } else if (((signature & 0xFFFFFF00) == 0x3C004400) || ((signature & 0xFFFFFF00) == 0x3C00A600)) {
return decode_qst_data_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(data); return decode_qst_data_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(data);
} else if ((signature & 0xFF00FFFF) == 0x44003C00 || (signature & 0xFF00FFFF) == 0xA6003C00) { } else if (((signature & 0xFF00FFFF) == 0x44003C00) || ((signature & 0xFF00FFFF) == 0xA6003C00)) {
// In PSO DC, the type field is only one byte, but in V3 it's two bytes and // In PSO DC, the type field is only one byte, but in V3 it's two bytes and
// the filename was shifted over by one byte. To detect this, we check if // the filename was shifted over by one byte. To detect this, we check if
// the V3 type field has a reasonable value, and if not, we assume the file // the V3 type field has a reasonable value, and if not, we assume the file
@@ -1132,7 +1142,7 @@ pair<string, string> decode_qst_data(const string& data) {
} else { } else {
return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(data); return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(data);
} }
} else if ((signature & 0xFF00FFFF) == 0x44005400 || (signature & 0xFF00FFFF) == 0xA6005400) { } else if (((signature & 0xFF00FFFF) == 0x44005400) || ((signature & 0xFF00FFFF) == 0xA6005400)) {
return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(data); return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(data);
} else { } else {
throw runtime_error("invalid qst file format"); throw runtime_error("invalid qst file format");
@@ -1150,7 +1160,14 @@ void add_command_header(
} }
template <typename HeaderT, typename CmdT> template <typename HeaderT, typename CmdT>
void add_open_file_command(StringWriter& w, const std::string& name, const std::string& filename, size_t file_size, bool is_download) { void add_open_file_command_t(
StringWriter& w,
const std::string& name,
const std::string& filename,
const std::string&,
uint32_t,
size_t file_size,
bool is_download) {
add_command_header<HeaderT>(w, is_download ? 0xA6 : 0x44, 0x00, sizeof(CmdT)); add_command_header<HeaderT>(w, is_download ? 0xA6 : 0x44, 0x00, sizeof(CmdT));
CmdT cmd; CmdT cmd;
cmd.name.assign_raw("PSO/" + name); cmd.name.assign_raw("PSO/" + name);
@@ -1162,8 +1179,28 @@ void add_open_file_command(StringWriter& w, const std::string& name, const std::
w.put(cmd); w.put(cmd);
} }
template <>
void add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(
StringWriter& w,
const std::string& name,
const std::string& filename,
const std::string& xb_filename,
uint32_t quest_number,
size_t file_size,
bool is_download) {
add_command_header<PSOCommandHeaderDCV3>(w, is_download ? 0xA6 : 0x44, 0x00, sizeof(S_OpenFile_XB_44_A6));
S_OpenFile_XB_44_A6 cmd;
cmd.name.assign_raw("PSO/" + name);
cmd.filename.encode(filename);
cmd.type = 0;
cmd.file_size = file_size;
cmd.xb_filename.encode(xb_filename);
cmd.content_meta = 0x30000000 | quest_number;
w.put(cmd);
}
template <typename HeaderT> template <typename HeaderT>
void add_write_file_commands( void add_write_file_commands_t(
StringWriter& w, StringWriter& w,
const string& filename, const string& filename,
const string& data, const string& data,
@@ -1191,12 +1228,14 @@ string encode_qst_file(
const string& dat_data, const string& dat_data,
const string& name, const string& name,
uint32_t quest_number, uint32_t quest_number,
uint8_t language,
QuestScriptVersion version, QuestScriptVersion version,
bool is_dlq_encoded) { bool is_dlq_encoded) {
StringWriter w; StringWriter w;
string bin_filename = string_printf("q%" PRIu32 ".bin", quest_number); string bin_filename = string_printf("quest%" PRIu32 ".bin", quest_number);
string dat_filename = string_printf("q%" PRIu32 ".dat", quest_number); string dat_filename = string_printf("quest%" PRIu32 ".dat", quest_number);
string xb_filename = string_printf("quest%" PRIu32 "_%c.dat", quest_number, tolower(char_for_language_code(language)));
// Some tools expect both open file commands at the beginning, hence this // Some tools expect both open file commands at the beginning, hence this
// unfortunate abstraction-breaking. // unfortunate abstraction-breaking.
@@ -1204,39 +1243,39 @@ string encode_qst_file(
case QuestScriptVersion::DC_NTE: case QuestScriptVersion::DC_NTE:
case QuestScriptVersion::DC_V1: case QuestScriptVersion::DC_V1:
case QuestScriptVersion::DC_V2: case QuestScriptVersion::DC_V2:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, bin_filename, bin_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, dat_filename, dat_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
add_write_file_commands<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
add_write_file_commands<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false);
break; break;
case QuestScriptVersion::PC_V2: case QuestScriptVersion::PC_V2:
add_open_file_command<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, bin_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
add_open_file_command<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, dat_filename, dat_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
add_write_file_commands<PSOCommandHeaderPC>(w, bin_filename, bin_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderPC>(w, bin_filename, bin_data, is_dlq_encoded, false);
add_write_file_commands<PSOCommandHeaderPC>(w, dat_filename, dat_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderPC>(w, dat_filename, dat_data, is_dlq_encoded, false);
break; break;
case QuestScriptVersion::GC_NTE: case QuestScriptVersion::GC_NTE:
case QuestScriptVersion::GC_V3: case QuestScriptVersion::GC_V3:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, bin_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, dat_filename, dat_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
add_write_file_commands<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
add_write_file_commands<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false);
break; break;
case QuestScriptVersion::GC_EP3: case QuestScriptVersion::GC_EP3:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, bin_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
add_write_file_commands<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
break; break;
case QuestScriptVersion::XB_V3: case QuestScriptVersion::XB_V3:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, bin_filename, bin_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, dat_filename, dat_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
add_write_file_commands<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
add_write_file_commands<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false);
break; break;
case QuestScriptVersion::BB_V4: case QuestScriptVersion::BB_V4:
add_open_file_command<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, bin_filename, bin_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
add_open_file_command<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, dat_filename, dat_data.size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
add_write_file_commands<PSOCommandHeaderBB>(w, bin_filename, bin_data, is_dlq_encoded, true); add_write_file_commands_t<PSOCommandHeaderBB>(w, bin_filename, bin_data, is_dlq_encoded, true);
add_write_file_commands<PSOCommandHeaderBB>(w, dat_filename, dat_data, is_dlq_encoded, true); add_write_file_commands_t<PSOCommandHeaderBB>(w, dat_filename, dat_data, is_dlq_encoded, true);
break; break;
default: default:
throw logic_error("invalid game version"); throw logic_error("invalid game version");
+2
View File
@@ -81,6 +81,7 @@ struct VersionedQuest {
std::string bin_filename() const; std::string bin_filename() const;
std::string dat_filename() const; std::string dat_filename() const;
std::string xb_filename() const;
std::shared_ptr<VersionedQuest> create_download_quest(uint8_t override_language = 0xFF) const; std::shared_ptr<VersionedQuest> create_download_quest(uint8_t override_language = 0xFF) const;
std::string encode_qst() const; std::string encode_qst() const;
@@ -149,5 +150,6 @@ std::string encode_qst_file(
const std::string& dat_data, const std::string& dat_data,
const std::string& name, const std::string& name,
uint32_t quest_number, uint32_t quest_number,
uint8_t language,
QuestScriptVersion version, QuestScriptVersion version,
bool is_dlq_encoded); bool is_dlq_encoded);
+146 -44
View File
@@ -30,6 +30,8 @@ const char* BATTLE_TABLE_DISCONNECT_HOOK_NAME = "battle_table_state";
const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME = "quest_barrier"; const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME = "quest_barrier";
const char* ADD_NEXT_CLIENT_DISCONNECT_HOOK_NAME = "add_next_game_client"; const char* ADD_NEXT_CLIENT_DISCONNECT_HOOK_NAME = "add_next_game_client";
void on_login_complete(shared_ptr<Client> c);
static shared_ptr<const Menu> proxy_options_menu_for_client(shared_ptr<const Client> c) { static shared_ptr<const Menu> proxy_options_menu_for_client(shared_ptr<const Client> c) {
auto s = c->require_server_state(); auto s = c->require_server_state();
@@ -91,14 +93,32 @@ static shared_ptr<const Menu> proxy_options_menu_for_client(shared_ptr<const Cli
return ret; return ret;
} }
static void send_client_to_lobby_server(shared_ptr<Client> c) { void send_client_to_login_server(shared_ptr<Client> c) {
auto s = c->require_server_state(); if (c->version() == GameVersion::XB) {
const auto& port_name = version_to_lobby_port_name.at(static_cast<size_t>(c->version())); c->server_behavior = ServerBehavior::LOGIN_SERVER;
send_reconnect(c, s->connect_address_for_client(c), on_login_complete(c);
s->name_to_port_config.at(port_name)->port);
} else {
const auto& port_name = version_to_login_port_name.at(static_cast<size_t>(c->version()));
auto s = c->require_server_state();
send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at(port_name)->port);
}
} }
static void send_client_to_proxy_server(shared_ptr<Client> c) { void send_client_to_lobby_server(shared_ptr<Client> c) {
if (c->version() == GameVersion::XB) {
c->server_behavior = ServerBehavior::LOBBY_SERVER;
on_login_complete(c);
} else {
auto s = c->require_server_state();
const auto& port_name = version_to_lobby_port_name.at(static_cast<size_t>(c->version()));
send_reconnect(c, s->connect_address_for_client(c),
s->name_to_port_config.at(port_name)->port);
}
}
void send_client_to_proxy_server(shared_ptr<Client> c) {
auto s = c->require_server_state(); auto s = c->require_server_state();
const auto& port_name = version_to_proxy_port_name.at(static_cast<size_t>(c->version())); const auto& port_name = version_to_proxy_port_name.at(static_cast<size_t>(c->version()));
@@ -117,7 +137,12 @@ static void send_client_to_proxy_server(shared_ptr<Client> c) {
ses->remote_guild_card_number = 0; ses->remote_guild_card_number = 0;
} }
send_reconnect(c, s->connect_address_for_client(c), local_port); if (c->version() == GameVersion::XB) {
ses->resume_xb(c);
c->should_disconnect = true;
} else {
send_reconnect(c, s->connect_address_for_client(c), local_port);
}
} }
static void send_proxy_destinations_menu(shared_ptr<Client> c) { static void send_proxy_destinations_menu(shared_ptr<Client> c) {
@@ -239,7 +264,7 @@ static void send_main_menu(shared_ptr<Client> c) {
"Disconnect", 0); "Disconnect", 0);
main_menu->items.emplace_back(MainMenuItemID::CLEAR_LICENSE, "Clear license", main_menu->items.emplace_back(MainMenuItemID::CLEAR_LICENSE, "Clear license",
"Disconnect with an\ninvalid license error\nso you can enter a\ndifferent serial\nnumber, access key,\nor password", "Disconnect with an\ninvalid license error\nso you can enter a\ndifferent serial\nnumber, access key,\nor password",
MenuItem::Flag::INVISIBLE_ON_DCNTE | MenuItem::Flag::INVISIBLE_ON_BB); MenuItem::Flag::INVISIBLE_ON_DCNTE | MenuItem::Flag::INVISIBLE_ON_XB | MenuItem::Flag::INVISIBLE_ON_BB);
send_menu(c, main_menu); send_menu(c, main_menu);
} }
@@ -312,6 +337,10 @@ void on_disconnect(shared_ptr<Client> c) {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
static void on_05(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
c->should_disconnect = true;
}
static void set_console_client_flags(shared_ptr<Client> c, uint32_t sub_version) { static void set_console_client_flags(shared_ptr<Client> c, uint32_t sub_version) {
if (c->channel.crypt_in->type() == PSOEncryption::Type::V2) { if (c->channel.crypt_in->type() == PSOEncryption::Type::V2) {
if (sub_version <= 0x28) { if (sub_version <= 0x28) {
@@ -323,6 +352,7 @@ static void set_console_client_flags(shared_ptr<Client> c, uint32_t sub_version)
} }
} }
c->config.set_flags_for_version(c->version(), sub_version); c->config.set_flags_for_version(c->version(), sub_version);
c->sub_version = sub_version;
if (c->config.specific_version == default_specific_version_for_version(c->version(), -1)) { if (c->config.specific_version == default_specific_version_for_version(c->version(), -1)) {
c->config.specific_version = default_specific_version_for_version(c->version(), sub_version); c->config.specific_version = default_specific_version_for_version(c->version(), sub_version);
} }
@@ -747,21 +777,7 @@ static void on_9D_9E(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
} }
} else if (command == 0x9E) { } else if (command == 0x9E) {
// GC and XB send different amounts of data in this command. This is how const auto& cmd = check_size_t<C_Login_GC_9E>(data, sizeof(C_LoginExtended_GC_9E));
// newserv determines if a V3 client is GC or XB.
const auto& cmd = check_size_t<C_Login_GC_9E>(data, sizeof(C_LoginExtended_XB_9E));
switch (data.size()) {
case sizeof(C_Login_GC_9E):
case sizeof(C_LoginExtended_GC_9E):
break;
case sizeof(C_Login_XB_9E):
case sizeof(C_LoginExtended_XB_9E):
c->channel.version = GameVersion::XB;
c->log.info("Game version set to XB");
break;
default:
throw runtime_error("invalid size for 9E command");
}
base_cmd = &cmd; base_cmd = &cmd;
if (cmd.is_extended) { if (cmd.is_extended) {
const auto& cmd = check_size_t<C_LoginExtended_GC_9E>(data); const auto& cmd = check_size_t<C_LoginExtended_GC_9E>(data);
@@ -832,12 +848,11 @@ static void on_9D_9E(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
return; return;
} catch (const LicenseIndex::missing_license& e) { } catch (const LicenseIndex::missing_license& e) {
// On V3, the client should have sent a different command containing the // On GC, the client should have sent a different command containing the
// password already, which should have created and added a license. So, if // password already, which should have created and added a license. So, if
// no license exists at this point, disconnect the client even if // no license exists at this point, disconnect the client even if
// unregistered clients are allowed. // unregistered clients are allowed.
shared_ptr<License> l; if (c->version() == GameVersion::GC) {
if ((c->version() == GameVersion::GC) || (c->version() == GameVersion::XB)) {
send_command(c, 0x04, 0x04); send_command(c, 0x04, 0x04);
c->should_disconnect = true; c->should_disconnect = true;
return; return;
@@ -861,6 +876,81 @@ static void on_9D_9E(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
on_login_complete(c); on_login_complete(c);
} }
static void on_9E_XB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
auto s = c->require_server_state();
const auto& cmd = check_size_t<C_Login_XB_9E>(data, sizeof(C_LoginExtended_XB_9E));
if (cmd.is_extended) {
const auto& cmd = check_size_t<C_LoginExtended_XB_9E>(data);
if (cmd.extension.lobby_refs[0].menu_id == MenuID::LOBBY) {
c->preferred_lobby_id = cmd.extension.lobby_refs[0].item_id;
}
}
try {
c->config.parse_from(cmd.client_config);
} 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->config.set_flag(Client::Flag::AT_WELCOME_MESSAGE);
}
c->xb_netloc.reset(new XBNetworkLocation(cmd.netloc));
c->xb_9E_unknown_a1a = cmd.unknown_a1a;
c->channel.language = cmd.language;
c->config.set_flags_for_version(c->version(), -1);
set_console_client_flags(c, cmd.sub_version);
string xb_gamertag = cmd.serial_number.decode();
uint64_t xb_user_id = stoull(cmd.access_key.decode(), nullptr, 16);
uint64_t xb_account_id = cmd.netloc.account_id;
try {
shared_ptr<License> l = s->license_index->verify_xb(xb_gamertag, xb_user_id, xb_account_id);
bool should_save = false;
if (l->xb_user_id == 0) {
l->xb_user_id = xb_user_id;
c->log.info("Set license XB user ID to %016" PRIX64, l->xb_user_id);
should_save = true;
}
if (l->xb_account_id == 0) {
l->xb_account_id = xb_account_id;
c->log.info("Set license XB account ID to %016" PRIX64, l->xb_account_id);
should_save = true;
}
if (should_save && !s->is_replay) {
l->save();
}
c->set_license(l);
} catch (const LicenseIndex::no_username& e) {
send_command(c, 0x04, 0x03);
c->should_disconnect = true;
return;
} catch (const LicenseIndex::incorrect_access_key& e) {
send_command(c, 0x04, 0x03);
c->should_disconnect = true;
return;
} catch (const LicenseIndex::missing_license& e) {
shared_ptr<License> l(new License());
l->serial_number = fnv1a32(xb_gamertag) & 0x7FFFFFFF;
l->xb_gamertag = xb_gamertag;
l->xb_user_id = xb_user_id;
l->xb_account_id = xb_account_id;
s->license_index->add(l);
if (!s->is_replay) {
l->save();
}
c->set_license(l);
string l_str = l->str();
c->log.info("Created license %s", l_str.c_str());
}
send_update_client_config(c);
on_login_complete(c);
}
static void on_93_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) { static void on_93_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
const auto& cmd = check_size_t<C_Login_BB_93>(data, sizeof(C_Login_BB_93) - 8, sizeof(C_Login_BB_93)); const auto& cmd = check_size_t<C_Login_BB_93>(data, sizeof(C_Login_BB_93) - 8, sizeof(C_Login_BB_93));
auto s = c->require_server_state(); auto s = c->require_server_state();
@@ -1784,6 +1874,9 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
} }
case MainMenuItemID::PROXY_DESTINATIONS: case MainMenuItemID::PROXY_DESTINATIONS:
if (!c->game_data.player(false, false)) {
send_get_player_info(c);
}
send_proxy_destinations_menu(c); send_proxy_destinations_menu(c);
break; break;
@@ -1851,6 +1944,11 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
break; break;
case MainMenuItemID::DISCONNECT: case MainMenuItemID::DISCONNECT:
if (c->version() == GameVersion::XB) {
// On XB (at least via Insignia) the server has to explicitly tell
// the client to disconnect by sending this command.
send_command(c, 0x05, 0x00);
}
c->should_disconnect = true; c->should_disconnect = true;
break; break;
@@ -2140,8 +2238,9 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
string bin_filename = vq->bin_filename(); string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename(); string dat_filename = vq->dat_filename();
send_open_quest_file(lc, bin_filename, bin_filename, vq->bin_contents, QuestFileType::ONLINE); string xb_filename = vq->xb_filename();
send_open_quest_file(lc, dat_filename, dat_filename, vq->dat_contents, QuestFileType::ONLINE); send_open_quest_file(lc, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(lc, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
// There is no such thing as command AC on PSO V1 and V2 - quests just // There is no such thing as command AC on PSO V1 and V2 - quests just
// start immediately when they're done downloading. (This is also the // start immediately when they're done downloading. (This is also the
@@ -2171,11 +2270,12 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
// TODO: This is not true for Episode 3 Trial Edition. We also would // TODO: This is not true for Episode 3 Trial Edition. We also would
// have to convert the map to a MapDefinitionTrial, though. // have to convert the map to a MapDefinitionTrial, though.
if (vq->version == QuestScriptVersion::GC_EP3) { if (vq->version == QuestScriptVersion::GC_EP3) {
send_open_quest_file(c, q->name, vq->bin_filename(), vq->bin_contents, QuestFileType::EPISODE_3); send_open_quest_file(c, q->name, vq->bin_filename(), "", vq->quest_number, QuestFileType::EPISODE_3, vq->bin_contents);
} else { } else {
vq = vq->create_download_quest(c->language()); vq = vq->create_download_quest(c->language());
send_open_quest_file(c, q->name, vq->bin_filename(), vq->bin_contents, QuestFileType::DOWNLOAD); string xb_filename = vq->xb_filename();
send_open_quest_file(c, q->name, vq->dat_filename(), vq->dat_contents, QuestFileType::DOWNLOAD); send_open_quest_file(c, q->name, vq->bin_filename(), xb_filename, vq->quest_number, QuestFileType::DOWNLOAD, vq->bin_contents);
send_open_quest_file(c, q->name, vq->dat_filename(), xb_filename, vq->quest_number, QuestFileType::DOWNLOAD, vq->dat_contents);
} }
} }
break; break;
@@ -2354,11 +2454,7 @@ static void on_A0(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
send_message_box(c, ""); send_message_box(c, "");
} }
const auto& port_name = version_to_login_port_name.at(static_cast<size_t>(c->version())); send_client_to_login_server(c);
auto s = c->require_server_state();
send_reconnect(c, s->connect_address_for_client(c),
s->name_to_port_config.at(port_name)->port);
} }
static void on_A1(shared_ptr<Client> c, uint16_t command, uint32_t flag, string& data) { static void on_A1(shared_ptr<Client> c, uint16_t command, uint32_t flag, string& data) {
@@ -2555,7 +2651,7 @@ static void on_D7_GC(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
try { try {
static FileContentsCache gba_file_cache(300 * 1000 * 1000); static FileContentsCache gba_file_cache(300 * 1000 * 1000);
auto f = gba_file_cache.get_or_load("system/gba/" + filename).file; auto f = gba_file_cache.get_or_load("system/gba/" + filename).file;
send_open_quest_file(c, "", filename, f->data, QuestFileType::GBA_DEMO); send_open_quest_file(c, "", filename, "", 0, QuestFileType::GBA_DEMO, f->data);
} catch (const out_of_range&) { } catch (const out_of_range&) {
send_command(c, 0xD7, 0x00); send_command(c, 0xD7, 0x00);
} catch (const cannot_open_file&) { } catch (const cannot_open_file&) {
@@ -3281,6 +3377,11 @@ static void on_C6(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
} }
} }
static void on_C9_XB(shared_ptr<Client> c, uint16_t, uint32_t flag, string& data) {
check_size_v(data.size(), 0);
c->log.warning("Ignoring connection status change command (%02" PRIX32 ")", flag);
}
shared_ptr<Lobby> create_game_generic( shared_ptr<Lobby> create_game_generic(
shared_ptr<ServerState> s, shared_ptr<ServerState> s,
shared_ptr<Client> c, shared_ptr<Client> c,
@@ -3681,12 +3782,13 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
if (!vq) { if (!vq) {
throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest for client version"); throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest for client version");
} }
string bin_basename = vq->bin_filename(); string bin_filename = vq->bin_filename();
string dat_basename = vq->dat_filename(); string dat_filename = vq->dat_filename();
send_open_quest_file(c, bin_basename + ".bin", bin_basename, vq->bin_contents, QuestFileType::ONLINE); send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_basename + ".dat", dat_basename, vq->dat_contents, QuestFileType::ONLINE); send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
c->config.set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); c->config.set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
} else if (l->map) { } else if (l->map) {
send_rare_enemy_index_list(c, l->map->rare_enemy_indexes); send_rare_enemy_index_list(c, l->map->rare_enemy_indexes);
} }
@@ -4127,7 +4229,7 @@ static on_command_t handlers[0x100][6] = {
/* 02 */ {on_02_P, nullptr, nullptr, nullptr, nullptr, nullptr}, /* 02 */ {on_02_P, nullptr, nullptr, nullptr, nullptr, nullptr},
/* 03 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* 03 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
/* 04 */ {on_04_P, nullptr, nullptr, nullptr, nullptr, nullptr}, /* 04 */ {on_04_P, nullptr, nullptr, nullptr, nullptr, nullptr},
/* 05 */ {nullptr, on_ignored, on_ignored, on_ignored, on_ignored, on_ignored}, /* 05 */ {nullptr, on_05, on_05, on_05, on_05, on_05},
/* 06 */ {nullptr, on_06, on_06, on_06, on_06, on_06}, /* 06 */ {nullptr, on_06, on_06, on_06, on_06, on_06},
/* 07 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* 07 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
/* 08 */ {nullptr, on_08_E6, on_08_E6, on_08_E6, on_08_E6, on_08_E6}, /* 08 */ {nullptr, on_08_E6, on_08_E6, on_08_E6, on_08_E6, on_08_E6},
@@ -4284,7 +4386,7 @@ static on_command_t handlers[0x100][6] = {
/* 9B */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* 9B */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
/* 9C */ {nullptr, on_9C, on_9C, on_9C, on_9C, nullptr}, /* 9C */ {nullptr, on_9C, on_9C, on_9C, on_9C, nullptr},
/* 9D */ {nullptr, on_9D_9E, on_9D_9E, on_9D_9E, on_9D_9E, nullptr}, /* 9D */ {nullptr, on_9D_9E, on_9D_9E, on_9D_9E, on_9D_9E, nullptr},
/* 9E */ {nullptr, nullptr, on_9D_9E, on_9D_9E, on_9D_9E, nullptr}, /* 9E */ {nullptr, nullptr, on_9D_9E, on_9D_9E, on_9E_XB, nullptr},
/* 9F */ {nullptr, nullptr, nullptr, on_9F_V3, on_9F_V3, nullptr}, /* 9F */ {nullptr, nullptr, nullptr, on_9F_V3, on_9F_V3, nullptr},
// PATCH DC PC GC XB BB // PATCH DC PC GC XB BB
/* A0 */ {nullptr, on_A0, on_A0, on_A0, on_A0, on_A0}, /* A0 */ {nullptr, on_A0, on_A0, on_A0, on_A0, on_A0},
@@ -4329,7 +4431,7 @@ static on_command_t handlers[0x100][6] = {
/* C6 */ {nullptr, nullptr, on_C6, on_C6, on_C6, on_C6}, /* C6 */ {nullptr, nullptr, on_C6, on_C6, on_C6, on_C6},
/* C7 */ {nullptr, nullptr, on_C7, on_C7, on_C7, on_C7}, /* C7 */ {nullptr, nullptr, on_C7, on_C7, on_C7, on_C7},
/* C8 */ {nullptr, nullptr, on_C8, on_C8, on_C8, on_C8}, /* C8 */ {nullptr, nullptr, on_C8, on_C8, on_C8, on_C8},
/* C9 */ {nullptr, nullptr, nullptr, on_6x_C9_CB, nullptr, nullptr}, /* C9 */ {nullptr, nullptr, nullptr, on_6x_C9_CB, on_C9_XB, nullptr},
/* CA */ {nullptr, nullptr, nullptr, on_CA_Ep3, nullptr, nullptr}, /* CA */ {nullptr, nullptr, nullptr, on_CA_Ep3, nullptr, nullptr},
/* CB */ {nullptr, nullptr, nullptr, on_6x_C9_CB, nullptr, nullptr}, /* CB */ {nullptr, nullptr, nullptr, on_6x_C9_CB, nullptr, nullptr},
/* CC */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* CC */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
+6
View File
@@ -18,5 +18,11 @@ std::shared_ptr<Lobby> create_game_generic(
void on_connect(std::shared_ptr<Client> c); void on_connect(std::shared_ptr<Client> c);
void on_disconnect(std::shared_ptr<Client> c); void on_disconnect(std::shared_ptr<Client> c);
void on_login_complete(std::shared_ptr<Client> c);
void on_command(std::shared_ptr<Client> c, uint16_t command, uint32_t flag, std::string& data); void on_command(std::shared_ptr<Client> c, uint16_t command, uint32_t flag, std::string& data);
void on_command_with_header(std::shared_ptr<Client> c, const std::string& data); void on_command_with_header(std::shared_ptr<Client> c, const std::string& data);
void send_client_to_login_server(std::shared_ptr<Client> c);
void send_client_to_lobby_server(std::shared_ptr<Client> c);
void send_client_to_proxy_server(std::shared_ptr<Client> c);
+35 -9
View File
@@ -249,9 +249,11 @@ static void on_sync_joining_player_item_state(shared_ptr<Client> c, uint8_t comm
static void on_sync_joining_player_disp_and_inventory( static void on_sync_joining_player_disp_and_inventory(
shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) { shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
auto l = c->require_lobby(); auto s = c->require_server_state();
// In V1/V2 games, this command sometimes is sent after the new client has // In V1/V2 games, this command sometimes is sent after the new client has
// finished loading, so we don't check l->any_client_loading() here. // finished loading, so we don't check l->any_client_loading() here.
auto l = c->require_lobby();
if (!l->is_game()) { if (!l->is_game()) {
return; return;
} }
@@ -283,15 +285,34 @@ static void on_sync_joining_player_disp_and_inventory(
bool target_is_gc = (target->version() == GameVersion::GC); bool target_is_gc = (target->version() == GameVersion::GC);
if (target_is_gc == sender_is_gc) { if (target_is_gc == sender_is_gc) {
send_command(target, command, flag, data, size); send_command(target, command, flag, data, size);
} else {
auto out_cmd = check_size_t<G_SyncPlayerDispAndInventory_DC_PC_V3_6x70>(data, size); } else if (sender_is_gc) {
for (size_t z = 0; z < 30; z++) { // Convert GC command to XB command
// NOTE: If we use this codepath for non-V3 in the future, we'll need to G_SyncPlayerDispAndInventory_XB_6x70 out_cmd = {check_size_t<G_SyncPlayerDispAndInventory_DC_PC_GC_6x70>(data, size), 0, 0, 0};
// change this hardcoded version. This only works because GC's mag if (c->license->xb_user_id) {
// encoding/decoding is symmetric (encode and decode do the same thing). out_cmd.xb_user_id_high = static_cast<uint32_t>((c->license->xb_user_id >> 32) & 0xFFFFFFFF);
out_cmd.xb_user_id_low = static_cast<uint32_t>(c->license->xb_user_id & 0xFFFFFFFF);
} else {
out_cmd.xb_user_id_high = 0xAE000000;
out_cmd.xb_user_id_low = c->license->serial_number;
}
for (size_t z = 0; z < out_cmd.inventory.num_items; z++) {
out_cmd.inventory.items[z].data.decode_for_version(GameVersion::GC); out_cmd.inventory.items[z].data.decode_for_version(GameVersion::GC);
out_cmd.inventory.items[z].data.encode_for_version(GameVersion::XB, s->item_parameter_table_for_version(GameVersion::XB));
} }
send_command_t(target, command, flag, out_cmd); send_command_t(target, command, flag, out_cmd);
} else {
// Comvert XB command to GC command
static_assert(
sizeof(G_SyncPlayerDispAndInventory_DC_PC_GC_6x70) < sizeof(G_SyncPlayerDispAndInventory_XB_6x70),
"GC 6x70 command is larger than XB 6x70 command");
auto out_cmd = check_size_t<G_SyncPlayerDispAndInventory_XB_6x70>(data, size);
for (size_t z = 0; z < out_cmd.inventory.num_items; z++) {
out_cmd.inventory.items[z].data.decode_for_version(GameVersion::XB);
out_cmd.inventory.items[z].data.encode_for_version(GameVersion::GC, s->item_parameter_table_for_version(GameVersion::GC));
}
send_command(target, command, flag, &out_cmd, sizeof(G_SyncPlayerDispAndInventory_DC_PC_GC_6x70));
} }
} }
@@ -417,9 +438,13 @@ static void on_send_guild_card(shared_ptr<Client> c, uint8_t command, uint8_t fl
c->game_data.player(true, false)->guild_card_description = cmd.guild_card.description; c->game_data.player(true, false)->guild_card_description = cmd.guild_card.description;
break; break;
} }
case GameVersion::GC: case GameVersion::GC: {
const auto& cmd = check_size_t<G_SendGuildCard_GC_6x06>(data, size);
c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language());
break;
}
case GameVersion::XB: { case GameVersion::XB: {
const auto& cmd = check_size_t<G_SendGuildCard_V3_6x06>(data, size); const auto& cmd = check_size_t<G_SendGuildCard_XB_6x06>(data, size);
c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language()); c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language());
break; break;
} }
@@ -882,6 +907,7 @@ static void on_buy_shop_item(shared_ptr<Client> c, uint8_t command, uint8_t flag
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto p = c->game_data.player(); auto p = c->game_data.player();
ItemData item = cmd.item_data; ItemData item = cmd.item_data;
item.data2d = 0; // Clear the price field
item.decode_for_version(c->version()); item.decode_for_version(c->version());
l->on_item_id_generated_externally(item.id); l->on_item_id_generated_externally(item.id);
p->add_item(item); p->add_item(item);
+6 -3
View File
@@ -273,9 +273,12 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
auto& mask = check_size_t<S_JoinGame_PC_64>(mask_data, mask_size); auto& mask = check_size_t<S_JoinGame_PC_64>(mask_data, mask_size);
mask.variations.clear(0); mask.variations.clear(0);
mask.rare_seed = 0; mask.rare_seed = 0;
} else { // V3 } else if (version == GameVersion::XB) {
auto& mask = check_size_t<S_JoinGame_DC_64>( auto& mask = check_size_t<S_JoinGame_XB_64>(mask_data, mask_size);
mask_data, mask_size, sizeof(S_JoinGame_GC_Ep3_64)); mask.variations.clear(0);
mask.rare_seed = 0;
} else if (version == GameVersion::DC || version == GameVersion::GC) {
auto& mask = check_size_t<S_JoinGame_DC_64>(mask_data, mask_size, sizeof(S_JoinGame_GC_Ep3_64));
mask.variations.clear(0); mask.variations.clear(0);
mask.rare_seed = 0; mask.rare_seed = 0;
for (size_t offset = sizeof(S_JoinGame_GC_64) + for (size_t offset = sizeof(S_JoinGame_GC_64) +
+8 -8
View File
@@ -256,7 +256,7 @@ struct PSOGCCharacterFile {
/* 0460:0044 */ parray<parray<uint8_t, 0x80>, 4> quest_flags; /* 0460:0044 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 0660:0244 */ be_uint32_t death_count; /* 0660:0244 */ be_uint32_t death_count;
/* 0664:0248 */ PlayerBank bank; /* 0664:0248 */ PlayerBank bank;
/* 192C:1510 */ GuildCardV3 guild_card; /* 192C:1510 */ GuildCardGC guild_card;
/* 19BC:15A0 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats; /* 19BC:15A0 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats;
/* 1DDC:19C0 */ parray<PSOGCSaveFileChatShortcutEntry, 20> chat_shortcuts; /* 1DDC:19C0 */ parray<PSOGCSaveFileChatShortcutEntry, 20> chat_shortcuts;
/* 246C:2050 */ pstring<TextEncoding::SJIS, 0xAC> auto_reply; /* 246C:2050 */ pstring<TextEncoding::SJIS, 0xAC> auto_reply;
@@ -313,7 +313,7 @@ struct PSOGCEp3CharacterFile {
/* 0864:0448 */ be_uint32_t num_bank_items; /* 0864:0448 */ be_uint32_t num_bank_items;
/* 0868:044C */ be_uint32_t bank_meseta; /* 0868:044C */ be_uint32_t bank_meseta;
/* 086C:0450 */ parray<PlayerBankItem, 4> bank_items; /* 086C:0450 */ parray<PlayerBankItem, 4> bank_items;
/* 08CC:04B0 */ GuildCardV3 guild_card; /* 08CC:04B0 */ GuildCardGC guild_card;
/* 095C:0540 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats; /* 095C:0540 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats;
/* 0D7C:0960 */ parray<PSOGCSaveFileChatShortcutEntry, 20> chat_shortcuts; /* 0D7C:0960 */ parray<PSOGCSaveFileChatShortcutEntry, 20> chat_shortcuts;
/* 140C:0FF0 */ pstring<TextEncoding::SJIS, 0xAC> auto_reply; /* 140C:0FF0 */ pstring<TextEncoding::SJIS, 0xAC> auto_reply;
@@ -356,12 +356,12 @@ struct PSOGCGuildCardFile {
/* 0000 */ be_uint32_t checksum; /* 0000 */ be_uint32_t checksum;
/* 0004 */ parray<uint8_t, 0xC0> unknown_a1; /* 0004 */ parray<uint8_t, 0xC0> unknown_a1;
struct GuildCardBE { struct GuildCardBE {
// Note: This struct (up through offset 0x90) is identical to GuildCardV3 // Note: This struct (up through offset 0x90) is identical to GuildCardGC
// except for 32-bit fields, which are big-endian here. // except for the 32-bit fields, which are big-endian here.
/* 0000 */ be_uint32_t player_tag; // == 0x00000001 (not 0x00010000) /* 0000 */ be_uint32_t player_tag; // == 0x00000001 (not 0x00010000)
/* 0004 */ be_uint32_t guild_card_number; /* 0004 */ be_uint32_t guild_card_number;
/* 0008 */ pstring<TextEncoding::SJIS, 0x18> name; /* 0008 */ pstring<TextEncoding::ASCII, 0x18> name;
/* 0020 */ pstring<TextEncoding::SJIS, 0x6C> description; /* 0020 */ pstring<TextEncoding::MARKED, 0x6C> description;
/* 008C */ uint8_t present; /* 008C */ uint8_t present;
/* 008D */ uint8_t language; /* 008D */ uint8_t language;
/* 008E */ uint8_t section_id; /* 008E */ uint8_t section_id;
@@ -374,7 +374,7 @@ struct PSOGCGuildCardFile {
/* 0091 */ uint8_t unknown_a2; /* 0091 */ uint8_t unknown_a2;
/* 0092 */ uint8_t unknown_a3; /* 0092 */ uint8_t unknown_a3;
/* 0093 */ uint8_t unknown_a4; /* 0093 */ uint8_t unknown_a4;
/* 0094 */ pstring<TextEncoding::SJIS, 0x6C> comment; /* 0094 */ pstring<TextEncoding::MARKED, 0x6C> comment;
/* 0100 */ /* 0100 */
} __attribute__((packed)); } __attribute__((packed));
/* 00C4 */ parray<GuildCardEntry, 0xD2> entries; /* 00C4 */ parray<GuildCardEntry, 0xD2> entries;
@@ -396,7 +396,7 @@ struct PSOGCSnapshotFile {
/* 1800A */ be_int16_t max_players; /* 1800A */ be_int16_t max_players;
/* 1800C */ parray<be_uint32_t, 12> players_present; /* 1800C */ parray<be_uint32_t, 12> players_present;
/* 1803C */ parray<be_uint32_t, 12> player_levels; /* 1803C */ parray<be_uint32_t, 12> player_levels;
/* 1806C */ parray<pstring<TextEncoding::SJIS, 0x18>, 12> player_names; /* 1806C */ parray<pstring<TextEncoding::ASCII, 0x18>, 12> player_names;
/* 1818C */ /* 1818C */
bool checksum_correct() const; bool checksum_correct() const;
+215 -70
View File
@@ -163,14 +163,17 @@ void send_server_init_dc_pc_v3(shared_ptr<Client> c, uint8_t flags) {
c->channel.crypt_out.reset(new PSOV2Encryption(server_key)); c->channel.crypt_out.reset(new PSOV2Encryption(server_key));
break; break;
case GameVersion::DC: case GameVersion::DC:
case GameVersion::GC: case GameVersion::GC: {
case GameVersion::XB: {
shared_ptr<PSOV2OrV3DetectorEncryption> det_crypt(new PSOV2OrV3DetectorEncryption( shared_ptr<PSOV2OrV3DetectorEncryption> det_crypt(new PSOV2OrV3DetectorEncryption(
client_key, v2_crypt_initial_client_commands, v3_crypt_initial_client_commands)); client_key, v2_crypt_initial_client_commands, v3_crypt_initial_client_commands));
c->channel.crypt_in = det_crypt; c->channel.crypt_in = det_crypt;
c->channel.crypt_out.reset(new PSOV2OrV3ImitatorEncryption(server_key, det_crypt)); c->channel.crypt_out.reset(new PSOV2OrV3ImitatorEncryption(server_key, det_crypt));
break; break;
} }
case GameVersion::XB:
c->channel.crypt_in.reset(new PSOV3Encryption(client_key));
c->channel.crypt_out.reset(new PSOV3Encryption(server_key));
break;
default: default:
throw invalid_argument("incorrect client version"); throw invalid_argument("incorrect client version");
} }
@@ -273,44 +276,6 @@ void send_update_client_config(shared_ptr<Client> c) {
} }
} }
template <typename CommandT>
void send_quest_open_file_t(
shared_ptr<Client> c,
const string& quest_name,
const string& filename,
uint32_t file_size,
QuestFileType type) {
CommandT cmd;
uint8_t command_num;
switch (type) {
case QuestFileType::ONLINE:
command_num = 0x44;
cmd.name.encode("PSO/" + quest_name);
cmd.type = 0;
break;
case QuestFileType::GBA_DEMO:
command_num = 0xA6;
cmd.name.encode("GBA Demo");
cmd.type = 2;
break;
case QuestFileType::DOWNLOAD:
command_num = 0xA6;
cmd.name.encode("PSO/" + quest_name);
cmd.type = 0;
break;
case QuestFileType::EPISODE_3:
command_num = 0xA6;
cmd.name.encode("PSO/" + quest_name);
cmd.type = 3;
break;
default:
throw logic_error("invalid quest file type");
}
cmd.file_size = file_size;
cmd.filename.encode(filename);
send_command_t(c, command_num, 0x00, cmd);
}
void send_quest_buffer_overflow(shared_ptr<Client> c) { void send_quest_buffer_overflow(shared_ptr<Client> c) {
// PSO Episode 3 USA doesn't natively support the B2 command, but we can add // PSO Episode 3 USA doesn't natively support the B2 command, but we can add
// it back to the game with some tricky commands. For details on how this // it back to the game with some tricky commands. For details on how this
@@ -320,18 +285,21 @@ void send_quest_buffer_overflow(shared_ptr<Client> c) {
throw runtime_error("Episode 3 buffer overflow code must be a single segment"); throw runtime_error("Episode 3 buffer overflow code must be a single segment");
} }
static const string filename = "m999999p_e.bin"; S_OpenFile_PC_GC_44_A6 open_cmd;
send_quest_open_file_t<S_OpenFile_PC_GC_44_A6>( open_cmd.name.encode("PSO/BufferOverflow");
c, "BufferOverflow", filename, 0x18, QuestFileType::EPISODE_3); open_cmd.type = 3;
open_cmd.file_size = 0x18;
open_cmd.filename.encode("m999999p_e.bin");
send_command_t(c, 0xA6, 0x00, open_cmd);
S_WriteFile_13_A7 cmd; S_WriteFile_13_A7 write_cmd;
cmd.filename.encode(filename); write_cmd.filename.encode("m999999p_e.bin");
memcpy(cmd.data.data(), fn->code.data(), fn->code.size()); memcpy(write_cmd.data.data(), fn->code.data(), fn->code.size());
if (fn->code.size() < 0x400) { if (fn->code.size() < 0x400) {
memset(&cmd.data[fn->code.size()], 0, 0x400 - fn->code.size()); memset(&write_cmd.data[fn->code.size()], 0, 0x400 - fn->code.size());
} }
cmd.data_size = fn->code.size(); write_cmd.data_size = fn->code.size();
send_command_t(c, 0xA7, 0x00, cmd); send_command_t(c, 0xA7, 0x00, write_cmd);
} }
void empty_function_call_response_handler(uint32_t, uint32_t) {} void empty_function_call_response_handler(uint32_t, uint32_t) {}
@@ -1054,7 +1022,7 @@ void send_card_search_result(
} }
template <typename CmdT> template <typename CmdT>
void send_guild_card_dc_pc_v3_t( void send_guild_card_dc_pc_gc_t(
Channel& ch, Channel& ch,
uint32_t guild_card_number, uint32_t guild_card_number,
const string& name, const string& name,
@@ -1077,6 +1045,32 @@ void send_guild_card_dc_pc_v3_t(
ch.send(0x60, 0x00, &cmd, sizeof(cmd)); ch.send(0x60, 0x00, &cmd, sizeof(cmd));
} }
void send_guild_card_xb(
Channel& ch,
uint32_t guild_card_number,
uint64_t xb_user_id,
const string& name,
const string& description,
uint8_t language,
uint8_t section_id,
uint8_t char_class) {
G_SendGuildCard_XB_6x06 cmd;
cmd.header.subcommand = 0x06;
cmd.header.size = sizeof(G_SendGuildCard_XB_6x06) / 4;
cmd.header.unused = 0x0000;
cmd.guild_card.player_tag = 0x00010000;
cmd.guild_card.guild_card_number = guild_card_number;
cmd.guild_card.xb_user_id_high = (xb_user_id >> 32) & 0xFFFFFFFF;
cmd.guild_card.xb_user_id_low = xb_user_id & 0xFFFFFFFF;
cmd.guild_card.name.encode(name, ch.language);
cmd.guild_card.description.encode(description, ch.language);
cmd.guild_card.present = 1;
cmd.guild_card.language = language;
cmd.guild_card.section_id = section_id;
cmd.guild_card.char_class = char_class;
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
}
static void send_guild_card_bb( static void send_guild_card_bb(
Channel& ch, Channel& ch,
uint32_t guild_card_number, uint32_t guild_card_number,
@@ -1104,6 +1098,7 @@ static void send_guild_card_bb(
void send_guild_card( void send_guild_card(
Channel& ch, Channel& ch,
uint32_t guild_card_number, uint32_t guild_card_number,
uint64_t xb_user_id,
const string& name, const string& name,
const string& team_name, const string& team_name,
const string& description, const string& description,
@@ -1112,18 +1107,21 @@ void send_guild_card(
uint8_t char_class) { uint8_t char_class) {
switch (ch.version) { switch (ch.version) {
case GameVersion::DC: case GameVersion::DC:
send_guild_card_dc_pc_v3_t<G_SendGuildCard_DC_6x06>( send_guild_card_dc_pc_gc_t<G_SendGuildCard_DC_6x06>(
ch, guild_card_number, name, description, language, section_id, char_class); ch, guild_card_number, name, description, language, section_id, char_class);
break; break;
case GameVersion::PC: case GameVersion::PC:
send_guild_card_dc_pc_v3_t<G_SendGuildCard_PC_6x06>( send_guild_card_dc_pc_gc_t<G_SendGuildCard_PC_6x06>(
ch, guild_card_number, name, description, language, section_id, char_class); ch, guild_card_number, name, description, language, section_id, char_class);
break; break;
case GameVersion::GC: case GameVersion::GC:
case GameVersion::XB: send_guild_card_dc_pc_gc_t<G_SendGuildCard_GC_6x06>(
send_guild_card_dc_pc_v3_t<G_SendGuildCard_V3_6x06>(
ch, guild_card_number, name, description, language, section_id, char_class); ch, guild_card_number, name, description, language, section_id, char_class);
break; break;
case GameVersion::XB:
send_guild_card_xb(
ch, guild_card_number, xb_user_id, name, description, language, section_id, char_class);
break;
case GameVersion::BB: case GameVersion::BB:
send_guild_card_bb(ch, guild_card_number, name, team_name, description, language, section_id, char_class); send_guild_card_bb(ch, guild_card_number, name, team_name, description, language, section_id, char_class);
break; break;
@@ -1139,13 +1137,16 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
auto source_p = source->game_data.player(true, false); auto source_p = source->game_data.player(true, false);
uint32_t guild_card_number = source->license->serial_number; uint32_t guild_card_number = source->license->serial_number;
uint64_t xb_user_id = source->license->xb_user_id
? source->license->xb_user_id
: (0xAE00000000000000 | guild_card_number);
uint8_t language = source_p->inventory.language; uint8_t language = source_p->inventory.language;
string name = source_p->disp.name.decode(language); string name = source_p->disp.name.decode(language);
string description = source_p->guild_card_description.decode(language); string description = source_p->guild_card_description.decode(language);
uint8_t section_id = source_p->disp.visual.section_id; uint8_t section_id = source_p->disp.visual.section_id;
uint8_t char_class = source_p->disp.visual.char_class; uint8_t char_class = source_p->disp.visual.char_class;
send_guild_card(c->channel, guild_card_number, name, "", description, language, section_id, char_class); send_guild_card(c->channel, guild_card_number, xb_user_id, name, "", description, language, section_id, char_class);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -1694,6 +1695,16 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
case GameVersion::XB: { case GameVersion::XB: {
S_JoinGame_XB_64 cmd; S_JoinGame_XB_64 cmd;
size_t player_count = populate_v3_cmd(cmd); size_t player_count = populate_v3_cmd(cmd);
for (size_t x = 0; x < 4; x++) {
auto lc = l->clients[x];
if (lc) {
if (lc->xb_netloc) {
cmd.lobby_data[x].netloc = *lc->xb_netloc;
} else {
cmd.lobby_data[x].netloc.account_id = 0xAE00000000000000 | lc->license->serial_number;
}
}
}
send_command_t(c, 0x64, player_count, cmd); send_command_t(c, 0x64, player_count, cmd);
break; break;
} }
@@ -1802,6 +1813,79 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
send_command(c, command, used_entries, &cmd, cmd.size(used_entries)); send_command(c, command, used_entries, &cmd, cmd.size(used_entries));
} }
void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client = nullptr) {
auto s = c->require_server_state();
uint8_t command;
if (l->is_game()) {
if (joining_client) {
command = 0x65;
} else {
throw logic_error("send_join_lobby_xb should not be used for primary game join command");
}
} else {
command = joining_client ? 0x68 : 0x67;
}
send_player_records_t<PlayerRecordsEntry_V3>(c, l, joining_client);
uint8_t lobby_type;
if (c->config.override_lobby_number != 0x80) {
lobby_type = c->config.override_lobby_number;
} else if (l->check_flag(Lobby::Flag::IS_OVERFLOW)) {
lobby_type = c->config.check_flag(Client::Flag::IS_EPISODE_3) ? 15 : 0;
} else {
lobby_type = l->block - 1;
}
if ((lobby_type > 0x11) && (lobby_type != 0x67) && (lobby_type != 0xD4) && (lobby_type < 0xFC)) {
lobby_type = l->block - 1;
}
S_JoinLobby_XB_65_67_68 cmd;
cmd.lobby_flags.client_id = c->lobby_client_id;
cmd.lobby_flags.leader_id = l->leader_id;
cmd.lobby_flags.disable_udp = 0x01;
cmd.lobby_flags.lobby_number = lobby_type;
cmd.lobby_flags.block_number = l->block;
cmd.lobby_flags.unknown_a1 = 0;
cmd.lobby_flags.event = l->event;
cmd.lobby_flags.unknown_a2 = 0;
cmd.lobby_flags.unused = 0;
vector<shared_ptr<Client>> lobby_clients;
if (joining_client) {
lobby_clients.emplace_back(joining_client);
} else {
for (auto lc : l->clients) {
if (lc) {
lobby_clients.emplace_back(lc);
}
}
}
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.player();
auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card_number = lc->license->serial_number;
if (lc->xb_netloc) {
e.lobby_data.netloc = *lc->xb_netloc;
} else {
e.lobby_data.netloc.account_id = 0xAE00000000000000 | lc->license->serial_number;
}
e.lobby_data.client_id = lc->lobby_client_id;
e.lobby_data.name.encode(lp->disp.name.decode(lp->inventory.language), c->language());
e.inventory = lp->inventory;
e.inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(lp->disp, c->language(), lp->inventory.language);
e.disp.enforce_lobby_join_limits(c->version());
}
send_command(c, command, used_entries, &cmd, cmd.size(used_entries));
}
void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l, void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
shared_ptr<Client> joining_client = nullptr) { shared_ptr<Client> joining_client = nullptr) {
uint8_t command; uint8_t command;
@@ -1869,7 +1953,7 @@ void send_join_lobby(shared_ptr<Client> c, shared_ptr<Lobby> l) {
send_join_lobby_t<PlayerLobbyDataDCGC, PlayerDispDataDCPCV3, PlayerRecordsEntry_V3, false>(c, l); send_join_lobby_t<PlayerLobbyDataDCGC, PlayerDispDataDCPCV3, PlayerRecordsEntry_V3, false>(c, l);
break; break;
case GameVersion::XB: case GameVersion::XB:
send_join_lobby_t<PlayerLobbyDataXB, PlayerDispDataDCPCV3, PlayerRecordsEntry_V3, false>(c, l); send_join_lobby_xb(c, l);
break; break;
case GameVersion::BB: case GameVersion::BB:
send_join_lobby_t<PlayerLobbyDataBB, PlayerDispDataBB, PlayerRecordsEntry_BB, true>(c, l); send_join_lobby_t<PlayerLobbyDataBB, PlayerDispDataBB, PlayerRecordsEntry_BB, true>(c, l);
@@ -1904,7 +1988,7 @@ void send_player_join_notification(shared_ptr<Client> c,
send_join_lobby_t<PlayerLobbyDataDCGC, PlayerDispDataDCPCV3, PlayerRecordsEntry_V3, false>(c, l, joining_client); send_join_lobby_t<PlayerLobbyDataDCGC, PlayerDispDataDCPCV3, PlayerRecordsEntry_V3, false>(c, l, joining_client);
break; break;
case GameVersion::XB: case GameVersion::XB:
send_join_lobby_t<PlayerLobbyDataXB, PlayerDispDataDCPCV3, PlayerRecordsEntry_V3, false>(c, l, joining_client); send_join_lobby_xb(c, l, joining_client);
break; break;
case GameVersion::BB: case GameVersion::BB:
send_join_lobby_t<PlayerLobbyDataBB, PlayerDispDataBB, PlayerRecordsEntry_BB, true>(c, l, joining_client); send_join_lobby_t<PlayerLobbyDataBB, PlayerDispDataBB, PlayerRecordsEntry_BB, true>(c, l, joining_client);
@@ -2830,26 +2914,87 @@ void send_quest_file_chunk(
send_command_t(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd); send_command_t(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd);
} }
void send_open_quest_file(shared_ptr<Client> c, const string& quest_name, template <typename CommandT>
const string& basename, shared_ptr<const string> contents, QuestFileType type) { void send_open_quest_file_t(
shared_ptr<Client> c,
const string& quest_name,
const string& filename,
const string&,
uint32_t file_size,
uint32_t,
QuestFileType type) {
CommandT cmd;
uint8_t command_num;
switch (type) {
case QuestFileType::ONLINE:
command_num = 0x44;
cmd.name.encode("PSO/" + quest_name);
cmd.type = 0;
break;
case QuestFileType::GBA_DEMO:
command_num = 0xA6;
cmd.name.encode("GBA Demo");
cmd.type = 2;
break;
case QuestFileType::DOWNLOAD:
command_num = 0xA6;
cmd.name.encode("PSO/" + quest_name);
cmd.type = 0;
break;
case QuestFileType::EPISODE_3:
command_num = 0xA6;
cmd.name.encode("PSO/" + quest_name);
cmd.type = 3;
break;
default:
throw logic_error("invalid quest file type");
}
cmd.file_size = file_size;
cmd.filename.encode(filename);
send_command_t(c, command_num, 0x00, cmd);
}
template <>
void send_open_quest_file_t<S_OpenFile_XB_44_A6>(
shared_ptr<Client> c,
const string& quest_name,
const string& filename,
const string& xb_filename,
uint32_t file_size,
uint32_t quest_number,
QuestFileType type) {
S_OpenFile_XB_44_A6 cmd;
cmd.name.encode("PSO/" + quest_name);
cmd.type = 0;
cmd.file_size = file_size;
cmd.filename.encode(filename);
cmd.xb_filename.encode(xb_filename);
cmd.content_meta = 0x30000000 | quest_number;
send_command_t(c, (type == QuestFileType::ONLINE) ? 0x44 : 0xA6, 0x00, cmd);
}
void send_open_quest_file(
shared_ptr<Client> c,
const string& quest_name,
const string& filename,
const string& xb_filename,
uint32_t quest_number,
QuestFileType type,
shared_ptr<const string> contents) {
switch (c->version()) { switch (c->version()) {
case GameVersion::DC: case GameVersion::DC:
send_quest_open_file_t<S_OpenFile_DC_44_A6>( send_open_quest_file_t<S_OpenFile_DC_44_A6>(c, quest_name, filename, xb_filename, contents->size(), quest_number, type);
c, quest_name, basename, contents->size(), type);
break; break;
case GameVersion::PC: case GameVersion::PC:
case GameVersion::GC: case GameVersion::GC:
send_quest_open_file_t<S_OpenFile_PC_GC_44_A6>( send_open_quest_file_t<S_OpenFile_PC_GC_44_A6>(c, quest_name, filename, xb_filename, contents->size(), quest_number, type);
c, quest_name, basename, contents->size(), type);
break; break;
case GameVersion::XB: case GameVersion::XB:
send_quest_open_file_t<S_OpenFile_XB_44_A6>( send_open_quest_file_t<S_OpenFile_XB_44_A6>(c, quest_name, filename, xb_filename, contents->size(), quest_number, type);
c, quest_name, basename, contents->size(), type);
break; break;
case GameVersion::BB: case GameVersion::BB:
send_quest_open_file_t<S_OpenFile_BB_44_A6>( send_open_quest_file_t<S_OpenFile_BB_44_A6>(c, quest_name, filename, xb_filename, contents->size(), quest_number, type);
c, quest_name, basename, contents->size(), type);
break; break;
default: default:
throw logic_error("cannot send quest files to this version of client"); throw logic_error("cannot send quest files to this version of client");
@@ -2863,12 +3008,12 @@ void send_open_quest_file(shared_ptr<Client> c, const string& quest_name,
if (chunk_bytes > 0x400) { if (chunk_bytes > 0x400) {
chunk_bytes = 0x400; chunk_bytes = 0x400;
} }
send_quest_file_chunk(c, basename.c_str(), offset / 0x400, send_quest_file_chunk(c, filename.c_str(), offset / 0x400,
contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE)); contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
} }
} else { } else {
c->sending_files.emplace(basename, contents); c->sending_files.emplace(filename, contents);
c->log.info("Opened file %s", basename.c_str()); c->log.info("Opened file %s", filename.c_str());
} }
} }
+6 -3
View File
@@ -242,6 +242,7 @@ void send_card_search_result(
void send_guild_card( void send_guild_card(
Channel& ch, Channel& ch,
uint32_t guild_card_number, uint32_t guild_card_number,
uint64_t xb_user_id,
const std::string& name, const std::string& name,
const std::string& team_name, const std::string& team_name,
const std::string& description, const std::string& description,
@@ -362,9 +363,11 @@ enum class QuestFileType {
void send_open_quest_file( void send_open_quest_file(
std::shared_ptr<Client> c, std::shared_ptr<Client> c,
const std::string& quest_name, const std::string& quest_name,
const std::string& basename, const std::string& filename,
std::shared_ptr<const std::string> contents, const std::string& xb_filename,
QuestFileType type); uint32_t quest_number,
QuestFileType type,
std::shared_ptr<const std::string> contents);
void send_quest_file_chunk( void send_quest_file_chunk(
std::shared_ptr<Client> c, std::shared_ptr<Client> c,
const std::string& filename, const std::string& filename,
+10 -6
View File
@@ -29,13 +29,11 @@ using namespace std::placeholders;
void Server::disconnect_client(shared_ptr<Client> c) { void Server::disconnect_client(shared_ptr<Client> c) {
if (c->channel.is_virtual_connection) { if (c->channel.is_virtual_connection) {
server_log.info( server_log.info("Client disconnected: C-%" PRIX64 " on virtual connection %p", c->id, c->channel.bev.get());
"Client disconnected: C-%" PRIX64 " on virtual connection %p", } else if (c->channel.bev) {
c->id, c->channel.bev.get()); server_log.info("Client disconnected: C-%" PRIX64 " on fd %d", c->id, bufferevent_getfd(c->channel.bev.get()));
} else { } else {
server_log.info( server_log.info("Client C-%" PRIX64 " removed from game server", c->id);
"Client disconnected: C-%" PRIX64 " on fd %d",
c->id, bufferevent_getfd(c->channel.bev.get()));
} }
this->state->channel_to_client.erase(&c->channel); this->state->channel_to_client.erase(&c->channel);
@@ -165,6 +163,12 @@ void Server::connect_client(
} }
} }
void Server::connect_client(shared_ptr<Client> c, Channel&& ch) {
c->channel.replace_with(std::move(ch), Server::on_client_input, Server::on_client_error, this, string_printf("C-%" PRIX64, c->id));
this->state->channel_to_client.emplace(&c->channel, c);
server_log.info("Client C-%" PRIX64 " added to game server", c->id);
}
void Server::on_listen_error(struct evconnlistener* listener) { void Server::on_listen_error(struct evconnlistener* listener) {
int err = EVUTIL_SOCKET_ERROR(); int err = EVUTIL_SOCKET_ERROR();
server_log.error("Failure on listening socket %d: %d (%s)", server_log.error("Failure on listening socket %d: %d (%s)",
+2 -2
View File
@@ -15,8 +15,7 @@ public:
Server() = delete; Server() = delete;
Server(const Server&) = delete; Server(const Server&) = delete;
Server(Server&&) = delete; Server(Server&&) = delete;
Server(std::shared_ptr<struct event_base> base, Server(std::shared_ptr<struct event_base> base, std::shared_ptr<ServerState> state);
std::shared_ptr<ServerState> state);
virtual ~Server() = default; virtual ~Server() = default;
void listen(const std::string& addr_str, const std::string& socket_path, GameVersion version, ServerBehavior initial_state); void listen(const std::string& addr_str, const std::string& socket_path, GameVersion version, ServerBehavior initial_state);
@@ -27,6 +26,7 @@ public:
void connect_client(struct bufferevent* bev, uint32_t address, void connect_client(struct bufferevent* bev, uint32_t address,
uint16_t client_port, uint16_t server_port, uint16_t client_port, uint16_t server_port,
GameVersion version, ServerBehavior initial_state); GameVersion version, ServerBehavior initial_state);
void connect_client(std::shared_ptr<Client> c, Channel&& ch);
void disconnect_client(std::shared_ptr<Client> c); void disconnect_client(std::shared_ptr<Client> c);
std::shared_ptr<Client> get_client() const; std::shared_ptr<Client> get_client() const;
+17 -34
View File
@@ -141,6 +141,9 @@ Server commands:\n\
Add a license to the server. <parameters> is some subset of the following:\n\ Add a license to the server. <parameters> is some subset of the following:\n\
bb-username=<username> (BB username)\n\ bb-username=<username> (BB username)\n\
bb-password=<password> (BB password)\n\ bb-password=<password> (BB password)\n\
xb-gamertag=<gamertag> (Xbox gamertag)\n\
xb-user-id=<user-id> (Xbox user ID)\n\
xb-account-id=<account-id> (Xbox account ID)\n\
gc-password=<password> (GC password)\n\ gc-password=<password> (GC password)\n\
access-key=<access-key> (DC/GC/PC access key)\n\ access-key=<access-key> (DC/GC/PC access key)\n\
serial=<serial-number> (decimal serial number; required for all licenses)\n\ serial=<serial-number> (decimal serial number; required for all licenses)\n\
@@ -319,31 +322,21 @@ Proxy session commands:\n\
for (const string& token : split(command_args, ' ')) { for (const string& token : split(command_args, ' ')) {
if (starts_with(token, "bb-username=")) { if (starts_with(token, "bb-username=")) {
if (token.size() >= 32) {
throw invalid_argument("username too long");
}
l->bb_username = token.substr(12); l->bb_username = token.substr(12);
} else if (starts_with(token, "bb-password=")) { } else if (starts_with(token, "bb-password=")) {
if (token.size() >= 32) {
throw invalid_argument("bb-password too long");
}
l->bb_password = token.substr(12); l->bb_password = token.substr(12);
} else if (starts_with(token, "xb-gamertag=")) {
l->xb_gamertag = token.substr(12);
} else if (starts_with(token, "xb-user-id=")) {
l->xb_user_id = stoull(token.substr(11), nullptr, 16);
} else if (starts_with(token, "xb-account-id=")) {
l->xb_account_id = stoull(token.substr(14), nullptr, 16);
} else if (starts_with(token, "gc-password=")) { } else if (starts_with(token, "gc-password=")) {
if (token.size() > 20) {
throw invalid_argument("gc-password too long");
}
l->gc_password = token.substr(12); l->gc_password = token.substr(12);
} else if (starts_with(token, "access-key=")) { } else if (starts_with(token, "access-key=")) {
if (token.size() > 23) {
throw invalid_argument("access-key is too long");
}
l->access_key = token.substr(11); l->access_key = token.substr(11);
} else if (starts_with(token, "serial=")) { } else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7)); l->serial_number = stoul(token.substr(7), nullptr, 0);
} else if (starts_with(token, "flags=")) { } else if (starts_with(token, "flags=")) {
string mask = token.substr(6); string mask = token.substr(6);
@@ -386,31 +379,21 @@ Proxy session commands:\n\
try { try {
for (const string& token : tokens) { for (const string& token : tokens) {
if (starts_with(token, "bb-username=")) { if (starts_with(token, "bb-username=")) {
if (token.size() >= 32) {
throw invalid_argument("username too long");
}
l->bb_username = token.substr(12); l->bb_username = token.substr(12);
} else if (starts_with(token, "bb-password=")) { } else if (starts_with(token, "bb-password=")) {
if (token.size() >= 32) {
throw invalid_argument("bb-password too long");
}
l->bb_password = token.substr(12); l->bb_password = token.substr(12);
} else if (starts_with(token, "xb-gamertag=")) {
l->xb_gamertag = token.substr(12);
} else if (starts_with(token, "xb-user-id=")) {
l->xb_user_id = stoull(token.substr(11), nullptr, 16);
} else if (starts_with(token, "xb-account-id=")) {
l->xb_account_id = stoull(token.substr(14), nullptr, 16);
} else if (starts_with(token, "gc-password=")) { } else if (starts_with(token, "gc-password=")) {
if (token.size() > 20) {
throw invalid_argument("gc-password too long");
}
l->gc_password = token.substr(12); l->gc_password = token.substr(12);
} else if (starts_with(token, "access-key=")) { } else if (starts_with(token, "access-key=")) {
if (token.size() > 23) {
throw invalid_argument("access-key is too long");
}
l->access_key = token.substr(11); l->access_key = token.substr(11);
} else if (starts_with(token, "serial=")) { } else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7)); l->serial_number = stoul(token.substr(7), nullptr, 0);
} else if (starts_with(token, "flags=")) { } else if (starts_with(token, "flags=")) {
string mask = token.substr(6); string mask = token.substr(6);
+1 -1
View File
@@ -13,7 +13,7 @@ const vector<string> version_to_login_port_name = {
const vector<string> version_to_lobby_port_name = { const vector<string> version_to_lobby_port_name = {
"bb-patch", "console-lobby", "pc-lobby", "console-lobby", "console-lobby", "bb-lobby"}; "bb-patch", "console-lobby", "pc-lobby", "console-lobby", "console-lobby", "bb-lobby"};
const vector<string> version_to_proxy_port_name = { const vector<string> version_to_proxy_port_name = {
"", "dc-proxy", "pc-proxy", "gc-proxy", "xb-proxy", "bb-proxy"}; "", "dc-proxy", "pc-proxy", "gc-proxy", "xb", "bb-proxy"};
const char* name_for_version(GameVersion version) { const char* name_for_version(GameVersion version) {
switch (version) { switch (version) {
+1
View File
@@ -0,0 +1 @@
b88001-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88001-gc-f.bin
+1
View File
@@ -0,0 +1 @@
b88001-gc-g.bin
+1
View File
@@ -0,0 +1 @@
b88001-gc-j.bin
+1
View File
@@ -0,0 +1 @@
b88001-gc-s.bin
+1
View File
@@ -0,0 +1 @@
b88001-gc.dat
+1
View File
@@ -0,0 +1 @@
b88002-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88002-gc-f.bin
+1
View File
@@ -0,0 +1 @@
b88002-gc-g.bin
+1
View File
@@ -0,0 +1 @@
b88002-gc-j.bin
+1
View File
@@ -0,0 +1 @@
b88002-gc-s.bin
+1
View File
@@ -0,0 +1 @@
b88002-gc.dat
+1
View File
@@ -0,0 +1 @@
b88003-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88003-gc-f.bin
+1
View File
@@ -0,0 +1 @@
b88003-gc-g.bin
+1
View File
@@ -0,0 +1 @@
b88003-gc-j.bin
+1
View File
@@ -0,0 +1 @@
b88003-gc-s.bin
+1
View File
@@ -0,0 +1 @@
b88003-gc.dat
+1
View File
@@ -0,0 +1 @@
b88004-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88004-gc-f.bin
+1
View File
@@ -0,0 +1 @@
b88004-gc-g.bin
+1
View File
@@ -0,0 +1 @@
b88004-gc-j.bin
+1
View File
@@ -0,0 +1 @@
b88004-gc-s.bin
+1
View File
@@ -0,0 +1 @@
b88004-gc.dat
+1
View File
@@ -0,0 +1 @@
b88005-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88005-gc-f.bin
+1
View File
@@ -0,0 +1 @@
b88005-gc-g.bin
+1
View File
@@ -0,0 +1 @@
b88005-gc-j.bin
+1
View File
@@ -0,0 +1 @@
b88005-gc-s.bin
+1
View File
@@ -0,0 +1 @@
b88005-gc.dat
+1
View File
@@ -0,0 +1 @@
b88006-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88006-gc-f.bin
+1
View File
@@ -0,0 +1 @@
b88006-gc-g.bin
+1
View File
@@ -0,0 +1 @@
b88006-gc-j.bin
+1
View File
@@ -0,0 +1 @@
b88006-gc-s.bin
+1
View File
@@ -0,0 +1 @@
b88006-gc.dat
+1
View File
@@ -0,0 +1 @@
b88007-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88007-gc.dat
+1
View File
@@ -0,0 +1 @@
b88008-gc-e.bin
+1
View File
@@ -0,0 +1 @@
b88008-gc.dat
+1
View File
@@ -0,0 +1 @@
c88101-gc-e.bin
+1
View File
@@ -0,0 +1 @@
c88101-gc-f.bin
+1
View File
@@ -0,0 +1 @@
c88101-gc-g.bin
+1
View File
@@ -0,0 +1 @@
c88101-gc-j.bin
+1
View File
@@ -0,0 +1 @@
c88101-gc-s.bin
+1
View File
@@ -0,0 +1 @@
c88101-gc.dat
+1
View File
@@ -0,0 +1 @@
c88102-gc-e.bin
+1
View File
@@ -0,0 +1 @@
c88102-gc-f.bin
+1
View File
@@ -0,0 +1 @@
c88102-gc-g.bin
+1
View File
@@ -0,0 +1 @@
c88102-gc-j.bin
+1
View File
@@ -0,0 +1 @@
c88102-gc-s.bin
+1
View File
@@ -0,0 +1 @@
c88102-gc.dat
+1
View File
@@ -0,0 +1 @@
c88103-gc-e.bin
+1
View File
@@ -0,0 +1 @@
c88103-gc-f.bin
+1
View File
@@ -0,0 +1 @@
c88103-gc-g.bin
+1
View File
@@ -0,0 +1 @@
c88103-gc-j.bin
+1
View File
@@ -0,0 +1 @@
c88103-gc-s.bin
+1
View File
@@ -0,0 +1 @@
c88103-gc.dat
+1
View File
@@ -0,0 +1 @@
c88104-gc-e.bin
+1
View File
@@ -0,0 +1 @@
c88104-gc-f.bin
+1
View File
@@ -0,0 +1 @@
c88104-gc-g.bin
+1
View File
@@ -0,0 +1 @@
c88104-gc-j.bin
+1
View File
@@ -0,0 +1 @@
c88104-gc-s.bin
+1
View File
@@ -0,0 +1 @@
c88104-gc.dat
+1
View File
@@ -0,0 +1 @@
c88105-gc-e.bin
+1
View File
@@ -0,0 +1 @@
c88105-gc-f.bin
+1
View File
@@ -0,0 +1 @@
c88105-gc-g.bin
+1
View File
@@ -0,0 +1 @@
c88105-gc-j.bin
+1
View File
@@ -0,0 +1 @@
c88105-gc-s.bin
+1
View File
@@ -0,0 +1 @@
c88105-gc.dat
+1
View File
@@ -0,0 +1 @@
c88106-gc-e.bin
+1
View File
@@ -0,0 +1 @@
c88106-gc-f.bin
+1
View File
@@ -0,0 +1 @@
c88106-gc-g.bin

Some files were not shown because too many files have changed in this diff Show More