fix various ep3 lobby bugs
This commit is contained in:
@@ -15,6 +15,10 @@ using namespace std;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Client::Client(
|
Client::Client(
|
||||||
struct bufferevent* bev,
|
struct bufferevent* bev,
|
||||||
GameVersion version,
|
GameVersion version,
|
||||||
@@ -61,3 +65,27 @@ bool Client::send(string&& data) {
|
|||||||
evbuffer_add(buf, data.data(), data.size());
|
evbuffer_add(buf, data.data(), data.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientConfig Client::export_config() const {
|
||||||
|
ClientConfig cc;
|
||||||
|
cc.magic = CLIENT_CONFIG_MAGIC;
|
||||||
|
cc.bb_game_state = this->bb_game_state;
|
||||||
|
cc.bb_player_index = this->bb_player_index;
|
||||||
|
cc.flags = this->flags;
|
||||||
|
for (size_t x = 0; x < 5; x++) {
|
||||||
|
cc.unused[x] = 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
for (size_t x = 0; x < 2; x++) {
|
||||||
|
cc.unused_bb_only[x] = 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
return cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::import_config(const ClientConfig& cc) {
|
||||||
|
if (cc.magic != CLIENT_CONFIG_MAGIC) {
|
||||||
|
throw invalid_argument("invalid client config");
|
||||||
|
}
|
||||||
|
this->bb_game_state = cc.bb_game_state;
|
||||||
|
this->bb_player_index = cc.bb_player_index;
|
||||||
|
this->flags = cc.flags;
|
||||||
|
}
|
||||||
|
|||||||
+16
-13
@@ -19,24 +19,24 @@ enum class ServerBehavior {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct ClientConfig {
|
struct ClientConfig {
|
||||||
uint32_t magic; // must be set to 0x48615467
|
uint64_t magic;
|
||||||
uint8_t bb_game_state; // status of client connecting on BB
|
uint8_t bb_game_state;
|
||||||
uint8_t bb_player_index; // selected char
|
uint8_t bb_player_index;
|
||||||
uint16_t flags; // just in case we lose them somehow between connections
|
uint16_t flags;
|
||||||
uint16_t ports[4]; // used by shipgate clients
|
uint32_t unused[5];
|
||||||
uint32_t unused[4];
|
uint32_t unused_bb_only[2];
|
||||||
};
|
} __attribute__((packed));
|
||||||
|
|
||||||
struct ClientConfigBB {
|
|
||||||
ClientConfig cfg;
|
|
||||||
uint32_t unused[2];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Client {
|
struct Client {
|
||||||
// License & account
|
// License & account
|
||||||
std::shared_ptr<const License> license;
|
std::shared_ptr<const License> license;
|
||||||
ClientConfigBB config;
|
|
||||||
GameVersion version;
|
GameVersion version;
|
||||||
|
|
||||||
|
// Note: these fields are included in the client config. On GC, the client
|
||||||
|
// config can be up to 0x20 bytes; on BB it can be 0x28 bytes. We don't use
|
||||||
|
// all of that space.
|
||||||
|
uint8_t bb_game_state;
|
||||||
|
uint8_t bb_player_index;
|
||||||
uint16_t flags;
|
uint16_t flags;
|
||||||
|
|
||||||
// Encryption
|
// Encryption
|
||||||
@@ -78,4 +78,7 @@ struct Client {
|
|||||||
|
|
||||||
// adds data to the client's output buffer, encrypting it first
|
// adds data to the client's output buffer, encrypting it first
|
||||||
bool send(std::string&& data);
|
bool send(std::string&& data);
|
||||||
|
|
||||||
|
ClientConfig export_config() const;
|
||||||
|
void import_config(const ClientConfig& cc);
|
||||||
};
|
};
|
||||||
|
|||||||
+37
-29
@@ -90,7 +90,7 @@ void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case ServerBehavior::LoginServer: {
|
case ServerBehavior::LoginServer: {
|
||||||
if (!s->welcome_message.empty() && !(c->flags & ClientFlag::NoMessageBoxCloseConfirmation)) {
|
if (!s->welcome_message.empty()) {
|
||||||
c->flags |= ClientFlag::AtWelcomeMessage;
|
c->flags |= ClientFlag::AtWelcomeMessage;
|
||||||
}
|
}
|
||||||
send_server_init(s, c, true);
|
send_server_init(s, c, true);
|
||||||
@@ -121,7 +121,7 @@ void process_login_complete(shared_ptr<ServerState> s, shared_ptr<Client> c) {
|
|||||||
send_ep3_rank_update(c);
|
send_ep3_rank_update(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s->welcome_message.empty()) {
|
if (s->welcome_message.empty() || (c->flags & ClientFlag::NoMessageBoxCloseConfirmation)) {
|
||||||
c->flags &= ~ClientFlag::AtWelcomeMessage;
|
c->flags &= ~ClientFlag::AtWelcomeMessage;
|
||||||
send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false);
|
send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false);
|
||||||
} else {
|
} else {
|
||||||
@@ -139,10 +139,10 @@ void process_login_complete(shared_ptr<ServerState> s, shared_ptr<Client> c) {
|
|||||||
c->player.load_account_data("system/blueburst/default.nsa");
|
c->player.load_account_data("system/blueburst/default.nsa");
|
||||||
}
|
}
|
||||||
|
|
||||||
sprintf(c->player.bank_name, "player%d", c->config.cfg.bb_player_index + 1);
|
sprintf(c->player.bank_name, "player%d", c->bb_player_index + 1);
|
||||||
|
|
||||||
string player_filename = filename_for_player_bb(c->license->username,
|
string player_filename = filename_for_player_bb(c->license->username,
|
||||||
c->config.cfg.bb_player_index);
|
c->bb_player_index);
|
||||||
try {
|
try {
|
||||||
c->player.load_player_data(player_filename);
|
c->player.load_player_data(player_filename);
|
||||||
} catch (const exception&) {
|
} catch (const exception&) {
|
||||||
@@ -181,7 +181,7 @@ void process_disconnect(shared_ptr<ServerState> s, shared_ptr<Client> c) {
|
|||||||
c->player.disp.play_time += ((now() - c->play_time_begin) / 1000000);
|
c->player.disp.play_time += ((now() - c->play_time_begin) / 1000000);
|
||||||
string account_filename = filename_for_account_bb(c->license->username);
|
string account_filename = filename_for_account_bb(c->license->username);
|
||||||
string player_filename = filename_for_player_bb(c->license->username,
|
string player_filename = filename_for_player_bb(c->license->username,
|
||||||
c->config.cfg.bb_player_index);
|
c->bb_player_index);
|
||||||
string bank_filename = filename_for_bank_bb(c->license->username,
|
string bank_filename = filename_for_bank_bb(c->license->username,
|
||||||
c->player.bank_name);
|
c->player.bank_name);
|
||||||
c->player.save_account_data(account_filename);
|
c->player.save_account_data(account_filename);
|
||||||
@@ -325,8 +325,11 @@ void process_login_d_e_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
|||||||
char access_key[0x10];
|
char access_key[0x10];
|
||||||
char unused3[0x60];
|
char unused3[0x60];
|
||||||
char name[0x10];
|
char name[0x10];
|
||||||
|
// Note: there are 8 bytes at the end of cfg that are technically not
|
||||||
|
// included in the client config on GC, but the field after it is
|
||||||
|
// sufficiently large and unused anyway
|
||||||
ClientConfig cfg;
|
ClientConfig cfg;
|
||||||
uint8_t unused4[0x64];
|
uint8_t unused4[0x5C];
|
||||||
};
|
};
|
||||||
// sometimes the unused bytes aren't sent?
|
// sometimes the unused bytes aren't sent?
|
||||||
check_size(size, sizeof(Cmd) - 0x64, sizeof(Cmd));
|
check_size(size, sizeof(Cmd) - 0x64, sizeof(Cmd));
|
||||||
@@ -361,11 +364,11 @@ void process_login_d_e_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(&c->config.cfg, &cmd->cfg, sizeof(ClientConfig));
|
try {
|
||||||
|
c->import_config(cmd->cfg);
|
||||||
if (c->config.cfg.magic != CONFIG_MAGIC) {
|
} catch (const invalid_argument&) {
|
||||||
memset(&c->config, 0, sizeof(ClientConfigBB));
|
c->bb_game_state = 0;
|
||||||
c->config.cfg.magic = CONFIG_MAGIC;
|
c->bb_player_index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
send_update_client_config(c);
|
send_update_client_config(c);
|
||||||
@@ -381,7 +384,7 @@ void process_login_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
|||||||
char unused2[0x20];
|
char unused2[0x20];
|
||||||
char password[0x10];
|
char password[0x10];
|
||||||
char unused3[0x30];
|
char unused3[0x30];
|
||||||
ClientConfigBB cfg;
|
ClientConfig cfg;
|
||||||
};
|
};
|
||||||
check_size(size, sizeof(Cmd));
|
check_size(size, sizeof(Cmd));
|
||||||
const auto* cmd = reinterpret_cast<const Cmd*>(data);
|
const auto* cmd = reinterpret_cast<const Cmd*>(data);
|
||||||
@@ -397,18 +400,17 @@ void process_login_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(&c->config, &cmd->cfg, sizeof(ClientConfigBB));
|
try {
|
||||||
|
c->import_config(cmd->cfg);
|
||||||
if (c->config.cfg.magic != CONFIG_MAGIC) {
|
c->bb_game_state++;
|
||||||
memset(&c->config, 0, sizeof(ClientConfigBB));
|
} catch (const invalid_argument&) {
|
||||||
c->config.cfg.magic = CONFIG_MAGIC;
|
c->bb_game_state = 0;
|
||||||
} else {
|
c->bb_player_index = 0;
|
||||||
c->config.cfg.bb_game_state++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send_client_init_bb(c, 0);
|
send_client_init_bb(c, 0);
|
||||||
|
|
||||||
switch (c->config.cfg.bb_game_state) {
|
switch (c->bb_game_state) {
|
||||||
case ClientStateBB::InitialLogin:
|
case ClientStateBB::InitialLogin:
|
||||||
// first login? send them to the other port
|
// first login? send them to the other port
|
||||||
send_reconnect(c, s->connect_address_for_client(c),
|
send_reconnect(c, s->connect_address_for_client(c),
|
||||||
@@ -871,7 +873,7 @@ void process_change_lobby(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
|||||||
try {
|
try {
|
||||||
new_lobby = s->find_lobby(cmd->item_id);
|
new_lobby = s->find_lobby(cmd->item_id);
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby does not exist.");
|
send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby does not\nexist.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,9 +994,9 @@ void process_gba_file_request(shared_ptr<ServerState>, shared_ptr<Client> c,
|
|||||||
// player data commands
|
// player data commands
|
||||||
|
|
||||||
void process_player_data(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
void process_player_data(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||||
uint16_t command, uint32_t, uint16_t size, const void* data) { // 61 98
|
uint16_t command, uint32_t flag, uint16_t size, const void* data) { // 61 98
|
||||||
|
|
||||||
// note: we add extra buffer on the end when checking sizes because the
|
// Note: we add extra buffer on the end when checking sizes because the
|
||||||
// autoreply text is a variable length
|
// autoreply text is a variable length
|
||||||
switch (c->version) {
|
switch (c->version) {
|
||||||
case GameVersion::PC:
|
case GameVersion::PC:
|
||||||
@@ -1002,7 +1004,12 @@ void process_player_data(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
|||||||
c->player.import(*reinterpret_cast<const PSOPlayerDataPC*>(data));
|
c->player.import(*reinterpret_cast<const PSOPlayerDataPC*>(data));
|
||||||
break;
|
break;
|
||||||
case GameVersion::GC:
|
case GameVersion::GC:
|
||||||
|
if (flag == 4) { // Episode 3
|
||||||
|
check_size(size, sizeof(PSOPlayerDataGC) + 0x23FC);
|
||||||
|
// TODO: import Episode 3 data somewhere
|
||||||
|
} else {
|
||||||
check_size(size, sizeof(PSOPlayerDataGC), sizeof(PSOPlayerDataGC) + 0xAC);
|
check_size(size, sizeof(PSOPlayerDataGC), sizeof(PSOPlayerDataGC) + 0xAC);
|
||||||
|
}
|
||||||
c->player.import(*reinterpret_cast<const PSOPlayerDataGC*>(data));
|
c->player.import(*reinterpret_cast<const PSOPlayerDataGC*>(data));
|
||||||
break;
|
break;
|
||||||
case GameVersion::BB:
|
case GameVersion::BB:
|
||||||
@@ -1151,9 +1158,9 @@ void process_player_preview_request_bb(shared_ptr<ServerState>, shared_ptr<Clien
|
|||||||
check_size(size, sizeof(Cmd));
|
check_size(size, sizeof(Cmd));
|
||||||
const auto* cmd = reinterpret_cast<const Cmd*>(data);
|
const auto* cmd = reinterpret_cast<const Cmd*>(data);
|
||||||
|
|
||||||
if (c->config.cfg.bb_game_state == ClientStateBB::ChoosePlayer) {
|
if (c->bb_game_state == ClientStateBB::ChoosePlayer) {
|
||||||
c->config.cfg.bb_player_index = cmd->player_index;
|
c->bb_player_index = cmd->player_index;
|
||||||
c->config.cfg.bb_game_state++;
|
c->bb_game_state++;
|
||||||
send_client_init_bb(c, 0);
|
send_client_init_bb(c, 0);
|
||||||
send_approve_player_choice_bb(c);
|
send_approve_player_choice_bb(c);
|
||||||
} else {
|
} else {
|
||||||
@@ -1162,7 +1169,7 @@ void process_player_preview_request_bb(shared_ptr<ServerState>, shared_ptr<Clien
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
string filename = filename_for_player_bb(c->license->username,
|
string filename = filename_for_player_bb(c->license->username,
|
||||||
c->config.cfg.bb_player_index);
|
c->bb_player_index);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// generate a preview
|
// generate a preview
|
||||||
@@ -1235,7 +1242,7 @@ void process_create_character_bb(shared_ptr<ServerState> s, shared_ptr<Client> c
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
c->config.cfg.bb_player_index = cmd->player_index;
|
c->bb_player_index = cmd->player_index;
|
||||||
snprintf(c->player.bank_name, 0x20, "player%" PRIu32, cmd->player_index + 1);
|
snprintf(c->player.bank_name, 0x20, "player%" PRIu32, cmd->player_index + 1);
|
||||||
string player_filename = filename_for_player_bb(c->license->username, cmd->player_index);
|
string player_filename = filename_for_player_bb(c->license->username, cmd->player_index);
|
||||||
string bank_filename = filename_for_bank_bb(c->license->username, c->player.bank_name);
|
string bank_filename = filename_for_bank_bb(c->license->username, c->player.bank_name);
|
||||||
@@ -2033,7 +2040,8 @@ static process_command_t gc_handlers[0x100] = {
|
|||||||
process_ep3_menu_challenge, nullptr, nullptr, nullptr,
|
process_ep3_menu_challenge, nullptr, nullptr, nullptr,
|
||||||
|
|
||||||
// E0
|
// E0
|
||||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
nullptr, nullptr, nullptr, nullptr,
|
||||||
|
process_ignored_command, nullptr, nullptr, nullptr,
|
||||||
nullptr, nullptr, nullptr, nullptr,
|
nullptr, nullptr, nullptr, nullptr,
|
||||||
process_create_game_dc_gc, nullptr, nullptr, nullptr,
|
process_create_game_dc_gc, nullptr, nullptr, nullptr,
|
||||||
|
|
||||||
|
|||||||
+15
-7
@@ -242,7 +242,7 @@ void send_update_client_config(shared_ptr<Client> c) {
|
|||||||
} cmd = {
|
} cmd = {
|
||||||
0x00010000,
|
0x00010000,
|
||||||
c->license->serial_number,
|
c->license->serial_number,
|
||||||
c->config.cfg,
|
c->export_config(),
|
||||||
};
|
};
|
||||||
send_command(c, 0x04, 0x00, cmd);
|
send_command(c, 0x04, 0x00, cmd);
|
||||||
}
|
}
|
||||||
@@ -295,14 +295,14 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error) {
|
|||||||
uint32_t player_tag;
|
uint32_t player_tag;
|
||||||
uint32_t serial_number;
|
uint32_t serial_number;
|
||||||
uint32_t team_id; // just randomize it; teams aren't supported
|
uint32_t team_id; // just randomize it; teams aren't supported
|
||||||
ClientConfigBB cfg;
|
ClientConfig cfg;
|
||||||
uint32_t caps; // should be 0x00000102
|
uint32_t caps; // should be 0x00000102
|
||||||
} cmd = {
|
} cmd = {
|
||||||
error,
|
error,
|
||||||
0x00010000,
|
0x00010000,
|
||||||
c->license->serial_number,
|
c->license->serial_number,
|
||||||
static_cast<uint32_t>(random_object<uint32_t>()),
|
static_cast<uint32_t>(random_object<uint32_t>()),
|
||||||
c->config,
|
c->export_config(),
|
||||||
0x00000102,
|
0x00000102,
|
||||||
};
|
};
|
||||||
send_command(c, 0x00E6, 0x00000000, cmd);
|
send_command(c, 0x00E6, 0x00000000, cmd);
|
||||||
@@ -440,7 +440,7 @@ void send_approve_player_choice_bb(shared_ptr<Client> c) {
|
|||||||
struct {
|
struct {
|
||||||
uint32_t player_index;
|
uint32_t player_index;
|
||||||
uint32_t unused;
|
uint32_t unused;
|
||||||
} cmd = {c->config.cfg.bb_player_index, 1};
|
} cmd = {c->bb_player_index, 1};
|
||||||
send_command(c, 0x00E4, 0x00000000, cmd);
|
send_command(c, 0x00E4, 0x00000000, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1286,9 +1286,9 @@ void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void send_lobby_list(shared_ptr<Client> c, shared_ptr<ServerState> s) {
|
void send_lobby_list(shared_ptr<Client> c, shared_ptr<ServerState> s) {
|
||||||
// this command appeast to be deprecated, as PSO expects it to be exactly how
|
// This command appears to be deprecated, as PSO expects it to be exactly how
|
||||||
// this server sends it, and does not react if it's different, except by
|
// this server sends it, and does not react if it's different, except by
|
||||||
// changing the lobby IDs
|
// changing the lobby IDs.
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
uint32_t menu_id;
|
uint32_t menu_id;
|
||||||
@@ -1301,7 +1301,7 @@ void send_lobby_list(shared_ptr<Client> c, shared_ptr<ServerState> s) {
|
|||||||
if (!(l->flags & LobbyFlag::Default)) {
|
if (!(l->flags & LobbyFlag::Default)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(c->flags & ClientFlag::Episode3Games) != !(l->flags & LobbyFlag::Episode3)) {
|
if ((l->flags & LobbyFlag::Episode3) && !(c->flags & ClientFlag::Episode3Games)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,6 +1643,14 @@ void send_join_lobby(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
|||||||
throw logic_error("unimplemented versioned command");
|
throw logic_error("unimplemented versioned command");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the client will stop sending message box close confirmations after
|
||||||
|
// joining any lobby, set the appropriate flag and update the client config
|
||||||
|
if ((c->flags & (ClientFlag::NoMessageBoxCloseConfirmationAfterLobbyJoin | ClientFlag::NoMessageBoxCloseConfirmation))
|
||||||
|
== ClientFlag::NoMessageBoxCloseConfirmationAfterLobbyJoin) {
|
||||||
|
c->flags |= ClientFlag::NoMessageBoxCloseConfirmation;
|
||||||
|
send_update_client_config(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -13,24 +13,26 @@ enum class GameVersion {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum ClientFlag {
|
enum ClientFlag {
|
||||||
// after joining a lobby, client will no longer send D6 commands when they close message boxes
|
// After joining a lobby, client will no longer send D6 commands when they close message boxes
|
||||||
NoMessageBoxCloseConfirmationAfterLobbyJoin = 0x0004,
|
NoMessageBoxCloseConfirmationAfterLobbyJoin = 0x0004,
|
||||||
// client has the above flag and has already joined a lobby
|
// Client has the above flag and has already joined a lobby
|
||||||
NoMessageBoxCloseConfirmation = 0x0008,
|
NoMessageBoxCloseConfirmation = 0x0008,
|
||||||
// client can see Ep3 lobbies
|
// Client can see Ep3 lobbies
|
||||||
CanSeeExtraLobbies = 0x0010,
|
CanSeeExtraLobbies = 0x0010,
|
||||||
// client is episode 3 and should use its game mechanic
|
// Client is episode 3 and should use its game mechanic
|
||||||
Episode3Games = 0x0020,
|
Episode3Games = 0x0020,
|
||||||
// client is DC v1 (disables some features)
|
// Client is DC v1 (disables some features)
|
||||||
IsDCv1 = 0x0040,
|
IsDCv1 = 0x0040,
|
||||||
// client is loading into a game
|
// Client is loading into a game
|
||||||
Loading = 0x0080,
|
Loading = 0x0080,
|
||||||
|
|
||||||
// client is in the information menu (login server only)
|
// Client is in the information menu (login server only)
|
||||||
InInformationMenu = 0x0100,
|
InInformationMenu = 0x0100,
|
||||||
// client is at the welcome message (login server only)
|
// Client is at the welcome message (login server only)
|
||||||
AtWelcomeMessage = 0x0200,
|
AtWelcomeMessage = 0x0200,
|
||||||
|
|
||||||
|
// Note: There isn't a good way to detect Episode 3 until the player data is
|
||||||
|
// sent (via a 61 command), so the Episode3Games flag is set in that handler
|
||||||
DefaultV1 = IsDCv1,
|
DefaultV1 = IsDCv1,
|
||||||
DefaultV2DC = 0x0000,
|
DefaultV2DC = 0x0000,
|
||||||
DefaultV2PC = 0x0000,
|
DefaultV2PC = 0x0000,
|
||||||
|
|||||||
Reference in New Issue
Block a user