document hardware_id in login commands

This commit is contained in:
Martin Michelsen
2025-01-10 22:13:57 -08:00
parent 0704590238
commit 4d3595640a
10 changed files with 129 additions and 60 deletions
+41 -31
View File
@@ -347,7 +347,7 @@ struct S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B {
// TODO: Are the DCv1 and DCv2 formats the same as this structure?
struct C_LegacyLogin_PC_V3_03 {
/* 00 */ le_uint64_t unused = 0; // Same as unused field in 9D/9E
/* 00 */ be_uint64_t hardware_id;
/* 08 */ le_uint32_t sub_version = 0;
/* 0C */ uint8_t is_extended = 0;
/* 0D */ uint8_t language = 0;
@@ -403,7 +403,7 @@ struct S_ServerInitWithAfterMessageT_BB_03_9B {
// TODO: Are the DCv1 and DCv2 formats the same as this structure?
struct C_LegacyLogin_PC_V3_04 {
/* 00 */ le_uint64_t unused1 = 0; // Same as unused field in 9D/9E
/* 00 */ be_uint64_t hardware_id;
/* 08 */ le_uint32_t sub_version = 0;
/* 0C */ uint8_t is_extended = 0;
/* 0D */ uint8_t language = 0;
@@ -1594,7 +1594,7 @@ struct S_ArrowUpdateEntry_88 {
// The server should respond with an 8A command.
struct C_ConnectionInfo_DCNTE_8A {
pstring<TextEncoding::ASCII, 0x08> hardware_id;
be_uint64_t hardware_id;
le_uint32_t sub_version = 0x20;
le_uint32_t unknown_a1 = 0;
pstring<TextEncoding::ASCII, 0x30> username;
@@ -1624,7 +1624,7 @@ struct C_ConnectionInfo_DCNTE_8A {
struct C_Login_DCNTE_8B {
le_uint32_t player_tag = 0x00010000;
le_uint32_t guild_card_number = 0;
parray<uint8_t, 0x08> hardware_id;
be_uint64_t hardware_id;
le_uint32_t sub_version = 0x20;
uint8_t is_extended = 0;
uint8_t language = 0;
@@ -1685,13 +1685,13 @@ struct C_LoginV1_DC_PC_V3_90 {
// 92 (C->S): Register (DC)
struct C_RegisterV1_DC_92 {
parray<uint8_t, 0x08> unknown_a1;
be_uint64_t hardware_id;
le_uint32_t sub_version;
uint8_t is_extended = 0; // TODO: This is a guess
uint8_t language = 0; // TODO: This is a guess; verify it
parray<uint8_t, 2> unknown_a3;
pstring<TextEncoding::ASCII, 0x30> hardware_id;
pstring<TextEncoding::ASCII, 0x30> unknown_a4;
pstring<TextEncoding::ASCII, 0x30> serial_number2;
pstring<TextEncoding::ASCII, 0x30> access_key2;
pstring<TextEncoding::ASCII, 0x30> email; // According to Sylverant documentation
} __packed_ws__(C_RegisterV1_DC_92, 0xA0);
@@ -1702,20 +1702,20 @@ struct C_RegisterV1_DC_92 {
// 93 (C->S): Log in (DCv1)
struct C_LoginV1_DC_93 {
le_uint32_t player_tag = 0x00010000;
le_uint32_t guild_card_number = 0;
le_uint32_t unknown_a1 = 0;
le_uint32_t unknown_a2 = 0;
le_uint32_t sub_version = 0;
uint8_t is_extended = 0;
uint8_t language = 0;
parray<uint8_t, 2> unused1;
pstring<TextEncoding::ASCII, 0x11> serial_number;
pstring<TextEncoding::ASCII, 0x11> access_key;
pstring<TextEncoding::ASCII, 0x30> hardware_id;
pstring<TextEncoding::ASCII, 0x30> unknown_a3;
pstring<TextEncoding::ASCII, 0x10> name;
parray<uint8_t, 2> unused2;
/* 00 */ le_uint32_t player_tag = 0x00010000;
/* 04 */ le_uint32_t guild_card_number = 0;
/* 08 */ be_uint64_t hardware_id;
/* 10 */ le_uint32_t sub_version = 0;
/* 14 */ uint8_t is_extended = 0;
/* 15 */ uint8_t language = 0;
/* 16 */ parray<uint8_t, 2> unused1;
/* 18 */ pstring<TextEncoding::ASCII, 0x11> serial_number;
/* 29 */ pstring<TextEncoding::ASCII, 0x11> access_key;
/* 3A */ pstring<TextEncoding::ASCII, 0x30> serial_number2;
/* 6A */ pstring<TextEncoding::ASCII, 0x30> access_key2;
/* 9A */ pstring<TextEncoding::ASCII, 0x10> name;
/* AA */ parray<uint8_t, 2> unused2;
/* AC */
} __packed_ws__(C_LoginV1_DC_93, 0xAC);
struct C_LoginExtendedV1_DC_93 : C_LoginV1_DC_93 {
@@ -1886,14 +1886,15 @@ struct C_Login_DC_PC_V3_9A {
// It appears PSO GC sends uninitialized data in the header.flag field here.
struct C_Register_DC_PC_V3_9C {
le_uint64_t unused = 0;
le_uint32_t sub_version = 0;
uint8_t unused1 = 0;
uint8_t language = 0;
parray<uint8_t, 2> unused2;
pstring<TextEncoding::ASCII, 0x30> serial_number; // On XB, this is the XBL gamertag
pstring<TextEncoding::ASCII, 0x30> access_key; // On XB, this is the XBL user ID
pstring<TextEncoding::ASCII, 0x30> password; // On XB, this contains "xbox-pso"
/* 00 */ be_uint64_t hardware_id;
/* 08 */ le_uint32_t sub_version = 0;
/* 0C */ uint8_t unused1 = 0;
/* 0D */ uint8_t language = 0;
/* 0E */ parray<uint8_t, 2> unused2;
/* 10 */ pstring<TextEncoding::ASCII, 0x30> serial_number; // On XB, this is the XBL gamertag
/* 40 */ pstring<TextEncoding::ASCII, 0x30> access_key; // On XB, this is the XBL user ID
/* 70 */ pstring<TextEncoding::ASCII, 0x30> password; // On XB, this contains "xbox-pso"
/* A0 */
} __packed_ws__(C_Register_DC_PC_V3_9C, 0xA0);
struct C_Register_BB_9C {
@@ -1925,8 +1926,17 @@ struct C_Register_BB_9C {
struct C_Login_DC_PC_GC_9D {
/* 00 */ le_uint32_t player_tag = 0x00010000; // 0x00010000 if guild card is set (via 04)
/* 04 */ le_uint32_t guild_card_number = 0; // 0xFFFFFFFF if not set
/* 08 */ le_uint32_t unused1 = 0;
/* 0C */ le_uint32_t unused2 = 0;
// The hardware ID is different for various PSO versions:
// - All DC versions: the hardware ID comes from the FUNC_SYSINFO_ID syscall
// (as KallistiOS refers to it), which returns a 64-bit integer. PSO uses
// the low 48 bits; the high 16 bits are masked out and are always zero.
// - PC V2: the hardware ID is always 0000FFFFFFFFFFFF
// - GC NTE: the last byte of the hardware ID is uninitialized memory from
// the TProtocol constructor's stack; the other bytes are all zeroes.
// - V3: the hardware ID is all zeroes.
// On the client, this is actually an array of 8 bytes, but we treat it as a
// single integer for simplicity.
/* 08 */ be_uint64_t hardware_id;
/* 10 */ le_uint32_t sub_version = 0;
/* 14 */ uint8_t is_extended = 0; // If 1, structure has extended format
/* 15 */ uint8_t language = 0; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES
+12 -5
View File
@@ -48,7 +48,7 @@ DownloadSession::DownloadSession(
Version version,
uint8_t language,
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
uint32_t hardware_id,
uint32_t serial_number2,
uint32_t serial_number,
const std::string& access_key,
const std::string& username,
@@ -63,7 +63,7 @@ DownloadSession::DownloadSession(
bool show_command_data)
: output_dir(output_dir),
bb_key_file(bb_key_file),
hardware_id(hardware_id),
serial_number2(serial_number2),
serial_number(serial_number),
access_key(access_key),
username(username),
@@ -86,6 +86,7 @@ DownloadSession::DownloadSession(
phosg::render_sockaddr_storage(remote),
show_command_data ? phosg::TerminalFormat::FG_GREEN : phosg::TerminalFormat::END,
show_command_data ? phosg::TerminalFormat::FG_YELLOW : phosg::TerminalFormat::END),
hardware_id(generate_random_hardware_id(this->channel.version)),
guild_card_number(0),
prev_cmd_data(0),
client_config(0),
@@ -103,7 +104,7 @@ DownloadSession::DownloadSession(
switch (this->channel.version) {
case Version::DC_V1:
case Version::DC_V2:
if (this->hardware_id == 0 || this->serial_number == 0 || this->access_key.empty()) {
if (this->serial_number2 == 0 || this->serial_number == 0 || this->access_key.empty()) {
throw runtime_error("missing credentials");
}
break;
@@ -163,12 +164,13 @@ void DownloadSession::send_93_9D_9E(bool extended) {
C_LoginExtendedV1_DC_93 ret;
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
ret.guild_card_number = this->guild_card_number;
ret.hardware_id = this->hardware_id;
ret.sub_version = default_sub_version_for_version(this->channel.version);
ret.is_extended = extended ? 1 : 0;
ret.language = this->channel.language;
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
ret.access_key.encode(this->access_key);
ret.hardware_id.encode(phosg::string_printf("%08" PRIX32, this->hardware_id));
ret.serial_number2.encode(phosg::string_printf("%08" PRIX32, this->serial_number2));
ret.name.encode(this->character->disp.name.decode());
this->channel.send(0x93, 0x01, &ret, extended ? sizeof(ret) : sizeof(C_LoginV1_DC_93));
@@ -176,6 +178,7 @@ void DownloadSession::send_93_9D_9E(bool extended) {
C_LoginExtended_PC_9D ret;
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
ret.guild_card_number = this->guild_card_number;
ret.hardware_id = this->hardware_id;
ret.sub_version = default_sub_version_for_version(this->channel.version);
ret.is_extended = extended ? 1 : 0;
ret.language = this->channel.language;
@@ -193,6 +196,7 @@ void DownloadSession::send_93_9D_9E(bool extended) {
C_LoginExtended_GC_9E ret;
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
ret.guild_card_number = this->guild_card_number;
ret.hardware_id = this->hardware_id;
ret.sub_version = default_sub_version_for_version(this->channel.version);
ret.is_extended = extended ? 1 : 0;
ret.language = this->channel.language;
@@ -208,6 +212,7 @@ void DownloadSession::send_93_9D_9E(bool extended) {
C_LoginExtended_XB_9E ret;
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
ret.guild_card_number = this->guild_card_number;
ret.hardware_id = this->hardware_id;
ret.sub_version = default_sub_version_for_version(this->channel.version);
ret.is_extended = extended ? 1 : 0;
ret.language = this->channel.language;
@@ -377,13 +382,15 @@ void DownloadSession::on_channel_input(uint16_t command, uint32_t flag, std::str
if (flag == 1) {
if (is_v1(this->channel.version)) {
C_RegisterV1_DC_92 ret;
ret.hardware_id = this->hardware_id;
ret.sub_version = default_sub_version_for_version(this->channel.version);
ret.language = this->channel.language;
ret.hardware_id.encode(phosg::string_printf("%08" PRIX32, this->hardware_id));
ret.serial_number2.encode(phosg::string_printf("%08" PRIX32, this->serial_number2));
this->channel.send(0x92, 0x00, ret);
} else if (!is_v4(this->channel.version)) {
C_Register_DC_PC_V3_9C ret;
ret.hardware_id = this->hardware_id;
ret.sub_version = default_sub_version_for_version(this->channel.version);
ret.language = this->channel.language;
if (this->channel.version == Version::XB_V3) {
+3 -2
View File
@@ -24,7 +24,7 @@ public:
Version version,
uint8_t language,
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
uint32_t hardware_id,
uint32_t serial_number2,
uint32_t serial_number,
const std::string& access_key,
const std::string& username,
@@ -47,7 +47,7 @@ protected:
// Config (must be set by caller)
std::string output_dir;
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file;
uint32_t hardware_id;
uint32_t serial_number2;
uint32_t serial_number;
std::string access_key;
std::string username;
@@ -64,6 +64,7 @@ protected:
phosg::PrefixedLogger log;
std::shared_ptr<struct event_base> base;
Channel channel;
uint64_t hardware_id;
uint32_t guild_card_number;
parray<uint8_t, 0x28> prev_cmd_data;
parray<uint8_t, 0x20> client_config;
+1 -1
View File
@@ -734,7 +734,7 @@ phosg::JSON HTTPServer::generate_proxy_client_json_st(shared_ptr<const ProxyServ
{"Version", phosg::name_for_enum(ses->version())},
{"SubVersion", ses->sub_version},
{"Name", ses->character_name},
{"DCHardwareID", ses->hardware_id},
{"DCSerialNumber2", ses->serial_number2},
{"RemoteGuildCardNumber", ses->remote_guild_card_number},
{"RemoteClientConfigData", phosg::format_data_string(&ses->remote_client_config_data[0], ses->remote_client_config_data.size())},
{"Config", HTTPServer::generate_client_config_json_st(ses->config)},
+8 -13
View File
@@ -167,8 +167,7 @@ static HandlerResult S_G_9A(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.hardware_id = ses->hardware_id;
cmd.sub_version = ses->effective_sub_version();
cmd.is_extended = (ses->remote_guild_card_number < 0) ? 1 : 0;
cmd.language = ses->language();
@@ -277,15 +276,14 @@ static HandlerResult S_V123P_02_17(
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.unknown_a1 = 0;
cmd.unknown_a2 = 0;
cmd.hardware_id = ses->hardware_id;
cmd.sub_version = ses->effective_sub_version();
cmd.is_extended = 0;
cmd.language = ses->language();
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->dc_license->serial_number));
cmd.access_key.encode(ses->login->dc_license->access_key);
cmd.access_key.clear_after_bytes(8);
cmd.hardware_id.encode(ses->hardware_id);
cmd.serial_number2.encode(ses->serial_number2);
cmd.name.encode(ses->character_name);
ses->server_channel.send(0x93, 0x00, &cmd, sizeof(cmd));
return HandlerResult::Type::SUPPRESS;
@@ -334,7 +332,7 @@ static HandlerResult S_V123P_02_17(
cmd.access_key.clear_after_bytes(8);
}
if (is_dc(ses->version())) {
cmd.serial_number2.encode(ses->hardware_id);
cmd.serial_number2.encode(ses->serial_number2);
} else {
cmd.serial_number2 = cmd.serial_number;
}
@@ -353,8 +351,7 @@ static HandlerResult S_V123P_02_17(
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.hardware_id = ses->hardware_id;
cmd.sub_version = ses->effective_sub_version();
cmd.is_extended = 0;
cmd.language = ses->language();
@@ -364,7 +361,7 @@ static HandlerResult S_V123P_02_17(
cmd.access_key.clear_after_bytes(8);
}
if (is_dc(ses->version())) {
cmd.serial_number2.encode(ses->hardware_id);
cmd.serial_number2.encode(ses->serial_number2);
} else {
cmd.serial_number2 = cmd.serial_number;
}
@@ -417,8 +414,7 @@ static HandlerResult S_V123P_02_17(
C_LoginExtended_GC_9E cmd;
cmd.player_tag = 0x00010000;
cmd.guild_card_number = guild_card_number;
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.hardware_id = ses->hardware_id;
cmd.sub_version = ses->effective_sub_version();
cmd.is_extended = 0;
cmd.language = ses->language();
@@ -453,8 +449,7 @@ static HandlerResult S_V123P_02_17(
cmd.player_tag = 0x00010000;
cmd.guild_card_number = ses->remote_guild_card_number;
}
cmd.unused1 = 0;
cmd.unused2 = 0;
cmd.hardware_id = ses->hardware_id;
cmd.sub_version = ses->effective_sub_version();
cmd.is_extended = (ses->remote_guild_card_number < 0) ? 1 : 0;
cmd.language = ses->language();
+16 -4
View File
@@ -306,11 +306,13 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->log.info("Version changed to DC_NTE");
ses->config.specific_version = SPECIFIC_VERSION_DC_NTE;
const auto& cmd = check_size_t<C_Login_DCNTE_8B>(data, sizeof(C_LoginExtended_DCNTE_8B));
ses->set_login(s->account_index->from_dc_nte_credentials(cmd.serial_number.decode(), cmd.access_key.decode(), false));
ses->set_login(s->account_index->from_dc_nte_credentials(
cmd.serial_number.decode(), cmd.access_key.decode(), false));
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
// TODO: Parse cmd.hardware_id
ses->hardware_id = cmd.hardware_id;
} else if (command == 0x93) { // 11/2000 proto through DC V1
ses->channel.version = Version::DC_V1;
ses->log.info("Version changed to DC_V1");
@@ -323,7 +325,9 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
ses->hardware_id = cmd.hardware_id.decode();
ses->serial_number2 = cmd.serial_number2.decode();
ses->hardware_id = cmd.hardware_id;
} else if (command == 0x9D) {
const auto& cmd = check_size_t<C_Login_DC_PC_GC_9D>(data, sizeof(C_LoginExtended_DC_GC_9D));
if (cmd.sub_version >= 0x30) {
@@ -345,6 +349,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
ses->config.set_flags_for_version(ses->version(), cmd.sub_version);
ses->hardware_id = cmd.hardware_id;
} else {
throw runtime_error("command is not 93 or 9D");
}
@@ -362,6 +368,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
ses->hardware_id = cmd.hardware_id;
break;
}
@@ -376,6 +383,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
ses->hardware_id = cmd.hardware_id;
ses->config.parse_from(cmd.client_config);
if (cmd.sub_version >= 0x40) {
ses->log.info("Version changed to GC_EP3");
@@ -402,6 +410,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->character_name = cmd.name.decode(ses->channel.language);
ses->xb_netloc = cmd.netloc;
ses->xb_9E_unknown_a1a = cmd.unknown_a1a;
ses->hardware_id = cmd.hardware_id;
ses->channel.send(0x9F, 0x00);
return;
} else if (command == 0x9F) {
@@ -478,6 +487,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->detector_crypt,
ses->sub_version,
ses->character_name,
ses->serial_number2,
ses->hardware_id,
ses->xb_netloc,
ses->xb_9E_unknown_a1a);
@@ -617,11 +627,13 @@ void ProxyServer::LinkedSession::resume(
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
uint32_t sub_version,
const string& character_name,
const string& hardware_id,
const string& serial_number2,
uint64_t hardware_id,
const XBNetworkLocation& xb_netloc,
const parray<le_uint32_t, 3>& xb_9E_unknown_a1a) {
this->sub_version = sub_version;
this->character_name = character_name;
this->serial_number2 = serial_number2;
this->hardware_id = hardware_id;
this->xb_netloc = xb_netloc;
this->xb_9E_unknown_a1a = xb_9E_unknown_a1a;
+6 -3
View File
@@ -58,7 +58,8 @@ public:
uint32_t sub_version;
std::string character_name;
std::string hardware_id; // Only used for DC sessions
std::string serial_number2; // Only used for DC sessions
uint64_t hardware_id;
std::string login_command_bb;
XBNetworkLocation xb_netloc;
parray<le_uint32_t, 3> xb_9E_unknown_a1a;
@@ -170,7 +171,8 @@ public:
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
uint32_t sub_version,
const std::string& character_name,
const std::string& hardware_id,
const std::string& serial_number2,
uint64_t hardware_id,
const XBNetworkLocation& xb_netloc,
const parray<le_uint32_t, 3>& xb_9E_unknown_a1a);
void resume(
@@ -260,7 +262,8 @@ private:
std::string character_name;
Client::Config config;
std::string login_command_bb;
std::string hardware_id;
std::string serial_number2;
uint64_t hardware_id;
XBNetworkLocation xb_netloc;
parray<le_uint32_t, 3> xb_9E_unknown_a1a;
+15 -1
View File
@@ -703,11 +703,25 @@ CommandDefinition c_delete_license(
fprintf(stderr, "Account %08" PRIX32 " updated\n", account->account_id);
});
CommandDefinition c_lookup(
"lookup", "lookup USER\n\
Find the account for a logged-in user.",
true,
+[](CommandArgs& args) {
auto target = args.s->find_client(&args.args);
if (target->login) {
fprintf(stderr, "Found client %s with account ID %08" PRIX32 "\n",
target->channel.name.c_str(), target->login->account->account_id);
} else {
// This should be impossible
throw std::logic_error("find_client found user who is not logged in");
}
});
CommandDefinition c_kick(
"kick", "kick USER\n\
Disconnect a user from the server. USER may be an account ID, player name,\n\
or client ID (beginning with \"C-\"). This does not ban the user; they are\n\
free to reconnect after doing this.\n",
free to reconnect after doing this.",
true,
+[](CommandArgs& args) {
auto target = args.s->find_client(&args.args);
+25
View File
@@ -382,3 +382,28 @@ const char* file_path_token_for_version(Version version) {
throw runtime_error("invalid game version");
}
}
uint64_t generate_random_hardware_id(Version version) {
switch (version) {
case Version::DC_NTE:
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2:
return phosg::random_object<uint64_t>() & 0x0000FFFFFFFFFFFF;
case Version::PC_NTE:
case Version::PC_V2:
return 0x0000FFFFFFFFFFFF;
case Version::GC_NTE:
// On GC NTE, the low byte is uninitialized memory from the TProtocol
// constructor's stack
return phosg::random_object<uint8_t>();
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
case Version::XB_V3:
case Version::BB_V4:
return 0;
default:
throw runtime_error("invalid game version");
}
}
+2
View File
@@ -235,3 +235,5 @@ template <>
ServerBehavior phosg::enum_for_name<ServerBehavior>(const char* name);
const char* file_path_token_for_version(Version version);
uint64_t generate_random_hardware_id(Version version);